SpringSecurity+JWT认证流程解析 | 掘金新人第一弹 - 掘金
SpringBoot 整合SpringSecurity示例实现前后分离权限注解+JWT登录认证-阿里云开发者社区
20.配置认证过滤器_哔哩哔哩_bilibili
15-尚硅谷-SpringSecurity-web权限方案-用户授权(注解使用)_哔哩哔哩_bilibili
和耳朵/spring-boot-learning-demo

核心:过滤器链

SpringSecurity官方文档中有这么一句话:

Spring Security’s web infrastructure is based entirely on standard servlet filters.

也就是说,SpringSecurity的核心基础就是Servlet过滤器链
一个Web请求会通过一条完整的过滤器链,在经过过滤器链的时候对完成认证和授权,如果中间发现这条请求未认证或未授权,会根据被保护API的权限去抛出异常,然后由异常处理器去处理这些异常。
SpringSecurity整合JWT - 图1
上图中的UsernamePasswordAuthenticationFilterBasicAuthenticationFilter对应着config配置类中的formLoginhttpBasic的配置项,在配置项如果对这两个属性进行了配置,那么就会将上述两个过滤器加载到我们的过滤器链中

  • formLogin对应着你form表单认证方式,即UsernamePasswordAuthenticationFilter
  • httpBasic对应着Basic认证方式,即BasicAuthenticationFilter

重要概念

  • SecurityContext:上下文对象,会存放Authentication对象
  • SecurityContextHolder:用于拿到上下文对象的静态工具类
  • Authentication:认证接口,定义了认证对象的数据形式
  • AuthenticationManager:用于校验Authentication,返回一个认证完成后的Authentication对象

    1. SecurityContext

    image.png
    其中只有两个方法,对Authenticationset 或者 get

    2. SecurityContextHolder

    image.png
    相当于是SecurityContext的工具类,可以用来获取或者清除SecurityContext

    3. Authentication

    image.png
    几个方法的作用如下:

  • getAuthorities:获取用户权限,一般情况下获取到的是用户的角色信息

  • getCredentials:获取证明用户认证的信息,一般获取到的是密码等信息
  • getDetails:获取用户的额外信息
  • getPrincipal:获取用户身份信息,在未认证的情况下获取到的是用户名,已认证的情况下获取到的是UserDetails
  • isAuthenticated:获取当前Authentication 是否已完成认证
  • setAuthenticated:设置当前Authentication是否已认证

    4. AuthenticationManager

    1. public interface AuthenticationManager {
    2. // 认证方法
    3. Authentication authenticate(Authentication authentication)
    4. throws AuthenticationException;
    5. }
    AuthenticationManager定义了一个认证方法,它将一个未认证的Authentication传入,返回一个已认证的Authentication,默认使用的实现类为:ProviderManager

那么,如何将上述四个部分串联起来,从而构建出基于SpringSecurity的登录认证流程?

  • ✔️先是一个请求,携带着用户信息进来
  • ✔️通过AuthenticationManager或者AuthenticationProvider的认证
  • ✔️然后通过SecurityContextHolder获取SecurityContext
  • ✔️将认证的信息放到SecurityContext

自定义登录逻辑处理器

  1. package com.sheep.securitylearning.handler;
  2. import cn.hutool.log.Log;
  3. import cn.hutool.log.LogFactory;
  4. import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
  5. import com.sheep.securitylearning.entity.Users;
  6. import com.sheep.securitylearning.mapper.UserMapper;
  7. import org.springframework.beans.factory.annotation.Autowired;
  8. import org.springframework.security.authentication.AuthenticationProvider;
  9. import org.springframework.security.authentication.BadCredentialsException;
  10. import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
  11. import org.springframework.security.core.Authentication;
  12. import org.springframework.security.core.AuthenticationException;
  13. import org.springframework.security.core.GrantedAuthority;
  14. import org.springframework.security.core.authority.AuthorityUtils;
  15. import org.springframework.security.core.userdetails.UsernameNotFoundException;
  16. import org.springframework.stereotype.Component;
  17. import java.util.List;
  18. import java.util.Objects;
  19. /**
  20. * Created By Intellij IDEA
  21. *
  22. * @author ssssheep
  23. * @package com.sheep.securitylearning.handler
  24. * @datetime 2022/8/8 星期一
  25. */
  26. @Component
  27. public class UserAuthenticationProvider implements AuthenticationProvider {
  28. private static final Log log = LogFactory.get(UserAuthenticationProvider.class);
  29. @Autowired
  30. private UserMapper userMapper;
  31. @Override
  32. public Authentication authenticate(Authentication authentication) throws AuthenticationException {
  33. log.info("on authenticate");
  34. // 获取表单输入中返回的用户名
  35. String username = (String) authentication.getPrincipal();
  36. // 获取表单中输入的密码
  37. String password = (String) authentication.getCredentials();
  38. QueryWrapper<Users> queryWrapper = new QueryWrapper<>();
  39. queryWrapper.eq("username", username);
  40. Users users = userMapper.selectOne(queryWrapper);
  41. if (Objects.isNull(users)) {
  42. throw new UsernameNotFoundException("用户名不存在");
  43. }
  44. if (!users.getPassword().equalsIgnoreCase(password)) {
  45. throw new BadCredentialsException("密码不正确");
  46. }
  47. List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_admins");
  48. return new UsernamePasswordAuthenticationToken(users, password, auths);
  49. }
  50. @Override
  51. public boolean supports(Class<?> authentication) {
  52. return true;
  53. }
  54. }

封装TokenUtil

由于我们前后端分离需要基于JWT来进行认证,因此我们需要自行封装一个帮我们操作Token的工具类,主要需要以下三个方法

  • 生成token
  • 验证token
  • 反解析token ```java package com.sheep.securitylearning.utils;

import cn.hutool.core.lang.UUID; import cn.hutool.json.JSONUtil; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.SignatureException; import org.apache.tomcat.util.codec.binary.Base64; import org.springframework.security.core.userdetails.UserDetails;

import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.util.Date; import java.util.HashMap;

/**

  • Created By Intellij IDEA *
  • @author ssssheep
  • @package com.sheep.securitylearning.utils
  • @datetime 2022/8/8 星期一 */ public class JwtUtil {

    private static final Long DEFAULT_EXPIRATION_TIME = 259200000L;

    public static SecretKey generalKey() {

    1. String stringKey = "ssssheep";
    2. byte[] decodedKey = Base64.decodeBase64(stringKey);
    3. return new SecretKeySpec(decodedKey, "AES");

    }

    public static String createKey(String username, Long ttl) {

    1. if (ttl <= 0) {
    2. ttl = DEFAULT_EXPIRATION_TIME;
    3. }
    4. SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
    5. long nowMillis = System.currentTimeMillis();
    6. Date now = new Date(nowMillis);
    7. HashMap<String, Object> claims = new HashMap<>();
    8. claims.put("username", username);
    9. SecretKey key = generalKey();
    10. return Jwts.builder()
    11. .setClaims(claims)
    12. .setId(UUID.randomUUID().toString())
    13. .setIssuedAt(now)
    14. .setIssuer("yxr")
    15. .setSubject(JSONUtil.toJsonStr(claims))
    16. .signWith(signatureAlgorithm, key)
    17. .setExpiration(new Date(nowMillis + DEFAULT_EXPIRATION_TIME))
    18. .compact();

    }

    public static boolean isTokenExpired(Claims claims) {

    1. return claims.getExpiration().before(new Date());

    }

    /**

    • 验证token是否合法 *
    • @param token token值
    • @param userDetails 用户信息
    • @return 是否合法 */ public static boolean checkValid(String token, UserDetails userDetails) { Claims claims = parseKey(token); System.out.println(claims.getSubject()); String username = JSONUtil.parseObj(claims.getSubject()).getStr(“username”); return username.equals(userDetails.getUsername())
      1. && !isTokenExpired(claims);
      }
  1. public static Claims parseKey(String token) throws SignatureException {
  2. SecretKey key = generalKey();
  3. return Jwts.parser()
  4. .setSigningKey(key)
  5. .parseClaimsJws(token)
  6. .getBody();
  7. }

// public static void main(String[] args) { // String token = “eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ7XCJpc3NcIjpcInl4clwiLFwiaWF0XCI6MTY2MDAxMjY3NSxcImp0aVwiOlwiZTY2YzlmZmYtYzRjYi00NDU5LWJhOWQtMmQzZTZlZTBmNDE3XCIsXCJ1c2VybmFtZVwiOlwiYWRtaW5cIn0iLCJpc3MiOiJ5eHIiLCJleHAiOjE2NjAyNzE4NzUsImlhdCI6MTY2MDAxMjY3NSwianRpIjoiZTY2YzlmZmYtYzRjYi00NDU5LWJhOWQtMmQzZTZlZTBmNDE3IiwidXNlcm5hbWUiOiJhZG1pbiJ9.P2j2CgQNrDOLdA7HtYUPBFu2DT91wDAFYQj1rWpZXv8”; // Users users = new Users(1, “admin”, “123”); // System.out.println(checkValid(token, users)); // } }

  1. <a name="WuQ1i"></a>
  2. ## 编写请求登录接口
  3. 访问任何一个系统,最先访问的就是认证方法,如下是根据最简单的登录逻辑编写了一个登录接口
  4. ```java
  5. @GetMapping("/login")
  6. public ApiResult login(String username, String password) {
  7. UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
  8. Authentication authentication = userAuthenticationProvider.authenticate(authenticationToken);
  9. SecurityContextHolder.getContext().setAuthentication(authentication);
  10. Users principal = (Users) authentication.getPrincipal();
  11. String token = JwtUtil.createKey(principal.getUsername(), -1L);
  12. HashMap<String, Object> result = new HashMap<>();
  13. result.put("token", token);
  14. return ApiResult.success("登录成功", result);
  15. }
  • 🌈根据传入的用户名和密码构建一个UsernamePasswordAuthenticationToken
  • 🌈将UsernamePasswordAuthenticationToken传入我们的自定义登录逻辑处理器中,返回一个Authentication对象
  • 🌈如果认证成功,那么就会走到第三步。就可以通过SecurityContextHolder获取SecurityContext,然后将认证之后的Authentication对象,放入上下文对象中
  • 🌈从Authentication对象中拿到我们的UserDetails,然后根据封装的JwtUtil创建token,返回给前端

🗝️将token返回给前端后,前端之后的每次请求都要在请求头中携带上token

JWT过滤器

  1. package com.sheep.securitylearning.filter;
  2. import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
  3. import com.sheep.securitylearning.config.ApiResult;
  4. import com.sheep.securitylearning.entity.Users;
  5. import com.sheep.securitylearning.mapper.UserMapper;
  6. import com.sheep.securitylearning.utils.JwtUtil;
  7. import com.sheep.securitylearning.utils.ResultUtil;
  8. import io.jsonwebtoken.Claims;
  9. import io.jsonwebtoken.SignatureException;
  10. import lombok.RequiredArgsConstructor;
  11. import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
  12. import org.springframework.security.core.GrantedAuthority;
  13. import org.springframework.security.core.authority.AuthorityUtils;
  14. import org.springframework.security.core.context.SecurityContextHolder;
  15. import org.springframework.stereotype.Component;
  16. import org.springframework.util.StringUtils;
  17. import org.springframework.web.filter.OncePerRequestFilter;
  18. import javax.servlet.FilterChain;
  19. import javax.servlet.ServletException;
  20. import javax.servlet.http.HttpServletRequest;
  21. import javax.servlet.http.HttpServletResponse;
  22. import java.io.IOException;
  23. import java.util.List;
  24. import java.util.Objects;
  25. /**
  26. * Created By Intellij IDEA
  27. *
  28. * @author ssssheep
  29. * @package com.sheep.securitylearning.filter
  30. * @datetime 2022/8/8 星期一
  31. */
  32. @Component
  33. @RequiredArgsConstructor
  34. public class JwtAuthTokenFilter extends OncePerRequestFilter {
  35. final UserMapper userMapper;
  36. @Override
  37. protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
  38. System.out.println("进入了JwtAuthTokenFilter");
  39. // 获取token
  40. String token = request.getHeader("token");
  41. if (!StringUtils.hasText(token)) {
  42. filterChain.doFilter(request, response);
  43. return;
  44. }
  45. // 解析token
  46. Claims claims;
  47. try {
  48. claims = JwtUtil.parseKey(token);
  49. } catch (SignatureException e) {
  50. e.printStackTrace();
  51. ResultUtil.responseJson(response, ApiResult.error("token已失效"));
  52. return;
  53. }
  54. Integer uid = claims.get("uid", Integer.class);
  55. // 从数据库中获取用户信息
  56. QueryWrapper<Users> queryWrapper = new QueryWrapper<>();
  57. queryWrapper.eq("id", uid);
  58. Users users = userMapper.selectOne(queryWrapper);
  59. if (Objects.isNull(users)) {
  60. ResultUtil.responseJson(response, ApiResult.notLogin());
  61. return;
  62. }
  63. // 判断token是否合法
  64. if (!JwtUtil.checkValid(token, users)) {
  65. ResultUtil.responseJson(response, ApiResult.notLogin());
  66. return;
  67. }
  68. // 存储SecurityContextHolder
  69. List<GrantedAuthority> auths
  70. = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_admins");
  71. UsernamePasswordAuthenticationToken authenticationToken
  72. = new UsernamePasswordAuthenticationToken(users, users.getPassword(), auths);
  73. SecurityContextHolder.getContext().setAuthentication(authenticationToken);
  74. // 放行
  75. filterChain.doFilter(request, response);
  76. }
  77. }

在过滤器中主要是获取请求头中的token并且验证其是否合法
判断token合法后,获取用户的信息并组装好一个authentication对象,将它放在上下文的对象中,这样后面的过滤器就可以获取authentication对象,就相当于已经认证过了

对Security进行配置

  1. package com.sheep.securitylearning.config;
  2. import com.sheep.securitylearning.filter.JwtAuthTokenFilter;
  3. import com.sheep.securitylearning.handler.*;
  4. import lombok.RequiredArgsConstructor;
  5. import org.springframework.context.annotation.Bean;
  6. import org.springframework.context.annotation.Configuration;
  7. import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
  8. import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
  9. import org.springframework.security.config.annotation.web.builders.HttpSecurity;
  10. import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
  11. import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
  12. import org.springframework.security.config.http.SessionCreationPolicy;
  13. import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
  14. import org.springframework.security.crypto.password.PasswordEncoder;
  15. import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler;
  16. import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
  17. /**
  18. * Created By Intellij IDEA
  19. *
  20. * @author ssssheep
  21. * @package com.sheep.securitylearning.config
  22. * @datetime 2022/8/8 星期一
  23. */
  24. @Configuration
  25. @RequiredArgsConstructor
  26. @EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
  27. @EnableWebSecurity
  28. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  29. final JwtAuthTokenFilter jwtAuthTokenFilter;
  30. final UserLogoutSuccessHandler userLogoutSuccessHandler;
  31. final UserAuthAccessDeniedHandler userAuthAccessDeniedHandler;
  32. final UserAuthenticationProvider userAuthenticationProvider;
  33. final UserAuthenticationEntryPoint userAuthenticationEntryPoint;
  34. @Bean
  35. public DefaultWebSecurityExpressionHandler userSecurityExpressionHandler() {
  36. DefaultWebSecurityExpressionHandler handler = new DefaultWebSecurityExpressionHandler();
  37. handler.setPermissionEvaluator(new UserPermissionEvaluator());
  38. return handler;
  39. }
  40. @Override
  41. protected void configure(HttpSecurity http) throws Exception {
  42. http.csrf().disable()
  43. .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
  44. .and()
  45. .authorizeRequests()
  46. .antMatchers("/user/login").anonymous()
  47. //除了登录接口以外的所有接口都需要认证访问
  48. .anyRequest().authenticated()
  49. .and()
  50. .formLogin()
  51. .disable()
  52. .logout()
  53. .logoutSuccessHandler(userLogoutSuccessHandler)
  54. .and()
  55. .exceptionHandling()
  56. .authenticationEntryPoint(userAuthenticationEntryPoint)
  57. .accessDeniedHandler(userAuthAccessDeniedHandler)
  58. .and()
  59. .cors()
  60. .and()
  61. .csrf().disable();
  62. http.headers().cacheControl();
  63. // 将我们自定义的JWT过滤器加入到过滤器链中
  64. http.addFilterBefore(jwtAuthTokenFilter, UsernamePasswordAuthenticationFilter.class);
  65. }
  66. @Override
  67. protected void configure(AuthenticationManagerBuilder auth) throws Exception {
  68. auth.authenticationProvider(userAuthenticationProvider);
  69. }
  70. @Bean
  71. PasswordEncoder passwordEncoder() {
  72. return new BCryptPasswordEncoder();
  73. }

测试

准备三个接口

  • 登录接口
  • 权限接口1(有权限)
  • 权限接口2(无权限)

    访问登录接口

    首先请求登录接口,获取用户的token
    image.png

    访问权限接口1

    在请求头中携带上token,发起请求
    由于我们拥有此接口需要的权限,所以可以正常请求
    image.png

    访问权限接口2

    由于我们没有此接口的权限,因此会得到403状态码
    image.png
    至此,我们的系统已经成功整合了JWT实现接口的登录、授权以及鉴权