「工程实践」Spring Security(四)基于Redis的Token自动续签优化

本文基于上一篇文章:《Spring Security(三)整合 JWT 实现无状态登录示例》。

SpringSecurity 整合 JWT 实现无状态登录示例中,我们在 JwtAuthenticationFilter (自定义JWT认证过滤器) 解析 Token 成功后,提供了续签逻辑:

/**
 * 刷新Token的时机:
 * 1. 当前时间 < token过期时间
 * 2. 当前时间 > (签发时间 + (token过期时间 - token签发时间)/2)
 */
private void refreshToken(HttpServletResponse response, Claims claims) {
    // 当前时间
    long current = System.currentTimeMillis();
    // token签发时间
    long issuedAt = claims.getIssuedAt().getTime();
    // token过期时间
    long expiration = claims.getExpiration().getTime();
    // (当前时间 < token过期时间) && (当前时间 > (签发时间 + (token过期时间 - token签发时间)/2))
    if ((current < expiration) && (current > (issuedAt + ((expiration - issuedAt) / 2)))) {
        /*
         * 重新生成token
         */
        Calendar calendar = Calendar.getInstance();
        // 设置签发时间
        calendar.setTime(new Date());
        Date now = calendar.getTime();
        // 设置过期时间: 5分钟
        calendar.add(Calendar.MINUTE, 5);
        Date time = calendar.getTime();
        String refreshToken = Jwts.builder()
                .setSubject(claims.getSubject())
                // 签发时间
                .setIssuedAt(now)
                // 过期时间
                .setExpiration(time)
                // 算法与签名(同生成token):这里算法采用HS512,常量中定义签名key
                .signWith(SignatureAlgorithm.HS512, ConstantKey.SIGNING_KEY)
                .compact();
        // 主动刷新token,并返回给前端
        response.addHeader("refreshToken", refreshToken);
        log.info("刷新token执行时间: {}", (System.currentTimeMillis() - current) + " 毫秒");
    }
}

这里的逻辑是:Token 未过期并且当前时间已经超过 Token 有效时间的一半,重新生成一个 refreshToken,并返回给前端,前端需要用 refreshToken 替换之前旧的 Token

「工程实践」Spring Security(三)整合JWT实现无状态登录示例

JSON Web Token(缩写 JWT)基于JSON格式信息一种Token令牌,是目前最流行的跨域认证解决方案。

  • JWT 的原理是,服务器认证以后,生成一个 JSON 对象,发回给用户。
  • 此后,用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名。
  • 服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。

1. 依赖与配置文件

  1. pom.xml 中引入依赖:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.2.0</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.78</version>
</dependency>
  1. 用户信息从数据库中获取,在 application.yml 配置文件中配置:
server:
  port: 8080

spring:
  datasource:
    url: jdbc:mysql://192.168.2.100:3306/security?characterEncoding=UTF8&serverTimezone=Asia/Shanghai
    username: developer
    password: 05bZ/OxTB:X+yd%1

mybatis:
  mapper-locations: classpath:mapper/*.xml

2. 自定义Security策略

2.1 通过继承 WebSecurityConfigurerAdapter 实现自定义Security策略
/**
 * 1. 通过继承 WebSecurityConfigurerAdapter 实现自定义Security策略
 * @Configuration:声明当前类是一个配置类
 * @EnableWebSecurity:开启WebSecurity模式
 * @EnableGlobalMethodSecurity(securedEnabled=true):开启注解,支持方法级别的权限控制
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private UserDetailsService userDetailsService;

    @Resource
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    /**
     * 全局请求忽略规则配置
     */
    @Override
    public void configure(WebSecurity web) {
        // 需要放行的URL
        web.ignoring().antMatchers("/register", "/hello");
    }

    /**
     * 自定义认证策略:登录的时候会进入
     */
    @Override
    public void configure(AuthenticationManagerBuilder auth) {
        // 2.通过实现 AuthenticationProvider 自定义身份认证验证组件
        auth.authenticationProvider(new AuthenticationProviderImpl(userDetailsService, bCryptPasswordEncoder));
    }

    /**
     * HTTP 验证规则
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // 关闭Session
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            // 所有请求需要身份认证
            .and().authorizeRequests().anyRequest().authenticated()
            .and()
            // 3.自定义JWT登录过滤器
            .addFilter(new JwtLoginFilter(authenticationManager()))
            // 4.自定义JWT认证过滤器
            .addFilter(new JwtAuthenticationFilter(authenticationManager()))
            // 5.自定义认证拦截器,也可以直接使用内置实现类Http403ForbiddenEntryPoint
            .exceptionHandling().authenticationEntryPoint(new AuthenticationEntryPointImpl())
            // 允许跨域
            .and().cors()
            // 禁用跨站伪造
            .and().csrf().disable();
    }
}

BCryptPasswordEncoder 解析器注入到容器

0%