security整合JWT

前述

SpringSecurity整合JWT实现无状态登录

引入依赖

<!-- JWT -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

yml配置

spring:
  redis:
    host: localhost
    port: 6379
    database: 0
    password:
    timeout: 10s
    lettuce:
      pool:
        min-idle: 0
        max-idle: 8
        max-active: 8
        max-wait: -1ms
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/security-simple?useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: admin
    type: com.alibaba.druid.pool.DruidDataSource
  thymeleaf:
    cache: false
  mvc:
    static-path-pattern: /static/**
#mybatis-plus 配置
mybatis-plus:
  mapper-locations: classpath:/mapper/**/*.xml
  type-aliases-package: com.example.jwt.project.sys.entity.*
  type-aliases-super-type: java.lang.Object
  global-config:
    db-config:
      id-type: auto
  configuration:
    aggressive-lazy-loading: false
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
token:
  # 令牌自定义标识
  header: Authorization
  # 令牌密钥
  secret: guhjdsagyuhj5678324hfygf
  # 令牌有效期(默认30分钟)
  expireTime: 30

新增TokenManager管理器

/**
 * @version 1.0.0
 * @className: TokenManager
 * @description: jwt令牌管理:
 * JWT实现大体思路:
 * 根据UUID生成一个JWT——token,此 token 并不会设置过期时间,只做数据的记录
 * (缓存前缀 + UUID) 作为 Redis 缓存对象的 key,同时 UUID 又是保存在 token 中的,所以 token 也就与 Redis关联了起来
 * 只有通过 token 获取到 UUID后,才能在 Redis 中获取到登录对象,如果没有 token,也就获取不到对象,则认证失败,否则成功
 * @author: LiJunYi
 * @create: 2022/7/27 11:16
 */
@Component
@Slf4j
public class TokenManager
{
    /**
     * 令牌自定义标识
     */
    @Value("${token.header}")
    private String header;

    /**
     * 令牌秘钥
     */
    @Value("${token.secret}")
    private String secret;

    /**
     * 令牌有效期(默认30分钟)
     */
    @Value("${token.expireTime}")
    private int expireTime;

    protected static final long MILLIS_SECOND = 1000;

    protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;

    private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 1000L;

    private final RedisCache redisCache;

    public TokenManager(RedisCache redisCache) {
        this.redisCache = redisCache;
    }


    /**
     * 从 request 中获取token
     * 通过解析token获取用户在redis中缓存的key从而获取到登录用户信息
     *
     * @param request 请求
     * @return {@link LoginUser}
     */
    public LoginUser getLoginUser(HttpServletRequest request)
    {
        // 获取请求携带的令牌
        String token = getToken(request);
        if (StrUtil.isNotEmpty(token))
        {
            try
            {
                Claims claims = parseToken(token);
                // 获取UUID
                String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
                // 获取 Redis 缓存中的 key
                String userKey = getTokenKey(uuid);
                return redisCache.getCacheObject(userKey);
            }
            catch (Exception ignored)
            {

            }
        }
        return null;
    }

    /**
     * 设置用户身份信息
     */
    public void setLoginUser(LoginUser loginUser)
    {
        if (ObjectUtil.isNotNull(loginUser) && StrUtil.isNotEmpty(loginUser.getToken()))
        {
            refreshToken(loginUser);
        }
    }

    /**
     * 删除令牌
     *
     * @param token 令牌
     */
    public void removeToken(String token)
    {
        if (StrUtil.isNotEmpty(token))
        {
            String userKey = getTokenKey(token);
            redisCache.deleteObject(userKey);
        }
    }

    /**
     * 创建令牌
     *
     * @param loginUser 登录用户
     * @return {@link String}
     */
    public String createToken(LoginUser loginUser)
    {
        String uuid = IdUtil.fastUUID();
        loginUser.setToken(uuid);
        refreshToken(loginUser);

        Map<String, Object> claims = new HashMap<>(8);
        claims.put(Constants.LOGIN_USER_KEY, uuid);
        return createToken(claims);
    }


    /**
     * 验证令牌有效期,自动刷新缓存
     *
     * @param loginUser 登录用户
     */
    public void verifyToken(LoginUser loginUser)
    {
        long expireTime = loginUser.getExpireTime();
        long currentTime = System.currentTimeMillis();
        if (expireTime - currentTime <= MILLIS_MINUTE_TEN)
        {
            refreshToken(loginUser);
        }
    }

    /**
     * 创建token时存入当前登录用户并同时刷新令牌过期时间
     *
     * @param loginUser 登录用户
     */
    public void refreshToken(LoginUser loginUser)
    {
        loginUser.setLoginTime(System.currentTimeMillis());
        loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
        // 根据uuid将loginUser缓存
        String userKey = getTokenKey(loginUser.getToken());
        redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
    }

    /**
     * 创建令牌
     *
     * @param claims 数据声明
     * @return {@link String}
     */
    public String createToken(Map<String, Object> claims)
    {
        return Jwts.builder()
                .setClaims(claims)
                .signWith(SignatureAlgorithm.HS512, secret).compact();
    }

    /**
     * 从令牌中获取数据声明
     *
     * @param token 令牌
     * @return 数据声明
     */
    private Claims parseToken(String token)
    {
        return Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token)
                .getBody();
    }

    /**
     * 获取令牌
     *
     * @param request 请求
     * @return {@link String}
     */
    private String getToken(HttpServletRequest request)
    {
        String token = request.getHeader(header);
        if (StrUtil.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX))
        {
            token = token.replace(Constants.TOKEN_PREFIX, "");
        }
        return token;
    }

    /**
     * 获取令牌在缓存中的key
     *
     * @param uuid 随机UUID
     * @return {@link String}
     */
    private String getTokenKey(String uuid)
    {
        return Constants.LOGIN_TOKEN_KEY + uuid;
    }
}

新增JWT自定义认证过滤器

/**
 * @version 1.0.0
 * @className: JwtAuthenticationTokenFilter
 * @description: JWT自定义认证过滤器
 * @author: LiJunYi
 * @create: 2022/7/27 11:54
 */
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{
    private final TokenManager tokenManager;


    public JwtAuthenticationTokenFilter(TokenManager tokenManager) {
        this.tokenManager = tokenManager;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        LoginUser loginUser = tokenManager.getLoginUser(request);
        if (ObjectUtil.isNotNull(loginUser) && ObjectUtil.isNull(SecurityUtils.getAuthentication()))
        {
            tokenManager.verifyToken(loginUser);
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }
        chain.doFilter(request,response);
    }
}

修改退出处理器

/**
 * @version 1.0.0
 * @className: LogoutSuccessHandlerImpl
 * @description: 退出处理器
 * @author: LiJunYi
 * @create: 2022/7/26 8:34
 */
@Configuration
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler
{
    private final TokenManager tokenManager;

    public LogoutSuccessHandlerImpl(TokenManager tokenManager)
    {
        this.tokenManager = tokenManager;
    }


    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication e) throws IOException {
        LoginUser loginUser = tokenManager.getLoginUser(request);
        if(ObjectUtil.isNotNull(loginUser))
        {
            //移除
            tokenManager.removeToken(loginUser.getToken());
        }
        ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.success("退出成功")));
    }
}

修改SpringSecurity配置

/**
 * @version 1.0.0
 * @className: SecurityConfig
 * @description: SpringSecurity 5.7.x新用法配置
 * @author: LiJunYi
 * @create: 2022/7/26 8:43
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig
{
    /**
     * 自定义用户登录处理逻辑
     */
    private final MyUserDetailsServiceImpl userDetailsService;

    /**
     * 注销成功后处理器
     */
    private final LogoutSuccessHandlerImpl logoutSuccessHandler;

    /**
     * 无权限处理器
     */
    private final AuthenticationEntryPointImpl unauthorizedHandler;

    /**
     * 并发登录控制处理器
     */
    private final CustomExpiredSessionStrategyImpl expiredSessionStrategy;

    /**
     * jwt身份验证令牌过滤器
     */
    private final JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    /**
     * 安全配置
     * 构造函数注入
     *
     * @param tokenManager                 令牌管理器
     * @param redisCache                   缓存
     * @param userDetailsService           用户详细信息服务
     * @param logoutSuccessHandler         退出处理器
     * @param unauthorizedHandler          无权限处理器
     * @param expiredSessionStrategy       并发登录控制处理器
     * @param jwtAuthenticationTokenFilter JWT登录过滤器
     */
    @Autowired
    public SecurityConfig(TokenManager tokenManager, RedisCache redisCache, MyUserDetailsServiceImpl userDetailsService, LogoutSuccessHandlerImpl logoutSuccessHandler,
                          AuthenticationEntryPointImpl unauthorizedHandler,
                          CustomExpiredSessionStrategyImpl expiredSessionStrategy,
                          JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter) {
        this.jwtAuthenticationTokenFilter = jwtAuthenticationTokenFilter;
        this.userDetailsService = userDetailsService;
        this.logoutSuccessHandler = logoutSuccessHandler;
        this.unauthorizedHandler = unauthorizedHandler;
        this.expiredSessionStrategy = expiredSessionStrategy;
    }


    /**
     * 获取AuthenticationManager(认证管理器),登录时认证使用
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
        AuthenticationManager authenticationManager = configuration.getAuthenticationManager();
        return configuration.getAuthenticationManager();
    }

    /**
     * 配置加密方式
     *
     * @return {@link PasswordEncoder}
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 侦听器配置,使 Spring Security 更新有关会话生命周期事件的信息
     * 并发会话控制:https://docs.spring.io/spring-security/reference/servlet/authentication/session-management.html
     * @return {@link HttpSessionEventPublisher}
     */
    @Bean
    public HttpSessionEventPublisher httpSessionEventPublisher() {
        return new HttpSessionEventPublisher();
    }

    /**
     * 过滤器链
     *
     * @param http http
     * @return {@link SecurityFilterChain}
     * @throws Exception 异常
     */
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception
    {
        http.authorizeRequests(authorize ->
                authorize.mvcMatchers("/userLogin","/noPermission").permitAll()
                        .anyRequest().authenticated()
        )
                .csrf().disable()
                // 基于token,不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .headers().frameOptions().disable();
        // 退出过滤器
        http.logout(logout -> logout
                .logoutUrl("/logout")
                .deleteCookies("JSESSIONID")
                .logoutSuccessHandler(logoutSuccessHandler));
        // 添加JWT filter
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        // by default uses a Bean by the name of corsConfigurationSource
        http.cors(withDefaults());
        // 无权限处理器
        http.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).accessDeniedPage("/noPermission");
        // 并发会话控制
        http.sessionManagement(session ->  session
                .maximumSessions(1)
                .expiredSessionStrategy(expiredSessionStrategy));
        http.userDetailsService(userDetailsService);
        return http.build();
    }

    /**
     * 配置跨源访问(CORS)
     * 官方文档:https://docs.spring.io/spring-security/reference/servlet/integrations/cors.html
     * @return {@link CorsConfigurationSource}
     */
    @Bean
    CorsConfigurationSource corsConfigurationSource()
    {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("http://localhost:8080/","https://example.com"));
        configuration.setAllowedMethods(Arrays.asList("GET","POST"));
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

修改登录接口

/**
 * 登录控制器
 *
 * @author LiJunYi
 * @date 2022/07/26
 */
@RestController
public class LoginController
{
    @Autowired
    ISysUserService userService;

    @Autowired
    SysLoginService loginService;

    /**
     * 登录方法
     *
     * @param user 用户
     * @return {@link AjaxResult}
     */
    @PostMapping("userLogin")
    public AjaxResult login(SysUser user)
    {
        if (StrUtil.isEmpty(user.getUsername()) || StrUtil.isEmpty(user.getPassword()))
        {
            return AjaxResult.error("用户名或密码未输入!");
        }
        String token = loginService.login(user.getUsername(),user.getPassword());
        return AjaxResult.success(Constants.TOKEN, token);
    }
}

登录业务逻辑代码

/**
 * @version 1.0.0
 * @className: SysLoginService
 * @description: 登录业务
 * @author: LiJunYi
 * @create: 2022/7/27 12:53
 */
@Component
public class SysLoginService
{
    @Autowired
    private TokenManager tokenManager;

    @Resource
    private AuthenticationManager authenticationManager;

    @Autowired
    private RedisCache redisCache;

    @Autowired
    private ISysUserService userService;

    /**
     * 登录验证
     *
     * @param username 用户名
     * @param password 密码
     * @return 结果
     */
    public String login(String username, String password)
    {
        // 用户验证
        Authentication authentication;
        try
        {
            // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
            authentication = authenticationManager
                    .authenticate(new UsernamePasswordAuthenticationToken(username, password));
        }
        catch (Exception e)
        {
            if (e instanceof BadCredentialsException)
            {
                throw new ServiceException("用户名密码错误");
            }
            else
            {
                throw new ServiceException(e.getMessage());
            }
        }
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        // 生成token
        return tokenManager.createToken(loginUser);
    }
}

测试效果图

SpringSecuritySpringSecuritySpringSecuritySpringSecuritySpringSecurity
Last Updated 6/14/2024, 3:05:31 AM