Spring Security + Jwt 集成实现登录

Spring Security + Jwt 集成实现登录_第1张图片

文章目录

  • 前言
  • Maven 相关依赖
  • 配置文件
  • 自定义springsecurity相关认证流程
    • 继承WebSecurityConfigurerAdapter
    • 继承AbstractAuthenticationToken
    • 继承AbstractAuthenticationProcessingFilter
    • 实现AuthenticationProvider
    • 实现UserDetailsService
    • 实现AccessDeniedHandler
    • 实现AuthenticationEntryPoint
    • 实现AuthenticationFailureHandler
    • 实现AuthenticationFailureHandler
    • 实现AuthenticationSuccessHandler
    • 实现AuthenticationSuccessHandler
    • 继承OncePerRequestFilter
  • 总结


前言

主要实现了如下登录方式:

  1. 用户名+密码+验证码登录
  2. 邮箱+验证码登录


Maven 相关依赖

主要引入了如下依赖:

  1. spring-boot-starter-security依赖
    1. 为了解决警告排除了kotlin相关的三个依赖包
  2. jjwt依赖
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-securityartifactId>
            
            <exclusions>
                <exclusion>
                    <groupId>org.jetbrains.kotlingroupId>
                    <artifactId>kotlin-stdlib-jdk8artifactId>
                exclusion>
                <exclusion>
                    <groupId>org.jetbrains.kotlingroupId>
                    <artifactId>kotlin-stdlibartifactId>
                exclusion>
                <exclusion>
                    <groupId>org.jetbrains.kotlingroupId>
                    <artifactId>kotlin-reflectartifactId>
                exclusion>
            exclusions>
        dependency>
        <dependency>
            <groupId>io.jsonwebtokengroupId>
            <artifactId>jjwtartifactId>
        dependency>

配置文件

主要配置如下:

  1. 配置了jwt生成token秘钥
  2. 配置了jwt的token过期时间,单位为毫秒
  3. 配置了header中携带token的key值
 jwt: #jwt参数配置
  secret: secret
  expiration: 14400000
  authorization: Authorization

自定义springsecurity相关认证流程

继承WebSecurityConfigurerAdapter

主要做了如下操作:

  1. 重写了安全框架全局配置configureGlobal(AuthenticationManagerBuilder auth)方法
    1. 指定了密码加密规则
    2. 关闭了隐藏用户找不到异常功能
  2. 放开了http防火墙,不对非法json请求拦截
  3. 重写了http安全配置configure(HttpSecurity http)方法
    1. 禁用csr攻击保护
    2. 禁用form表单登录
    3. 禁用页面缓存
    4. 自定义异常拦截处理
      1. 身份验证失败MyAuthenticationEntryPoint()异常拦截器
      2. 访问被拒绝MyAccessDeniedHandler()异常拦截器
    5. 会话创建策略无状态(不使用session)
    6. 授权请求配置
      1. 接口请求
    7. UsernamePasswordAuthenticationFilter()拦截器之前添加令牌验证过滤器
    8. 登出成功MyLogoutSuccessHandler()拦截器
    9. 添加自定义身份验证适配器MyAuthenticationConfigurer()
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private MyAuthenticationEntryPoint authenticationEntryPoint;
    @Autowired
    private MyAccessDeniedHandler accessDeniedHandler;
    @Autowired
    private MyLogoutSuccessHandler logoutSuccessHandler;
    @Autowired
    private JwtAuthorizationTokenFilter jwtAuthorizationTokenFilter;

    /**
     * 全局配置
     *
     * @param auth 身份验证管理器生成器
     */
    @SneakyThrows
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) {
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        //自定义service
        daoAuthenticationProvider.setUserDetailsService(super.userDetailsService());
        //配置密码生成规则
        daoAuthenticationProvider.setPasswordEncoder(new BCryptPasswordEncoder());
        //不隐藏异常
        daoAuthenticationProvider.setHideUserNotFoundExceptions(false);
        auth.authenticationProvider(daoAuthenticationProvider);

    }

    /**
     * http防火墙,放开非法json请求
     *
     * @return HttpFirewall
     */
    @Bean
    public HttpFirewall httpFirewall() {
        return new DefaultHttpFirewall();
    }

    /**
     * http安全配置
     *
     * @param http the {@link HttpSecurity} to modify
     */
    @SneakyThrows
    @Override
    protected void configure(HttpSecurity http) {
        http
        //禁用csr攻击保护
        .csrf().disable()
        // 禁用form表单登录
        .formLogin().disable()
        // 禁用页面缓存
        .headers().cacheControl().disable().and()
        //异常处理
        .exceptionHandling()
        //身份验证失败
        .authenticationEntryPoint(authenticationEntryPoint)
        //访问被拒绝的处理程序
        .accessDeniedHandler(accessDeniedHandler)
        .and()
        //会话创建策略无状态(不使用session)
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
        //授权请求
        .authorizeRequests()
        //允许所有
        .antMatchers(HttpMethod.OPTIONS, HttpPermitAll.getHttpOptionPermits()).permitAll()
        .antMatchers(HttpMethod.POST, HttpPermitAll.getHttpPostPermits()).permitAll()
        .antMatchers(HttpMethod.PUT, HttpPermitAll.getHttpPutPermits()).permitAll()
        .antMatchers(HttpMethod.GET, HttpPermitAll.getHttpGetPermits()).permitAll()
        .antMatchers(WebPermitAll.getWebPermits()).permitAll()
        //除了上面配置的请求白名单,任何请求都需要验证
        .anyRequest().authenticated()
        .and()
        //添加令牌验证过滤器
        .addFilterAfter(jwtAuthorizationTokenFilter, UsernamePasswordAuthenticationFilter.class)
        //登出成功过滤器
        .logout().logoutSuccessHandler(logoutSuccessHandler)
        //添加自定义身份验证适配器
        .and().apply(new MyAuthenticationConfigurer());
    }
    }

继承AbstractAuthenticationToken

主要做了如下操作:

  1. 重新定义了principal概念
  2. 增加了验证码字段code
  3. 增加了验证码key字段uuid
  4. 登录类型type
  5. 自定义带参的构造函数
  6. 新增的参数对应的Getter和Setter方法
public class MyAuthenticationToken extends AbstractAuthenticationToken {
    /**
     * 用户名/邮箱
     */
    private final Object principal;

    /**
     * 密码
     */
    private Object credentials;

    /**
     * 验证码
     */
    private String code;

    /**
     * 验证码id
     */
    private String uuid;

    /**
     * 登录类型
     */
    private String type;

    public MyAuthenticationToken(Object principal,
                                 Object credentials,
                                 String code,
                                 String uuid,
                                 String type) {
        super(null);
        this.principal = principal;
        this.credentials = credentials;
        this.code = code;
        this.uuid = uuid;
        this.type = type;
        setAuthenticated(false);
    }

    public MyAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities, Object credentials) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true);
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getUuid() {
        return uuid;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    @Override
    public Object getCredentials() {
        return this.credentials;
    }

    @Override
    public Object getPrincipal() {
        return this.principal;
    }

    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        if (isAuthenticated) {
            throw new IllegalArgumentException(
                    "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        }

        super.setAuthenticated(false);
    }

    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
        credentials = null;
    }
}

继承AbstractAuthenticationProcessingFilter

主要做了如下操作:

  1. 从HttpServletRequest获取到相关参数
  2. 整理验证参数实体Authentication并移交给验证器AuthenticationManager
public class MyAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    public MyAuthenticationFilter(RequestMatcher matcher, AuthenticationManager localAuthManager) {
        super(matcher, localAuthManager);
    }

    @SneakyThrows
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        String username = request.getParameter(Constant.USERNAME);
        String password = request.getParameter(Constant.PASSWORD);
        String code = request.getParameter(Constant.CODE);
        String uuid = request.getParameter(Constant.UUID);
        String type = request.getParameter(Constant.TYPE);
        //整理验证信息并移交给验证器
        Authentication authentication = new MyAuthenticationToken(username, password, code, uuid, type);
        return this.getAuthenticationManager().authenticate(authentication);
    }
}

实现AuthenticationProvider

主要做了如下操作:

  1. 实现了认证方法authenticate(Authentication authentication)
    1. 通过Authentication获取登录相关信息
    2. 验证登录类型是否为空
    3. 如果是用户名密码验证码登录则执行defaultLogin()
    4. 如果是邮箱验证码登录则执行emailLogin()函数
    5. 调用loadUserByUsername()函数,校验用户是否有效
    6. 整理用户信息,回填验证信息到上下文中
  2. 创建了用户名密码验证码登录私有化函数defaultLogin()
    1. 通过uuid获取缓存中验证码
    2. 验证输入uuid和输入的验证码是否为空
    3. 验证缓存中是否存在uuid对应的验证码
    4. 验证输入的验证码和缓存中的验证码是否一致
    5. 通过用户名称获取用户信息
    6. 验证用户是否存在
    7. 验证密码是否正确
    8. 返回用户名称或者对应的错误代码
  3. 创建了邮箱-验证码登录私有化函数emailLogin()
    1. 通过uuid获取缓存中验证码
    2. 验证输入uuid和输入的验证码是否为空
    3. 验证缓存中是否存在uuid对应的验证码
    4. 验证输入的验证码和缓存中的验证码是否一致
    5. 通过邮箱获取用户信息
    6. 验证用户是否存在
    7. 返回用户名称或者对应的错误代码
  4. 实现了supports(Class authentication)方法
    1. 指定自定义身份验证方法MyAuthenticationProvider使用自定义的MyAuthenticationToken实体类
public class MyAuthenticationProvider implements AuthenticationProvider {

    private final RedisUtil redisUtil;

    private final IUserService userService;

    private final IUserInfoService userInfoService;

    public MyAuthenticationProvider(RedisUtil redisUtil,
                                    IUserService userService,
                                    IUserInfoService userInfoService) {
        this.redisUtil = redisUtil;
        this.userService = userService;
        this.userInfoService = userInfoService;
    }

    @SneakyThrows
    @Override
    public Authentication authenticate(Authentication authentication) {
        MyAuthenticationToken authenticationToken = (MyAuthenticationToken) authentication;
        String username = (String) authenticationToken.getPrincipal();
        String credentials = (String) authenticationToken.getCredentials();
        String code = authenticationToken.getCode();
        String uuid = authenticationToken.getUuid();
        String type = authenticationToken.getType();
        //验证类型为空
        if (StringUtils.isBlank(type)){
            username = MyCode.AUTHENTICATION_TYPE_EMPTY;
        }
        //用户名密码验证码登录
        if (type.equals(AuthTypeEnum.USERNAME_PASSWORD.getCode())) {
            username = defaultLogin(username, credentials, code, uuid);
        }else
        //邮箱验证码登录
        if (type.equals(AuthTypeEnum.EMAIL_CODE.getCode())) {
            username = emailLogin(username, code, uuid);
        }else {
            //验证类型无效
            username = MyCode.AUTHENTICATION_TYPE_INVALID;
        }
        //校验用户并返回用户信息
        UserDetails userDetails = userService.loadUserByUsername(username);
        //回填验证信息到上下文中
        MyAuthenticationToken myAuthenticationToken =
                new MyAuthenticationToken(userDetails,
                        userDetails.getAuthorities(), authenticationToken.getCredentials());
        myAuthenticationToken.setDetails(authenticationToken);
        return myAuthenticationToken;
    }

    /**
     * 用户名密码验证码登录
     * @param username 用户名
     * @param credentials 密码
     * @param code 验证码
     * @param uuid 随机数
     * @return 用户名称或者状态码
     */
    private String defaultLogin(String username, String credentials, String code, String uuid) {
        String accountCode = redisUtil.get(RedisConstant.USER_VERIFICATION_CODE.concat(CommonConstant.COLON).concat(uuid));
        if (StringUtils.isBlank(code) || StringUtils.isBlank(uuid)) {
            username = MyCode.VERIFICATION_CODE_EMPTY;
        }else if (accountCode == null){
            username = MyCode.VERIFICATION_CODE_FAIL;
        }else if (!Objects.equals(accountCode, code)) {
            username = MyCode.VERIFICATION_CODE_ERROR;
        }else {
            QueryWrapper<UserInfo> userInfoQueryWrapper = new QueryWrapper<>();
            userInfoQueryWrapper.eq(UserInfo.USERNAME, username);
            UserInfo userInfo = userInfoService.getOne(userInfoQueryWrapper);
            if (userInfo == null){
                username = MyCode.USER_NOT_FOUND;
            }else if (!new BCryptPasswordEncoder().matches(credentials, userInfo.getPwd())) {
                username = MyCode.PASSWORD_ERROR;
            }else {
                username = userInfo.getUsername();
            }

        }
        return username;
    }

    /**
     * 邮箱验证码登录
     * @param username 邮箱
     * @param code 验证码
     * @param uuid 随机数
     * @return 用户名称或者状态码
     */
    private String emailLogin(String username, String code, String uuid) {
        String accountCode = redisUtil.get(RedisConstant.EMAIL_VERIFICATION_CODE.concat(CommonConstant.COLON).concat(uuid));
        if (StringUtils.isBlank(code) || StringUtils.isBlank(uuid)) {
            username = MyCode.VERIFICATION_CODE_EMPTY;
        }else if (accountCode == null){
            username = MyCode.VERIFICATION_CODE_FAIL;
        } else if (!accountCode.equals(code)) {
            username = MyCode.VERIFICATION_CODE_ERROR;
        }else {
            QueryWrapper<UserInfo> userInfoQueryWrapper = new QueryWrapper<>();
            userInfoQueryWrapper.eq(UserInfo.EMAIL, username);
            UserInfo userInfo = userInfoService.getOne(userInfoQueryWrapper);
            if (userInfo == null){
                username = MyCode.USER_NOT_FOUND;
            }else {
                username = userInfo.getUsername();
            }
        }
        return username;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return MyAuthenticationToken.class.isAssignableFrom(authentication);
    }
}

实现UserDetailsService

主要做了如下操作:

  1. 实现了loadUserByUsername(String username)方法
    1. 对输入的username(错误代码或者是用户名)进行匹配
      1. 验证码为空
      2. 验证码失效
      3. 验证码错误
      4. 用户不存在
      5. 密码错误
      6. 获取缓存中的用户信息
      7. 校验缓存中是否存在用户信息
      8. 不存在则通过用户名称获取用户信息
      9. 封装为LoginUserVo()实体
      10. 验证用户名密码错误
      11. 验证用户过期
      12. 验证用户锁定
      13. 验证用户凭证过期
      14. 验证用户禁用
      15. 返回用户信息UserDetails()
@Service
public class UserServiceImpl implements IUserService, UserDetailsService {

    @Autowired
    private RedisUtil redisUtil;

    @Autowired
    private IUserInfoService userInfoService;

    @Override
    public UserDetails loadUserByUsername(String username) {
        // 验证码为空
        if (username.equals(MyCode.VERIFICATION_CODE_EMPTY)){
            throw new UsernameNotFoundException(
                    I18nUtils.getMessage(LoginConstant.VERIFICATION_CODE_EMPTY));
        }
        // 验证码失效
        if (username.equals(MyCode.VERIFICATION_CODE_FAIL)){
            throw new UsernameNotFoundException(
                    I18nUtils.getMessage(LoginConstant.VERIFICATION_CODE_FAIL));
        }
        // 验证码错误
        if (username.equals(MyCode.VERIFICATION_CODE_ERROR)){
            throw new UsernameNotFoundException(
                    I18nUtils.getMessage(LoginConstant.VERIFICATION_CODE_ERROR));
        }
        // 用户不存在
        if (username.equals(MyCode.USER_NOT_FOUND)){
            throw new UsernameNotFoundException(
                    I18nUtils.getMessage(LoginConstant.BADCREDENTIALS_EXCEPTION));
        }
        // 密码错误
        if (username.equals(MyCode.PASSWORD_ERROR)){
            throw new UsernameNotFoundException(
                    I18nUtils.getMessage(LoginConstant.PASSWORD_ERROR));
        }
        String userInfoStr = redisUtil.get(RedisConstant.USER.concat(CommonConstant.COLON).concat(username));
        LoginUserVo loginUserVo = null;
        if (userInfoStr != null) {
            loginUserVo = JSON.parseObject(userInfoStr, LoginUserVo.class);
        }else {
            QueryWrapper<UserInfo> userInfoQueryWrapper = new QueryWrapper<>();
            userInfoQueryWrapper.eq(UserInfo.USERNAME, username);
            UserInfo userInfo = userInfoService.getOne(userInfoQueryWrapper);
            if (userInfo != null) {
                loginUserVo = new LoginUserVo();
                BeanUtils.copyProperties(userInfo, loginUserVo);
            }
        }
        //用户名或密码错误
        if (loginUserVo == null) {
            throw new UsernameNotFoundException(
                    I18nUtils.getMessage(LoginConstant.BADCREDENTIALS_EXCEPTION));
        }
        //用户过期
        if (loginUserVo.getIsExpired()) {
            throw new AccountExpiredException(
                    I18nUtils.getMessage(LoginConstant.ACCOUNTEXPIRED_EXCEPTION));
        }
        //用户锁定
        if (loginUserVo.getIsLocked()) {
            throw new LockedException(
                    I18nUtils.getMessage(LoginConstant.LOCKED_EXCEPTION));
        }
        //用户凭证过期
        if (loginUserVo.getIsCredentialsExpired()) {
            throw new CredentialsExpiredException(
                    I18nUtils.getMessage(LoginConstant.CREDENTIALSEXPIRED_EXCEPTION));
        }
        //用户已禁用过期
        if (loginUserVo.getIsDisable()) {
            throw new DisabledException(
                    I18nUtils.getMessage(LoginConstant.DISABLED_EXCEPTION));
        }
        return loginUserVo;
    }
}

实现AccessDeniedHandler

主要做了如下操作:

  1. 以文本输出流的形式返回给客户端相关信息(加持了中英文)
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {

    @SneakyThrows
    @Override
    public void handle(HttpServletRequest httpServletRequest,
                       HttpServletResponse httpServletResponse, AccessDeniedException e) {
        httpServletResponse.setContentType(Constant.APPLICATION_JSON_UTF8_VALUE);
        httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
        PrintWriter out = httpServletResponse.getWriter();
        out.write(JSON.toJSONString(
                RespJson.error(HttpServletResponse.SC_FORBIDDEN,
                        I18nUtils.getMessage(LoginConstant.FORBIDDEN))));
    }
}

实现AuthenticationEntryPoint

主要做了如下操作:

  1. 以文本输出流的形式返回给客户端相关信息(加持了中英文)
@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @SneakyThrows
    @Override
    public void commence(HttpServletRequest httpServletRequest,
                         HttpServletResponse httpServletResponse, AuthenticationException e) {
        httpServletResponse.setContentType(Constant.APPLICATION_JSON_UTF8_VALUE);
        httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        PrintWriter out = httpServletResponse.getWriter();
        out.write(JSON.toJSONString(
                RespJson.error(HttpServletResponse.SC_UNAUTHORIZED,
                        I18nUtils.getMessage(LoginConstant.UNAUTHORIZED))));
    }
}

实现AuthenticationFailureHandler

主要做了如下操作:

  1. 以文本输出流的形式返回给客户端相关信息(加持了中英文)
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {

    @SneakyThrows
    @Override
    public void onAuthenticationFailure(
            HttpServletRequest httpServletRequest,
            HttpServletResponse httpServletResponse, AuthenticationException e) {
        httpServletResponse.setContentType(Constant.APPLICATION_JSON_UTF8_VALUE);
        httpServletResponse.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        PrintWriter out = httpServletResponse.getWriter();
        if (e instanceof BadCredentialsException) {
            out.write(JSON.toJSONString(
                    RespJson.error(I18nUtils.getMessage(LoginConstant.PASSWORD_ERROR))));
        } else {
            out.write(JSON.toJSONString(RespJson.error(e.getMessage())));
        }
    }
}

实现AuthenticationFailureHandler

主要做了如下操作:

  1. 以文本输出流的形式返回给客户端相关信息(加持了中英文)
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {

    @SneakyThrows
    @Override
    public void onAuthenticationFailure(
            HttpServletRequest httpServletRequest,
            HttpServletResponse httpServletResponse, AuthenticationException e) {
        httpServletResponse.setContentType(Constant.APPLICATION_JSON_UTF8_VALUE);
        httpServletResponse.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        PrintWriter out = httpServletResponse.getWriter();
        if (e instanceof BadCredentialsException) {
            out.write(JSON.toJSONString(
                    RespJson.error(I18nUtils.getMessage(LoginConstant.PASSWORD_ERROR))));
        } else {
            out.write(JSON.toJSONString(RespJson.error(e.getMessage())));
        }
    }
}

实现AuthenticationSuccessHandler

主要做了如下操作:

  1. 根据上下文中用户信息生成token
  2. 以文本输出流的形式返回给客户端相关信息(加持了中英文)
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {


    private final IUserService userService;

    public MyAuthenticationSuccessHandler(IUserService userService) {
        this.userService = userService;
    }

    @SneakyThrows
    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest,
                                        HttpServletResponse httpServletResponse,
                                        Authentication authentication) {
        String token = userService.getToken((LoginUserVo) authentication.getPrincipal());
        httpServletResponse.setContentType(Constant.APPLICATION_JSON_UTF8_VALUE);
        httpServletResponse.setStatus(HttpServletResponse.SC_OK);
        PrintWriter out = httpServletResponse.getWriter();
        out.write(JSON.toJSONString(
                RespJson.success(I18nUtils.getMessage(LoginConstant.LOGIN_SUCCESS), token)));
    }
}

实现AuthenticationSuccessHandler

主要做了如下操作:

  1. HttpServletRequest中获取token
  2. 解析token拿到用户名称
  3. 根据用户名称删除缓存中的用户信息
  4. 根据用户名称删除缓存中的权限信息
  5. 根据用户名称删除缓存中的用户获取token剩余过期时间
  6. 将未过期token加入到黑名单中,过期时间为剩余过期时间
  7. 以文本输出流的形式返回给客户端相关信息(加持了中英文)
@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {

    @Autowired
    private RedisUtil redisUtil;

    @Value("${jwt.authorization}")
    private String authorization;

    @Autowired
    private JwtTokenUtils jwtTokenUtils;

    @SneakyThrows
    @Override
    public void onLogoutSuccess(HttpServletRequest httpServletRequest,
                                HttpServletResponse httpServletResponse,
                                Authentication authentication) {
        //获取token
        String token = httpServletRequest.getHeader(authorization);
        //获取token中的信息
        Claims allClaimsFromToken = jwtTokenUtils.getAllClaimsFromToken(token);
        //删除redis中用户信息
        redisUtil.delete(RedisConstant.USER
                .concat(CommonConstant.COLON)
                .concat(allClaimsFromToken.getSubject()));
        //删除redis中权限信息
        redisUtil.delete(RedisConstant.URL
                .concat(CommonConstant.COLON)
                .concat(allClaimsFromToken.getSubject()));
        //获取当前token剩余毫秒数
        long expiration = jwtTokenUtils.getExpirationTimeMillisFromToken(token);
        //添加token黑名单
        redisUtil.setEx(
                RedisConstant.EXPIRATION_TOKEN
                        .concat(CommonConstant.COLON)
                        .concat(token),
                JSON.toJSONString(allClaimsFromToken),
                expiration,
                TimeUnit.MILLISECONDS);
        //返回提示
        httpServletResponse.setContentType(Constant.APPLICATION_JSON_UTF8_VALUE);
        PrintWriter out = httpServletResponse.getWriter();
        out.write(JSON.toJSONString(
                RespJson.success(I18nUtils.getMessage(LoginConstant.LOGOUT_SUCCESS))));
    }
}

继承OncePerRequestFilter

主要做了如下操作:

  1. 通过HttpServletRequest获取token
  2. 验证token是否为空
  3. 解析token获取用户名称
  4. 验证用户名称是否为空
  5. 验证上下文中认证信息是否存在
  6. 验证token是否有效
  7. 验证token是否在黑名单中
  8. 调用loadUserByUsername()函数,校验用户是否有效
  9. 整理用户信息,回填验证信息到上下文中
@Component
public class JwtAuthorizationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private JwtTokenUtils jwtTokenUtils;

    @Value("${jwt.authorization}")
    private String authorization;

    @Autowired
    private IUserService userService;

    @Autowired
    private RedisUtil redisUtil;

    @SneakyThrows
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    @NonNull HttpServletResponse response,
                                    @NonNull FilterChain chain) {
        //获取请求携带的token
        final String token = request.getHeader(authorization);
        //验证token是否为空
        if (StringUtils.isNotBlank(token)) {
            //解析token中的用户名
            String username = jwtTokenUtils.getUsernameFromToken(token);
            //验证用户名称是否为空
            //验证上下文中认证信息是否存在
            //验证token是否有效
            //验证token是否在黑名单中
            if (StringUtils.isNotBlank(username) &&
                    SecurityContextHolder.getContext().getAuthentication() == null &&
                    jwtTokenUtils.validateToken(token, username) &&
                    !redisUtil.hasKey(RedisConstant.EXPIRATION_TOKEN.concat(CommonConstant.COLON).concat(token))) {
                //校验用户并返回用户信息
                UserDetails userDetails = userService.loadUserByUsername(username);
                //回填验证信息到上下文中
                MyAuthenticationToken authentication = new MyAuthenticationToken(
                        userDetails, userDetails.getAuthorities(), token);
                authentication.setDetails(
                        new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }
        //继续执行
        chain.doFilter(request, response);
    }
}

总结

我们为了实现多种登陆方式,重写了springsecurity的认证拦截器中的方法Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response),包装了带有自定义参数的AbstractAuthenticationToken.java,自定义了身份验证接口AuthenticationProvider()中的Authentication authenticate(Authentication authentication)方法,完成了多种登录方式的整合。
至此,完成了springboot+springsecurity+jwt的整合。


你可能感兴趣的:(单应用多租户SaaS平台实践,spring,security,jwt,安全框架,spring,boot,登录)