精品SpringSecurity集成JWT #yyds干货盘点#

Posted 梁云亮

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了精品SpringSecurity集成JWT #yyds干货盘点#相关的知识,希望对你有一定的参考价值。

本博客代码下载地址:

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.22</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.2</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>
  • application.yml
jwt:
  # 为JWT基础信息加密和解密的密钥,长度需要大于等于43
  # 在实际生产中通常不直接写在配置文件里面。而是通过应用的启动参数传递,并且需要定期修改
  secret: oQZSeguYloAPAmKwvKqqnifiQatxMEPNOvtwPsCLasd
  # JWT令牌的有效时间,单位秒,默认2周
  expiration: 1209600
  header: Authorization
spring:
  main:
    allow-circular-references: true   # 允许循环注入

项目使用SpringBoot版本为2.6.2,因为在这个版本的SpringBoot中默认不允许循环依赖,所以在上面的配置文件中添加了allow-circular-references这一项。

1、创建Jwt工具类

  • 工具类
@Slf4j
@Component
//@ConfigurationProperties(prefix = "jwt")
public class JwtUtil 
    /**
     * 携带JWT令牌的HTTP的Header的名称,在实际生产中可读性越差越安全
     */
    @Getter
    @Value("$jwt.header")
    private String header;

    /**
     * 为JWT基础信息加密和解密的密钥
     * 在实际生产中通常不直接写在配置文件里面。而是通过应用的启动参数传递,并且需要定期修改。
     */
    @Value("$jwt.secret")
    private String secret;

    /**
     * JWT令牌的有效时间,单位秒
     * - 默认2周
     */
    @Value("$jwt.expiration")
    private Long expiration;

    /**
     * SecretKey 根据 SECRET 的编码方式解码后得到:
     * Base64 编码:SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretString));
     * Base64URL 编码:SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64URL.decode(secretString));
     * 未编码:SecretKey key = Keys.hmacShaKeyFor(secretString.getBytes(StandardCharsets.UTF_8));
     */
    private static SecretKey getSecretKey(String secret) 
        byte[] encodeKey = Decoders.BASE64.decode(secret);
        return Keys.hmacShaKeyFor(encodeKey);
    

    /**
     * 用claims生成token
     *
     * @param claims 数据声明,用来创建payload的私有声明
     * @return token 令牌
     */
    private String generateToken(Map<String, Object> claims) 
        SecretKey key = getSecretKey(secret);
        //SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256); //两种方式等价

        // 添加payload声明
        JwtBuilder jwtBuilder = Jwts.builder()
                // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
                .setClaims(claims)
                // 设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
                .setId(UUID.randomUUID().toString())
                // iat: jwt的签发时间
                .setIssuedAt(new Date())

                // 你也可以改用你喜欢的算法,支持的算法详见:https://github.com/jwtk/jjwt#features
                // SignatureAlgorithm.HS256:指定签名的时候使用的签名算法,也就是header那部分
                .signWith(key, SignatureAlgorithm.HS256)
                .setExpiration(new Date(System.currentTimeMillis() + this.expiration * 1000));

        String token = jwtBuilder.compact();
        return token;
    

    /**
     * 生成Token令牌
     *
     * @param userDetails 用户
     * @return 令牌Token
     */
    public String generateToken(UserDetails userDetails) 
        Map<String, Object> claims = new HashMap<>();
        claims.put("sub", userDetails.getUsername());
        claims.put("created", new Date());
        return generateToken(claims);
    

    /**
     * 从token中获取数据声明claim
     *
     * @param token 令牌token
     * @return 数据声明claim
     */
    public Claims getClaimsFromToken(String token) 
        try 
            SecretKey key = getSecretKey(secret);
            Claims claims = Jwts.parser()
                    .setSigningKey(key)
                    .parseClaimsJws(token)
                    .getBody();
            return claims;
         catch (ExpiredJwtException | UnsupportedJwtException | MalformedJwtException | IllegalArgumentException e) 
            log.error("token解析错误", e);
            throw new IllegalArgumentException("Token invalided.");
        
    

    public String getUserRole(String token) 
        return (String) getClaimsFromToken(token).get("role");
    

    /**
     * 从token中获取登录用户名
     *
     * @param token 令牌
     * @return 用户名
     */
    public String getSubjectFromToken(String token) 
        String subject;
        try 
            Claims claims = getClaimsFromToken(token);
            subject = claims.getSubject();
         catch (Exception e) 
            subject = null;
        
        return subject;
    

    /**
     * 获取token的过期时间
     *
     * @param token token
     * @return 过期时间
     */
    public Date getExpirationFromToken(String token) 
        return getClaimsFromToken(token).getExpiration();
    

    /**
     * 判断token是否过期
     *
     * @param token 令牌
     * @return 是否过期:已过期返回true,未过期返回false
     */
    public Boolean isTokenExpired(String token) 
        Date expiration = getExpirationFromToken(token);
        return expiration.before(new Date());
    

    /**
     * 验证令牌:判断token是否非法
     *
     * @param token       令牌
     * @param userDetails 用户
     * @return 如果token未过期且合法,返回true,否则返回false
     */
    public Boolean validateToken(String token, UserDetails userDetails) 
        //如果已经过期返回false
        if (isTokenExpired(token)) 
            return false;
        
        String usernameFromToken = getSubjectFromToken(token);
        String username = userDetails.getUsername();
        return username.equals(usernameFromToken);
    

  • 测试代码
@SpringBootTest
public class JwtUtilTest 

    @Resource
    private JwtUtil jwtUtil;

    @Resource
    private PasswordEncoder passwordEncoder;

    @Test
    void fun() 
        System.out.println(passwordEncoder);
        SecretKey secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);
        System.out.println(secretKey);
    

    //生成token
    @Test
    void generateToken() 
        //用户信息
        String encode = passwordEncoder.encode("1234");
        User user = new User("zhangsan", encode, AuthorityUtils.createAuthorityList());
        String token = jwtUtil.generateToken(user);
        System.out.println(token);
    

    @Test
    void getClaimsFromToken() 
        //用户信息
        String encode = passwordEncoder.encode("1234");
        User user = new User("zhangsan", encode, AuthorityUtils.createAuthorityList());

        String token = jwtUtil.generateToken(user);
        System.out.println(token);

        Claims claims = jwtUtil.getClaimsFromToken(token);
        System.out.println(claims);
    

    @Test
    void getSubjectFromToken() 
        //用户信息
        String encode = passwordEncoder.encode("1234");
        User user = new User("zhangsan", encode, AuthorityUtils.createAuthorityList());
        String token = jwtUtil.generateToken(user);
        System.out.println(token);

        String username = jwtUtil.getSubjectFromToken(token);
        System.out.println(username);
    

    @Test
    void getExpirationFromToken() 
        //用户信息
        String encode = passwordEncoder.encode("1234");
        User user = new User("zhangsan", encode, AuthorityUtils.createAuthorityList());
        String token = jwtUtil.generateToken(user);

        System.out.println(token);
        Date date = jwtUtil.getExpirationFromToken(token);
        System.out.println(new SimpleDateFormat("YYYY-MM-dd HH:mm:ss").format(date));
    

    @Test
    void isTokenExpired() 
        //用户信息
        String encode = passwordEncoder.encode("1234");
        User user = new User("zhangsan", encode, AuthorityUtils.createAuthorityList());
        String token = jwtUtil.generateToken(user);

        System.out.println(token);
        Boolean res = jwtUtil.isTokenExpired(token);
        System.out.println(res);
    

    @Test
    void validateToken() 
        //用户信息
        String encode = passwordEncoder.encode("1234");
        User user = new User("zhangsan", encode, AuthorityUtils.createAuthorityList());
        String token = jwtUtil.generateToken(user);
        System.out.println(token);

        User user2 = new User("zhangsan", "", AuthorityUtils.createAuthorityList());
        Boolean res = jwtUtil.validateToken(token, user2);
        System.out.println(res);
    

    //模拟篡改
    @Test
    void fake() 
        // 将我改成你生成的token的第一段(以.为边界)
        String encodedHeader = "eyJhbGciOiJIUzI1NiJ9";
        // 测试4: 解密Header
        byte[] header = Base64.decodeBase64(encodedHeader.getBytes());
        System.out.println(new String(header));

        // 将我改成你生成的token的第二段(以.为边界)
        String encodedPayload = "eyJpZCI6IjEiLCJpYXQiOjE1NjU1ODk1NDEsImV4cCI6MTU2Njc5OTE0MX0";
        // 测试5: 解密Payload
        byte[] payload = Base64.decodeBase64(encodedPayload.getBytes());
        System.out.println(new String(payload));

        //用户信息
        String encode = passwordEncoder.encode("1234");
        User user = new User("zhangsan", encode, AuthorityUtils.createAuthorityList());
        // 测试6: 这是一个被篡改的token,因此会报异常,说明JWT是安全的
        jwtUtil.validateToken("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEiLCJpYXQiOjE1NjU1ODk3MzIsImV4cCI6MTU2Njc5OTMzMn0.nDv25ex7XuTlmXgNzGX46LqMZItVFyNHQpmL9UQf-aUx", user);
    

2、创建没有权限时,Jwt拒绝访问的处理器

/**
 * 当用户在没有授权的时候,返回的指定信息
 */
@Slf4j
@Component
public class jwtAccessDeniedHandler implements AccessDeniedHandler 
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws ServletException 
        log.info("用户访问没有授权资源:",e.getMessage());

        response.setContentType("application/json;charset=utf-8");
        response.setCharacterEncoding("utf-8");
        try(PrintWriter out = response.getWriter();)
            Result result = ResultUtil.fail("用户访问未授权资源").setCode(HttpServletResponse.SC_UNAUTHORIZED);
            out.write(JsonUtil.obj2String(result));
            out.flush();
        catch (IOException exception)

        

    

3、创建没有token时,Jwt的EntryPoint

/**
 *用户访问资源没有携带正确的token,时返回的信息
 */
@Slf4j
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint 
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws ServletException, IOException 
        log.info("用户访问资源没有携带正确的token:",e.getMessage());
        response.setContentType("application/json;charset=utf-8");
        response.setCharacterEncoding("utf-8");
        try(PrintWriter out = response.getWriter();)
            Result result = ResultUtil.fail("用户访问资源没有携带正确的token").setCode(HttpServletResponse.SC_UNAUTHORIZED);
            out.write(JsonUtil.obj2String(result));
            out.flush();
        catch (IOException exception)

        
    

4、创建UserDetailsService

@Service
public class UserDetailsServiceImpl implements UserDetailsService 
    @Resource
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException 
        //直接写死数据信息,可以在这里获取数据库的信息并进行验证
        //UserDetails user = User.withUsername(username)
        //        .password(passwordEncoder.encode("1234"))
        //        .authorities("Role_vip,user:list,user:update")
        //        .build();

        User user = new User(username, passwordEncoder.encode("1234"),
                AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_vip,user:list,user:update"));
        return user;
    

5、创建Jwt认证过滤器

@Slf4j
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter 
    @Resource
    private JwtUtil jwtUtil;

    @Resource
    private UserDetailsServiceImpl userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException 
        String token = request.getHeader(jwtUtil.getHeader());
        log.info("header token:", token);
        //如果请求头中有token,则进行解析,并且设置认证信息
        if (token != null && token.trim().length() > 0) 
            //根据token获取用户名
            String username = jwtUtil.getSubjectFromToken(token);
            // 验证username,如果验证合法则保存到SecurityContextHolder
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) 
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                // JWT验证通过,使用Spring Security 管理
                if (jwtUtil.validateToken(token, userDetails)) 
                    //加载用户、角色、权限信息
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, null, userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                
            
        
        //如果请求头中没有Authorization信息则直接放行
        chain.doFilter(request, response);
    

6、配置SpringSecurity

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringSecurityJwtConfig extends WebSecurityConfigurerAdapter 
    @Bean
    public PasswordEncoder passwordEncoder() 
        return new BCryptPasswordEncoder();
    

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception 
        return super.authenticationManagerBean();
    

    @Resource
    private com.hc.jwt.jwtAccessDeniedHandler jwtAccessDeniedHandler;

    @Resource
    private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;

    @Resource
    private JwtAuthenticationFilter jwtAuthenticationFilter;

    @Override
    protected void configure(HttpSecurity http) throws Exception 
        http.csrf().disable();
        // 禁用session
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        http.authorizeRequests()
                //login 不拦截
                .antMatchers("/login").permitAll()
                .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                .antMatchers("/").permitAll()
                .anyRequest().authenticated();

        //用户访问没有授权资源
        http.exceptionHandling().accessDeniedHandler(jwtAccessDeniedHandler);
        //授权错误信息处理
        //用户访问资源没有携带正确的token
        http.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint);
        // 使用自己定义的拦截机制验证请求是否正确,拦截jwt
        http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    

7、创建控制器

@RestController
public class UserController 

    @Resource
    private JwtUtil jwtUtil;

    @Resource
    private UserDetailsServiceImpl userDetailsService;

    @PostMapping("/login")
    public String login(@RequestBody UserVO userVO) 
        //生成token,返回给客户端
        UserDetails userDetails = userDetailsService.loadUserByUsername(userVO.getUsername());

        String token = jwtUtil.generateToken(userDetails);
        return token;
    

    @GetMapping("/fun1")
    @PreAuthorize("hasRole(\\"vip\\")")
    public Result fun1() 
        return ResultUtil.success("fun1");
    

    @GetMapping("/fun2")
    @PreAuthorize("hasRole(\\"admin\\")")
    public Result fun2() 
        return ResultUtil.success("fun1");
    

    @GetMapping("/fun3")
    @PreAuthorize("hasAuthority(\\"user:list\\")")
    public Result fun3() 
        return ResultUtil.success("fun1");
    

    @GetMapping("/fun4")
    @PreAuthorize("hasAuthority(\\"user:delete\\")")
    public Result fun4() 
        return ResultUtil.success("fun1");
    

结果

  • 用户登录
  • 角色

  • 权限

其他代码

  • UserVO
@Getter
@Setter
public class UserVO 
    private String username;
    private String password;

以上是关于精品SpringSecurity集成JWT #yyds干货盘点#的主要内容,如果未能解决你的问题,请参考以下文章

SpringBoot 集成 SpringSecurity + MySQL + JWT 附源码,废话不多直接盘

Spring Security+JWT简述

jwt和spring security集成

Angular集成Spring Boot,Spring Security,JWT和CORS

第十一篇SpringSecurity基于JWT实现Token的处理

第十一篇SpringSecurity基于JWT实现Token的处理