6.鉴权

简介: 本文介绍基于Spring Security与JWT的客户端Token认证方案,涵盖实现思路、核心代码及完整流程。通过自定义过滤器与验证组件,结合RBAC权限模型,实现安全的Token生成与校验,保护Spring Boot应用接口。

1.客户端Token方案

1.1 实现思路

📎简单客户端token校验.vsdx


1.2 实现细节

参考:https://www.cnblogs.com/dalaoyang/p/11783225.html

2.JWT + Security


JWT很大程度上还是个新技术,通过使用HMAC(Hash-based Message Authentication Code)计算信息摘要,也可以用RSA公私钥中的私钥进行签名。这个根据业务场景进行选择。

2.1 pom依赖

<!--JWT依赖开始-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt</artifactId>
  <version>0.7.0</version>
</dependency>
<!--JWT依赖结束-->

/login进行登录并获得Token。剩余接口做token验签,这里我们需要将spring-boot-starter-security加入pom.xml。加入后,我们的Spring Boot项目将需要提供身份验证,相关的pom.xml如下:


至此我们剩余所有的路由都需要身份验证。我们将引入一个安全设置类WebSecurityConfig,这个类需要从WebSecurityConfigurerAdapter类继承。

2.2 安全设置类WebSecurityConfig


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    // 设置 HTTP 验证规则
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 关闭csrf验证
        http.csrf().disable()
                // 对请求进行认证
                .authorizeRequests()
                // 所有 / 的所有请求 都放行
                .antMatchers("/").permitAll()
                // 所有 /login 的POST请求 都放行
                .antMatchers(HttpMethod.POST, "/login").permitAll()
                // 权限检查
                .antMatchers("/hello").hasAuthority("AUTH_WRITE")
                // 角色检查
                .antMatchers("/world").hasRole("ADMIN")
                // 所有请求需要身份认证
                .anyRequest().authenticated()
            .and()
                // 添加一个过滤器 所有访问 /login 的请求交给 JWTLoginFilter 来处理 这个类处理所有的JWT相关内容
                .addFilterBefore(new JWTLoginFilter("/login", authenticationManager()),
                        UsernamePasswordAuthenticationFilter.class)
                // 添加一个过滤器验证其他请求的Token是否合法
                .addFilterBefore(new JWTAuthenticationFilter(),
                        UsernamePasswordAuthenticationFilter.class);
    }
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 使用自定义身份验证组件
        auth.authenticationProvider(new CustomAuthenticationProvider());
    }
    
    // 注入自定义Bean,保证该类能够注入其它Bean,如果没有这步将导致CustomAuthenticationProvider中注入Bean失败
    @Bean
    CustomAuthenticationProvider customAuthenticationProvider() {
        return new CustomAuthenticationProvider();
    }
}


先放两个基本类,一个负责存储用户名密码,另一个是一个权限类型,负责存储权限和角色。

2.3 权限类型及角色类

import org.springframework.security.core.GrantedAuthority;
class GrantedAuthorityImpl implements GrantedAuthority{
    private String authority;
    public GrantedAuthorityImpl(String authority) {
        this.authority = authority;
    }
    public void setAuthority(String authority) {
        this.authority = authority;
    }
    @Override
    public String getAuthority() {
        return this.authority;
    }
}

2.4 用户名密码类

class AccountCredentials {
    private String username;
    private String password;
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
}


在上面的安全设置类中,我们设置所有人都能访问/POST方式访问/login,其他的任何路由都需要进行认证。然后将所有访问/login的请求,都交给JWTLoginFilter过滤器来处理。稍后我们会创建这个过滤器和其他这里需要的JWTAuthenticationFilterCustomAuthenticationProvider两个类。

2.5 JWT生成及验签类


import com.test.framework.client.dto.response.JSONResultDTO;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;
import java.util.List;
class TokenAuthenticationService {
    // 5天(单位ms,需要是24H的整数倍:如0.1倍,1倍,10倍,不能0.34倍)
    static final long EXPIRATIONTIME = 432_000_000;     
    static final String SECRET = "P@ssw02d";            // JWT密码
    static final String TOKEN_PREFIX = "Bearer";        // Token前缀
    static final String HEADER_STRING = "Authorization";// 存放Token的Header Key
  // JWT生成方法
    static void addAuthentication(HttpServletResponse response, String username) {
    // 生成JWT
        String JWT = Jwts.builder()
                // 保存权限(角色)
                .claim("authorities", "ROLE_ADMIN,AUTH_WRITE")
                // 用户名写入标题
                .setSubject(username)
                // 有效期设置
                        .setExpiration(new Date(System.currentTimeMillis() + EXPIRATIONTIME))
                // 签名设置
                        .signWith(SignatureAlgorithm.HS512, SECRET)
                        .compact();
        // 将 JWT 写入 body
        try {
            response.setContentType("application/json");
            response.setStatus(HttpServletResponse.SC_OK);
            response.getOutputStream().println(JSONResult.fillResultString(0, "", JWT));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
  // JWT验证方法
    static Authentication getAuthentication(HttpServletRequest request) {
        // 从Header中拿到token
        String token = request.getHeader(HEADER_STRING);
        if (token != null) {
            // 解析 Token
            Claims claims = Jwts.parser()
                    // 验签
                    .setSigningKey(SECRET)
                    // 去掉 Bearer
                    .parseClaimsJws(token.replace(TOKEN_PREFIX, ""))
                    .getBody();
            // 拿用户名
            String user = claims.getSubject();
            // 得到 权限(角色)
            List<GrantedAuthority> authorities =  AuthorityUtils.commaSeparatedStringToAuthorityList((String) claims.get("authorities"));
            // 返回验证令牌
            return user != null ?
                    new UsernamePasswordAuthenticationToken(user, null, authorities) :
                    null;
        }
        return null;
    }
}


这个类就两个static方法,一个负责生成JWT,一个负责认证JWT最后生成验证令牌。注释已经写得很清楚了,这里不多说了。

下面来看自定义验证组件,这里简单写了,这个类就是提供密码验证功能,在实际使用时换成自己相应的验证逻辑,从数据库中取出、比对、赋予用户相应权限。

2.6 自定义验证组件类


import com.test.framework.web.domain.dbdo.doctor.DoctorDTO;
import com.test.framework.web.domain.dbdo.patient.UserDTO;
import com.test.framework.web.domain.vo.GrantedAuthorityVo;
import com.test.framework.web.service.doctor.DoctorService;
import com.test.framework.web.service.user.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import java.util.ArrayList;
// 自定义身份认证验证组件
class CustomAuthenticationProvider implements AuthenticationProvider {
    
    @Autowired
    private UserService userService;
    
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // 获取认证的用户名 & 密码
        String name = authentication.getName();
        String password = authentication.getCredentials().toString();
        // 认证逻辑,我这里以password为类型,name为真正的查询参数进行DB查询,不同业务场景可以自定义参数查询
        // 验证用户名密码是否存在
        boolean isExist = false;
        if("patient".equalsIgnoreCase(password)) {
            // 查询患者信息是否存在
            UserDTO user = userService.getUserByIdCardNo(name);
            if(null != user) {
                isExist = true;
            }
        }
        if (isExist) {
            // 这里设置权限和角色
            ArrayList<GrantedAuthority> authorities = new ArrayList<>();
            authorities.add( new GrantedAuthorityImpl("ROLE_ADMIN") );
            authorities.add( new GrantedAuthorityImpl("AUTH_WRITE") );
            // 生成令牌
            Authentication auth = new UsernamePasswordAuthenticationToken(name, password, authorities);
            return auth;
        }else {
            throw new BadCredentialsException("密码错误~");
        }
    }
    // 是否可以提供输入类型的认证服务
    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}


2.7 接口类

@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private UserMapper userMapper;
    @Override
    public UserDTO getUserByIdCardNo(String idCardNo) {
        return userMapper.getUserByIdCardNo(idCardNo);
    }
}
@Repository
public interface UserMapper {
    /**
     * 查找患者信息
     * @param idCardNo
     * @return
     */
    UserDTO getUserByIdCardNo(String idCardNo);
}
<select id="getUserByIdCardNo" parameterType="String" resultMap="userEntity">
  SELECT * FROM USER
  <where>
    <if test="idCardNo != null">
      ID_CARD_NO =#{idCardNo}
    </if>
    AND VALID_FLAG='ENABLE'
  </where>
  LIMIT 1
</select>


下面实现JWTLoginFilter 这个Filter比较简单,除了构造函数需要重写三个方法。

  • attemptAuthentication - 登录时需要验证时候调用
  • successfulAuthentication - 验证成功后调用
  • unsuccessfulAuthentication - 验证失败后调用,这里直接灌入500错误返回,由于同一JSON返回,HTTP就都返回200了


2.8 JWTLoginFilter


import com.fasterxml.jackson.databind.ObjectMapper;
import com.test.framework.client.dto.response.JSONResultDTO;
import com.test.framework.web.domain.vo.AccountCredentials;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
class JWTLoginFilter extends AbstractAuthenticationProcessingFilter {
    public JWTLoginFilter(String url, AuthenticationManager authManager) {
        super(new AntPathRequestMatcher(url));
        setAuthenticationManager(authManager);
    }
    @Override
    public Authentication attemptAuthentication(
            HttpServletRequest req, HttpServletResponse res)
            throws AuthenticationException, IOException, ServletException {
        // JSON反序列化成 AccountCredentials
        AccountCredentials creds = new ObjectMapper().readValue(req.getInputStream(), AccountCredentials.class);
        // 返回一个验证令牌
        return getAuthenticationManager().authenticate(
                new UsernamePasswordAuthenticationToken(
                        creds.getUsername(),
                        creds.getPassword()
                )
        );
    }
    @Override
    protected void successfulAuthentication(
            HttpServletRequest req,
            HttpServletResponse res, FilterChain chain,
            Authentication auth) throws IOException, ServletException {
        TokenAuthenticationService.addAuthentication(res, auth.getName());
    }
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        response.setContentType("application/json");
        response.setStatus(HttpServletResponse.SC_OK);
        response.getOutputStream().println(JSONResult.fillResultString(500, "Internal Server Error!!!", JSONObject.NULL));
    }
}


再完成最后一个类JWTAuthenticationFilter,这也是个拦截器,它拦截所有需要JWT的请求,然后调用TokenAuthenticationService类的静态方法去做JWT验证。


2.9 拦截器JWTAuthenticationFilter


import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
class JWTAuthenticationFilter extends GenericFilterBean {
    @Override
    public void doFilter(ServletRequest request,
                         ServletResponse response,
                         FilterChain filterChain)
            throws IOException, ServletException {
        Authentication authentication = TokenAuthenticationService
                .getAuthentication((HttpServletRequest)request);
        SecurityContextHolder.getContext()
                .setAuthentication(authentication);
        filterChain.doFilter(request,response);
    }
}


现在代码就写完了,整个Spring Security结合JWT基本就差不多了,下面我们来测试下,并说下整体流程。

开始测试,先运行整个项目,这里介绍下过程:

  • 先程序启动 - main函数
  • 注册验证组件 - WebSecurityConfigconfigure(AuthenticationManagerBuilder auth)方法,这里我们注册了自定义验证组件
  • 设置验证规则 - WebSecurityConfigconfigure(HttpSecurity http)方法,这里设置了各种路由访问规则
  • 初始化过滤组件 - JWTLoginFilterJWTAuthenticationFilter 类会初始化

首先测试获取Token,这里使用CURL命令行工具来测试。

2.10 验证


curl -H "Content-Type: application/json" -X POST -d '{"username":"admin","password":"123456"}' http://127.0.0.1:8080/login


结果:


{
  "result": "eyJhbGciOiJIUzUxMiJ9.eyJhdXRob3JpdGllcyI6IlJPTEVfQURNSU4sQVVUSF9XUklURSIsInN1YiI6ImFkbWluIiwiZXhwIjoxNDkzNzgyMjQwfQ.HNfV1CU2CdAnBTH682C5-KOfr2P71xr9PYLaLpDVhOw8KWWSJ0lBo0BCq4LoNwsK_Y3-W3avgbJb0jW9FNYDRQ",
  "message": "",
  "status": 0
}


这里我们得到了相关的JWT,反Base64之后,就是下面的内容,标准JWT


{"alg":"HS512"}{"authorities":"ROLE_ADMIN,AUTH_WRITE","sub":"admin","exp":1493782240}ͽ]BS`pS6~hCVH%
ܬ)֝ଖoE5р


整个过程如下:

  • 拿到传入JSON,解析用户名密码 - JWTLoginFilterattemptAuthentication 方法
  • 自定义身份认证验证组件,进行身份认证 - CustomAuthenticationProviderauthenticate 方法
  • 盐城成功 - JWTLoginFiltersuccessfulAuthentication 方法
  • 生成JWT - TokenAuthenticationServiceaddAuthentication方法


再测试一个访问资源的:

curl -H "Content-Type: application/json" -H "Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJhdXRob3JpdGllcyI6IlJPTEVfQURNSU4sQVVUSF9XUklURSIsInN1YiI6ImFkbWluIiwiZXhwIjoxNDkzNzgyMjQwfQ.HNfV1CU2CdAnBTH682C5-KOfr2P71xr9PYLaLpDVhOw8KWWSJ0lBo0BCq4LoNwsK_Y3-W3avgbJb0jW9FNYDRQ"http://127.0.0.1:8080/users


结果:


{
  "result":["freewolf","tom","jerry"],
  "message":"",
  "status":0
}


说明我们的Token生效可以正常访问。其他的结果您可以自己去测试。再回到处理流程:

  • 接到请求进行拦截 - JWTAuthenticationFilter 中的方法
  • 验证JWT - TokenAuthenticationServicegetAuthentication 方法
  • 访问Controller

这样本文的主要流程就结束了,本文主要介绍了,如何用Spring Security结合JWT保护你的Spring Boot应用。如何使用RoleAuthority,这里多说一句其实在Spring Security中,对于GrantedAuthority接口实现类来说是不区分是Role还是Authority,二者区别就是如果是hasAuthority判断,就是判断整个字符串,判断hasRole时,系统自动加上ROLE_到判断的Role字符串上,也就是说hasRole("CREATE")hasAuthority('ROLE_CREATE')是相同的。利用这些可以搭建完整的RBAC体系。本文到此,你已经会用了本文介绍的知识点。

3.完整代码(未脱敏)

📎framework-spring.rar

相关文章
|
4月前
|
负载均衡 应用服务中间件 Nacos
Nacos配置中心
本文详细介绍Nacos作为配置中心的实现原理与实战应用,涵盖配置管理、热更新、共享配置及优先级规则,并演示集群搭建与高可用部署,提升微服务架构下配置的动态管理能力。
|
4月前
|
SpringCloudAlibaba Java Nacos
SpringCloud概述
Spring Cloud是微服务的统一解决方案,具备约定大于配置、组件丰富、开箱即用等特点。通过整合Netflix与Alibaba生态,形成完整技术栈,其中Spring Cloud Alibaba因Nacos、Sentinel等优秀组件成为主流选择。
|
4月前
|
安全 Java 开发工具
1.工程搭建与验证
本文介绍如何使用阿里云脚手架快速搭建Spring Boot工程,并整合Spring Security。内容涵盖项目创建、代码导入、Web依赖配置及安全验证,通过默认登录机制演示权限控制,附完整代码仓库地址。
 1.工程搭建与验证
|
4月前
|
敏捷开发 Java 测试技术
为什么要单元测试
本文探讨单元测试在现代软件开发中的核心价值,打破“写单测拖慢进度”的误区,阐述其如何通过提升代码质量、降低维护成本、加速迭代来真正加快研发速度,并结合测试金字塔与反模式分析,倡导开发者重视单元测试,推动软件工程高效演进。
 为什么要单元测试
|
4月前
|
安全 Java 应用服务中间件
4.认识SpringSecurity
Spring Security 是基于过滤器链的成熟安全框架,提供认证、鉴权及防御 CSRF 等攻击的核心功能,支持多种认证方式与自定义扩展,保障 Web 应用安全。
|
4月前
|
缓存 Java 数据库连接
1.常见配置
本文介绍了MyBatis的核心配置机制,包括属性加载优先级(方法参数 &gt; resource/url &gt; properties元素)、常见配置项如缓存、延迟加载、执行器类型等,并详解了多环境配置方式及事务管理(JDBC与MANAGED)的使用场景,适用于数据库连接与事务控制的灵活管理。
|
4月前
|
Java Spring
1. 整合Logback,滚动记录+多文件
`logback-spring.xml` 是 Spring Boot 项目的日志配置文件,支持按级别、模块分离输出日志,包含控制台、文件滚动等配置,并可通过 `LogProxy.getLogger()` 获取指定名称的日志实例,实现精准日志管理与追踪。
|
4月前
|
JSON 数据格式
4. 不定参数入参
本文介绍了接口参数的两种常见传参方式:非JSON格式与JSON格式。前者支持基本类型、对象等直接绑定,后者通过@RequestBody接收JSON数据,可封装为对象或使用JSONObject解析,并结合@Valid实现参数校验,提升接口安全性与稳定性。
|
4月前
|
Web App开发 安全 JavaScript
5.跨域处理
本文介绍跨域问题及其解决方案。当协议、域名、端口任一不同时即产生跨域,浏览器因同源策略限制资源访问。通过CORS(跨域资源共享)可在服务端设置Access-Control-Allow-Origin等响应头,实现安全跨域。常用方案包括@CrossOrigin注解、WebMvcConfigurer全局配置及Filter拦截器方式,文中结合Spring Boot代码示例详细演示了各类实现方法。
|
4月前
|
存储 JSON NoSQL
3-MongoDB常用命令
本文介绍如何使用MongoDB存储文章评论数据,涵盖数据库与集合的创建、删除,以及文档的增删改查操作。内容包括:使用`use`创建articledb数据库,通过`insert()`插入评论文档,利用`find()`查询数据并支持投影与排序,结合`limit()`和`skip()`实现分页,以及使用`update()`和`remove()`进行更新与删除操作,全面讲解MongoDB基本CRUD语法及注意事项。