SpringBoot中双Token实现无感刷新方案

一、双Token机制原理

1.1 基本概念

双Token机制是指同时使用Access Token和Refresh Token来实现认证和刷新:

图片[1]_SpringBoot中双Token实现无感刷新方案_知途无界
  • Access Token:短期有效(通常30分钟-2小时),用于业务请求
  • Refresh Token:长期有效(通常7天-30天),用于获取新Access Token
sequenceDiagram
    participant Client
    participant Server
    Client->>Server: 登录请求(用户名+密码)
    Server-->>Client: 返回accessToken+refreshToken
    Client->>Server: 业务请求(带accessToken)
    alt accessToken有效
        Server-->>Client: 返回业务数据
    else accessToken过期
        Server-->>Client: 401 Unauthorized
        Client->>Server: 使用refreshToken请求新accessToken
        Server-->>Client: 返回新accessToken
        Client->>Server: 重试原请求(带新accessToken)
        Server-->>Client: 返回业务数据
    end

1.2 优势分析

  • 安全性:Access Token短期有效,即使泄露影响有限
  • 用户体验:无感刷新避免频繁登录
  • 灵活性:可独立控制两种Token的过期时间

二、SpringBoot实现方案

2.1 依赖配置

<!-- pom.xml -->
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
        <version>0.11.5</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-impl</artifactId>
        <version>0.11.5</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-jackson</artifactId>
        <version>0.11.5</version>
        <scope>runtime</scope>
    </dependency>
</dependencies>

2.2 JWT工具类

public class JwtUtil {
    private static final String SECRET_KEY = "your-256-bit-secret";
    private static final long ACCESS_EXPIRE = 30 * 60 * 1000; // 30分钟
    private static final long REFRESH_EXPIRE = 7 * 24 * 60 * 60 * 1000; // 7天

    // 生成accessToken
    public static String generateAccessToken(String username) {
        return Jwts.builder()
                .setSubject(username)
                .setExpiration(new Date(System.currentTimeMillis() + ACCESS_EXPIRE))
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY)
                .compact();
    }

    // 生成refreshToken
    public static String generateRefreshToken(String username) {
        return Jwts.builder()
                .setSubject(username)
                .setExpiration(new Date(System.currentTimeMillis() + REFRESH_EXPIRE))
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY)
                .compact();
    }

    // 验证并解析Token
    public static Claims parseToken(String token) {
        return Jwts.parser()
                .setSigningKey(SECRET_KEY)
                .parseClaimsJws(token)
                .getBody();
    }
}

2.3 登录接口实现

@RestController
@RequestMapping("/auth")
public class AuthController {

    @PostMapping("/login")
    public ResponseEntity<Map<String, String>> login(@RequestBody LoginRequest request) {
        // 1. 验证用户名密码(省略)
        // 2. 生成双Token
        String accessToken = JwtUtil.generateAccessToken(request.getUsername());
        String refreshToken = JwtUtil.generateRefreshToken(request.getUsername());

        // 3. 存储refreshToken(可存Redis或数据库)
        redisTemplate.opsForValue().set(
            "refresh:" + request.getUsername(), 
            refreshToken, 
            JwtUtil.REFRESH_EXPIRE, 
            TimeUnit.MILLISECONDS
        );

        // 4. 返回结果
        Map<String, String> tokens = new HashMap<>();
        tokens.put("accessToken", accessToken);
        tokens.put("refreshToken", refreshToken);
        return ResponseEntity.ok(tokens);
    }
}

2.4 无感刷新实现

2.4.1 拦截器配置

public class TokenRefreshInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token = request.getHeader("Authorization");

        try {
            Claims claims = JwtUtil.parseToken(token);
            // Token有效,放行
            return true;
        } catch (ExpiredJwtException e) {
            // Access Token过期,尝试刷新
            return refreshToken(request, response, e.getClaims().getSubject());
        } catch (Exception e) {
            // 其他异常
            response.sendError(HttpStatus.UNAUTHORIZED.value(), "无效的Token");
            return false;
        }
    }

    private boolean refreshToken(HttpServletRequest request, HttpServletResponse response, String username) throws IOException {
        String refreshToken = request.getHeader("Refresh-Token");

        // 1. 验证refreshToken是否存在且有效
        String storedRefreshToken = redisTemplate.opsForValue().get("refresh:" + username);
        if (refreshToken == null || !refreshToken.equals(storedRefreshToken)) {
            response.sendError(HttpStatus.UNAUTHORIZED.value(), "Refresh Token无效");
            return false;
        }

        try {
            Claims claims = JwtUtil.parseToken(refreshToken);
            // 2. 生成新的accessToken
            String newAccessToken = JwtUtil.generateAccessToken(username);

            // 3. 返回新token给客户端
            response.setHeader("New-Access-Token", newAccessToken);
            return true;
        } catch (Exception e) {
            response.sendError(HttpStatus.UNAUTHORIZED.value(), "Refresh Token过期");
            return false;
        }
    }
}

2.4.2 注册拦截器

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new TokenRefreshInterceptor())
                .addPathPatterns("/api/**")
                .excludePathPatterns("/auth/**");
    }
}

三、前端配合实现

3.1 Axios请求拦截

// 创建axios实例
const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_API,
  timeout: 5000
})

// 请求拦截器
service.interceptors.request.use(
  config => {
    const accessToken = localStorage.getItem('accessToken')
    if (accessToken) {
      config.headers['Authorization'] = `Bearer ${accessToken}`
    }
    return config
  },
  error => {
    return Promise.reject(error)
  }
)

// 响应拦截器
service.interceptors.response.use(
  response => {
    // 如果返回了新accessToken则更新
    const newAccessToken = response.headers['new-access-token']
    if (newAccessToken) {
      localStorage.setItem('accessToken', newAccessToken)
    }
    return response.data
  },
  async error => {
    const originalRequest = error.config

    // 处理401错误且不是刷新token请求
    if (error.response.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true

      try {
        // 使用refreshToken获取新accessToken
        const refreshToken = localStorage.getItem('refreshToken')
        const res = await axios.post('/auth/refresh', { refreshToken })

        // 存储新token
        localStorage.setItem('accessToken', res.data.accessToken)

        // 重试原始请求
        originalRequest.headers['Authorization'] = `Bearer ${res.data.accessToken}`
        return service(originalRequest)
      } catch (err) {
        // 刷新token失败,跳转登录
        router.push('/login')
        return Promise.reject(err)
      }
    }
    return Promise.reject(error)
  }
)

四、安全增强措施

4.1 Refresh Token保护

  1. 存储安全
  • 服务端:Redis存储,设置合理过期时间
  • 客户端:HttpOnly Cookie或Secure LocalStorage
  1. 使用限制
  • 每个Refresh Token只能使用一次
  • 使用后立即生成新Refresh Token(类似OAuth2机制)

4.2 黑名单机制

public class TokenBlacklist {
    // 添加token到黑名单
    public static void addToBlacklist(String token, long expireTime) {
        redisTemplate.opsForValue().set(
            "blacklist:" + token, 
            "1", 
            expireTime, 
            TimeUnit.MILLISECONDS
        );
    }

    // 检查token是否在黑名单
    public static boolean isBlacklisted(String token) {
        return redisTemplate.hasKey("blacklist:" + token);
    }
}

// 登出时调用
public void logout(String accessToken, String username) {
    // 计算token剩余有效时间
    long expireTime = JwtUtil.getExpireTime(accessToken) - System.currentTimeMillis();
    if (expireTime > 0) {
        TokenBlacklist.addToBlacklist(accessToken, expireTime);
    }
    // 删除refreshToken
    redisTemplate.delete("refresh:" + username);
}

五、性能优化建议

  1. 减少Refresh Token验证开销
  • 使用Redis缓存验证结果(设置短时间缓存)
  • 采用Bloom Filter预处理黑名单检查
  1. 并发请求处理
   // 使用ConcurrentHashMap防止并发刷新
   private static final ConcurrentHashMap<String, String> REFRESHING_USERS = new ConcurrentHashMap<>();

   boolean startRefresh(String username) {
       return REFRESHING_USERS.putIfAbsent(username, "1") == null;
   }

   void endRefresh(String username) {
       REFRESHING_USERS.remove(username);
   }
  1. 监控与告警
  • 监控Token刷新频率异常
  • 设置单用户刷新频率限制

六、完整流程图解

graph TD
    A[用户登录] --> B[生成双Token]
    B --> C[存储RefreshToken]
    C --> D[返回双Token给客户端]
    D --> E[客户端携带AccessToken请求]
    E --> F{Token有效?}
    F -->|是| G[处理业务请求]
    F -->|否| H[检查RefreshToken]
    H -->|有效| I[生成新AccessToken]
    I --> J[返回新Token并处理原请求]
    H -->|无效| K[返回401要求重新登录]

通过以上实现,SpringBoot应用可以构建一个安全、高效的无感刷新认证系统,既保证了用户体验,又确保了系统的安全性。实际应用中可根据业务需求调整Token过期时间和安全策略。

© 版权声明
THE END
喜欢就点个赞,支持一下吧!
点赞65 分享
评论 抢沙发
头像
欢迎您留下评论!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容