前端与 Spring Boot 后端无感 Token 刷新

举报
William 发表于 2025/07/25 09:20:06 2025/07/25
【摘要】 前端与 Spring Boot 后端无感 Token 刷新​​1. 引言​​在现代 Web 应用中,基于 Token 的身份验证(如 JWT)已成为主流方案。然而,Token 的固定有效期会导致用户体验问题:当 Token 过期时,用户需重新登录,打断操作流程。​​无感 Token 刷新​​通过在后台静默获取新 Token,避免了重复登录,显著提升了用户体验。本文将深入探讨如何在前端(Vue...

前端与 Spring Boot 后端无感 Token 刷新


​1. 引言​

在现代 Web 应用中,基于 Token 的身份验证(如 JWT)已成为主流方案。然而,Token 的固定有效期会导致用户体验问题:当 Token 过期时,用户需重新登录,打断操作流程。​​无感 Token 刷新​​通过在后台静默获取新 Token,避免了重复登录,显著提升了用户体验。本文将深入探讨如何在前端(Vue/React)与 Spring Boot 后端协同实现这一机制,涵盖技术原理、代码实现和最佳实践。


​2. 技术背景​

​2.1 Token 认证的挑战​

  • ​固定有效期​​:Access Token 通常有效期较短(如 30 分钟),过期后需重新登录。
  • ​安全性与体验的矛盾​​:延长 Token 有效期会降低安全性,而短有效期则影响体验。
  • ​刷新机制的必要性​​:通过 Refresh Token(长期有效)获取新的 Access Token,平衡安全与体验。

​2.2 无感刷新的核心目标​

  • ​用户无感知​​:在 Access Token 过期前或过期时,自动发起刷新请求,避免页面跳转或操作中断。
  • ​安全性保障​​:Refresh Token 需严格保护(如 HttpOnly Cookie),防止 XSS 攻击。
  • ​并发请求处理​​:多个并发请求触发刷新时,避免重复请求新 Token。

​3. 应用使用场景​

​3.1 场景1:后台管理系统​

  • ​目标​​:用户操作表格数据时,Token 过期自动刷新,避免弹窗提示重新登录。

​3.2 场景2:电商购物车流程​

  • ​目标​​:用户添加商品到购物车后,结算时若 Token 过期,静默刷新以完成支付。

​3.3 场景3:医疗健康应用​

  • ​目标​​:患者在填写健康问卷时,Token 过期不影响数据提交,提升服务连续性。

​4. 不同场景下详细代码实现​

​4.1 环境准备​

​4.1.1 技术栈​

  • ​前端​​:Vue 3 + Axios(或 React + Axios)。
  • ​后端​​:Spring Boot 2.7 + Spring Security + JWT。
  • ​工具​​:Postman(测试接口)、JWT Debuger(解析 Token)。

​4.1.2 依赖配置​

  • ​前端​​:
    npm install axios vue-router
  • ​后端​​:
    <!-- Spring Security 和 JWT 依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
        <version>0.11.5</version>
    </dependency>

​4.2 场景1:后台管理系统无感刷新​

​4.2.1 后端实现​

​1. JWT 工具类​
// JwtUtils.java
import io.jsonwebtoken.*;
import java.util.Date;

public class JwtUtils {
    private static final String SECRET_KEY = "your-secret-key"; // 实际应从配置读取
    private static final long ACCESS_TOKEN_EXPIRE = 30 * 60 * 1000; // 30分钟
    private static final long REFRESH_TOKEN_EXPIRE = 7 * 24 * 60 * 60 * 1000; // 7天

    public static String generateAccessToken(String username) {
        return Jwts.builder()
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_EXPIRE))
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY)
                .compact();
    }

    public static String generateRefreshToken(String username) {
        return Jwts.builder()
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + REFRESH_TOKEN_EXPIRE))
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY)
                .compact();
    }

    public static boolean isTokenExpired(String token) {
        try {
            Claims claims = Jwts.parser()
                    .setSigningKey(SECRET_KEY)
                    .parseClaimsJws(token)
                    .getBody();
            return claims.getExpiration().before(new Date());
        } catch (Exception e) {
            return true;
        }
    }
}
​2. Spring Security 配置​
// SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .authorizeRequests()
            .antMatchers("/auth/login", "/auth/refresh").permitAll()
            .anyRequest().authenticated()
            .and()
            .addFilter(new JwtAuthenticationFilter(authenticationManager()))
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
}

// JwtAuthenticationFilter.java
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) 
            throws IOException, ServletException {
        String token = request.getHeader("Authorization");
        if (token != null && token.startsWith("Bearer ")) {
            token = token.substring(7);
            if (!JwtUtils.isTokenExpired(token)) {
                // 验证通过,设置认证信息
                UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
                    JwtUtils.getUsernameFromToken(token), null, new ArrayList<>());
                SecurityContextHolder.getContext().setAuthentication(auth);
            }
        }
        chain.doFilter(request, response);
    }
}
​3. Token 刷新接口​
// AuthController.java
@RestController
public class AuthController {
    @PostMapping("/auth/refresh")
    public ResponseEntity<?> refreshAccessToken(HttpServletRequest request) {
        String refreshToken = request.getHeader("Refresh-Token");
        if (refreshToken == null || JwtUtils.isTokenExpired(refreshToken)) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Refresh Token 无效或过期");
        }
        String username = JwtUtils.getUsernameFromToken(refreshToken);
        String newAccessToken = JwtUtils.generateAccessToken(username);
        return ResponseEntity.ok(new JwtResponse(newAccessToken, refreshToken));
    }
}

​4.2.2 前端实现​

​1. Axios 拦截器配置​
// axios.js
import axios from 'axios';

const axiosInstance = axios.create({
    baseURL: 'http://localhost:8080',
});

let isRefreshing = false;
let failedRequests = [];

axiosInstance.interceptors.response.use(
    response => response,
    async error => {
        const originalRequest = error.config;
        if (error.response.status === 401 && !originalRequest._retry) {
            if (isRefreshing) {
                // 将请求加入队列,等待刷新完成后重试
                return new Promise(resolve => {
                    failedRequests.push(() => resolve(axiosInstance(originalRequest)));
                });
            }

            originalRequest._retry = true;
            isRefreshing = true;

            try {
                // 从 Cookie 获取 Refresh Token
                const refreshToken = getCookie('refresh_token');
                const response = await axiosInstance.post('/auth/refresh', {}, {
                    headers: { 'Refresh-Token': refreshToken }
                });

                const { access_token } = response.data;
                setToken(access_token); // 更新 Access Token
                originalRequest.headers['Authorization'] = `Bearer ${access_token}`;

                // 重试所有失败的请求
                failedRequests.forEach(cb => cb());
                failedRequests = [];
                return axiosInstance(originalRequest);
            } catch (refreshError) {
                // 刷新失败,跳转登录页
                logout();
                return Promise.reject(refreshError);
            } finally {
                isRefreshing = false;
            }
        }
        return Promise.reject(error);
    }
);

export default axiosInstance;
​2. 登录与 Token 存储​
// login.js
const login = async (username, password) => {
    const response = await axios.post('/auth/login', { username, password });
    const { access_token, refresh_token } = response.data;
    setToken(access_token); // 存储 Access Token 到内存或 LocalStorage
    setCookie('refresh_token', refresh_token, { httpOnly: true }); // Refresh Token 存储到 HttpOnly Cookie
};

​4.3 场景2:电商购物车无感刷新​

​4.3.1 前端优化:并发请求处理​

// cart.js
const addToCart = async (productId) => {
    try {
        const response = await axiosInstance.post('/cart/add', { productId });
        // 正常处理响应
    } catch (error) {
        if (error.response.status !== 401) {
            // 非 Token 过期错误,直接抛出
            throw error;
        }
        // 401 错误已由 Axios 拦截器自动处理,此处无需额外逻辑
    }
};

​5. 原理解释与原理流程图​

​5.1 无感刷新原理流程图​

[用户发起请求] → [携带 Access Token] → [Token 有效?]  
  → 是 → [正常处理请求]  
  → 否 → [检查是否正在刷新]  
    → 是 → [将请求加入队列]  
    → 否 → [发起 Refresh Token 请求]  
      → 成功 → [更新 Access Token,重试队列中的请求]  
      → 失败 → [跳转登录页]

​5.2 核心原理​

  • ​双 Token 机制​​:Access Token(短有效期)用于业务请求,Refresh Token(长有效期)用于获取新 Access Token。
  • ​拦截器自动化​​:前端通过 Axios 拦截器自动检测 401 错误并触发刷新流程。
  • ​请求队列​​:避免并发请求重复刷新,确保只有一个刷新请求发出。

​6. 核心特性​

特性 说明
​无感体验​ 用户无需感知 Token 过期,操作流程不被中断。
​安全性​ Refresh Token 通过 HttpOnly Cookie 存储,防止 XSS 攻击。
​并发控制​ 请求队列机制避免重复刷新,提升性能。
​可扩展性​ 支持多端(Web/App)统一认证逻辑。

​7. 环境准备与部署​

​7.1 生产环境建议​

  • ​HTTPS 强制使用​​:防止 Token 在传输中被窃取。
  • ​Refresh Token 轮换​​:每次刷新后生成新的 Refresh Token,旧 Token 立即失效。

​8. 运行结果​

​8.1 测试用例1:Token 过期自动刷新​

  • ​操作​​:登录后等待 Access Token 过期,发起 API 请求。
  • ​验证点​​:请求自动完成刷新并返回正确数据,无页面跳转。

​8.2 测试用例2:并发请求处理​

  • ​操作​​:在 Token 过期瞬间发起多个请求。
  • ​验证点​​:仅触发一次刷新,所有请求均成功返回。

​9. 测试步骤与详细代码​

​9.1 自动化测试脚本(Postman)​

  1. 登录获取 Access Token 和 Refresh Token。
  2. 使用过期 Access Token 发起请求,验证是否返回 401。
  3. 检查是否自动发起 Refresh Token 请求并重新获取 Access Token。

​10. 部署场景​

​10.1 微服务架构​

  • ​挑战​​:多个服务共享 Token 刷新逻辑。
  • ​解决方案​​:通过 API Gateway 统一拦截请求,集中处理刷新逻辑。

​10.2 Serverless 应用​

  • ​挑战​​:无状态环境下 Token 存储。
  • ​解决方案​​:结合云服务商的密钥管理服务(如 AWS Secrets Manager)。

​11. 疑难解答​

​常见问题1:刷新 Token 后旧 Access Token 仍有效​

  • ​原因​​:Token 未及时失效。
  • ​解决​​:在刷新接口中记录已刷新的 Token,拒绝旧 Token 的请求。

​常见问题2:Refresh Token 泄露​

  • ​解决​​:设置较短的 Refresh Token 有效期(如 7 天),并支持强制失效。

​12. 未来展望与技术趋势​

​12.1 技术趋势​

  • ​无密码认证​​:结合生物识别(如指纹)替代传统 Token。
  • ​OAuth 2.1​​:更严格的 Token 安全规范,支持 PKCE 增强安全性。

​12.2 挑战​

  • ​多设备同步​​:用户在不同设备登录时,如何协调 Token 刷新。
  • ​量子计算威胁​​:未来量子计算机可能破解现有加密算法,需提前布局抗量子加密。

​13. 总结​

无感 Token 刷新通过双 Token 机制和智能的客户端拦截逻辑,显著提升了用户体验和系统安全性。开发者需重点关注 Refresh Token 的存储与保护,并通过请求队列和并发控制优化性能。随着零信任架构和隐私计算的普及,Token 管理将向更动态、更安全的方向演进。掌握这一技术,是构建现代化 Web 应用的必备技能。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。