token无感刷新全流程

demo代码:Arata08/tokenDemo

Java (Spring Boot) 和 Vue (配合 Axios) 前端的无感刷新全流程:

方案:

  1. 双 Token 机制 : 服务器同时签发两个 Token:
    • access_token: 短时效,用于日常接口访问鉴权。
    • refresh_token: 长时效,非常安全,仅用于获取新的 access_token
  2. 前端拦截器 : Vue 前端使用 Axios 拦截器,在每个请求头中自动添加 access_token
  3. 后端校验 : Java 后端拦截器或过滤器校验 access_token 的有效性。
  4. 前端感知与刷新 :
    • 当后端返回特定状态码(如 401 Unauthorized)表示 access_token 过期时。
    • 前端不直接跳转登录页,而是发起一个专用的刷新 Token 接口 请求,并携带 refresh_token
    • 后端验证 refresh_token,如果有效,则生成新的 access_token(有时也更新 refresh_token)并返回给前端。
    • 前端收到新的 access_token 后,更新本地存储,并重试之前失败的请求。

流程总结

  1. 用户登录成功,后端返回 access_tokenrefresh_token,前端存储。
  2. 前端每次 API 请求时,通过 Axios 请求拦截器自动在 Authorization 头中添加 Bearer access_token
  3. 后端通过过滤器验证 access_token
  4. access_token 过期,后端返回 401。
  5. 前端 Axios 响应拦截器捕获 401 错误。
  6. 拦截器发起 /auth/refresh 请求,携带 refresh_token
  7. 后端验证 refresh_token,成功则返回新的 access_token
  8. 前端更新本地 access_token
  9. 拦截器自动重试之前失败的请求(使用新 access_token)。
  10. 如果 refresh_token 也失败或不存在,则清除本地 Token,跳转登录页。

后端 (Java - Spring Boot) 实现

登录接口 : 登录成功后返回 access_tokenrefresh_token

java 复制代码
@RestController
@RequestMapping("/api/auth")
public class AuthController {

    @Autowired
    private JwtUtil jwtUtil;

    // 模拟用户验证逻辑
    private boolean validateUser(String username, String password) {
        // 实际应用中应查询数据库
        return "admin".equals(username) && "password".equals(password);
    }

    @PostMapping("/login")
    public ResponseEntity<Map<String, String>> login(@RequestBody Map<String, String> loginRequest) {
        String username = loginRequest.get("username");
        String password = loginRequest.get("password");

        if (validateUser(username, password)) {
            String accessToken = jwtUtil.generateAccessToken(username);
            String refreshToken = jwtUtil.generateRefreshToken(username);

            Map<String, String> response = new HashMap<>();
            response.put("access_token", accessToken);
            response.put("refresh_token", refreshToken);
            response.put("token_type", "Bearer");

            return ResponseEntity.ok(response);
        } else {
            return ResponseEntity.status(401).body(null); // 或返回错误信息
        }
    }
}

刷新 Token 接口 : 接收 refresh_token,验证后返回新的 access_token

java 复制代码
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUtil jwtUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        String authHeader = request.getHeader("Authorization");

        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            String token = authHeader.substring(7); // 移除 "Bearer " 前缀
            try {
                Claims claims = jwtUtil.parseAccessToken(token);
                // 可以将用户信息存入 SecurityContext 或 Request Attributes
                String username = claims.getSubject();
                // SecurityContextHolder.getContext().setAuthentication(...);
                request.setAttribute("currentUser", username);
            } catch (RuntimeException e) {
                // Token 无效,返回 401
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                response.getWriter().write("{\"error\":\"Unauthorized\"}");
                return; // 不继续执行后续过滤器/控制器
            }
        }
        // 如果没有 Token 或 Token 有效,继续执行
        filterChain.doFilter(request, response);
    }
}

Token 工具类: 用于生成、解析、验证 JWT

java 复制代码
@Component
public class JwtUtil {

    // 注意:生产中应从配置文件读取,并且密钥要足够复杂且保密
    private final String ACCESS_TOKEN_SECRET = "your_very_strong_access_token_secret_here";
    private final String REFRESH_TOKEN_SECRET = "your_very_strong_refresh_token_secret_here";
    private final long ACCESS_TOKEN_EXPIRATION = 30 * 60 * 1000; // 30分钟
    private final long REFRESH_TOKEN_EXPIRATION = 7 * 24 * 60 * 60 * 1000; // 7天

    private SecretKey getAccessTokenKey() {
        return Keys.hmacShaKeyFor(ACCESS_TOKEN_SECRET.getBytes());
    }

    private SecretKey getRefreshTokenKey() {
        return Keys.hmacShaKeyFor(REFRESH_TOKEN_SECRET.getBytes());
    }

    // 生成 Access Token
    public String generateAccessToken(String username) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + ACCESS_TOKEN_EXPIRATION);

        return Jwts.builder()
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(expiryDate)
                .signWith(getAccessTokenKey())
                .compact();
    }

    // 生成 Refresh Token
    public String generateRefreshToken(String username) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + REFRESH_TOKEN_EXPIRATION);

        return Jwts.builder()
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(expiryDate)
                .signWith(getRefreshTokenKey())
                .compact();
    }

    // 解析 Access Token
    public Claims parseAccessToken(String token) {
        try {
            return Jwts.parserBuilder()
                    .setSigningKey(getAccessTokenKey())
                    .build()
                    .parseClaimsJws(token)
                    .getBody();
        } catch (JwtException | IllegalArgumentException e) {
            // Token 无效或过期
            throw new RuntimeException("Invalid or expired access token", e);
        }
    }

    // 解析 Refresh Token
    public Claims parseRefreshToken(String token) {
        try {
            return Jwts.parserBuilder()
                    .setSigningKey(getRefreshTokenKey())
                    .build()
                    .parseClaimsJws(token)
                    .getBody();
        } catch (JwtException | IllegalArgumentException e) {
            // Token 无效或过期
            throw new RuntimeException("Invalid or expired refresh token", e);
        }
    }

    // 验证 Access Token 是否过期
    public boolean isAccessTokenExpired(String token) {
        try {
            Claims claims = parseAccessToken(token);
            Date expiration = claims.getExpiration();
            return expiration.before(new Date());
        } catch (JwtException e) {
            return true; // 解析失败也视为过期
        }
    }
}

JWT 拦截器/过滤器 : 在访问需要认证的接口前,验证 access_token

java 复制代码
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUtil jwtUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        String authHeader = request.getHeader("Authorization");

        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            String token = authHeader.substring(7); // 移除 "Bearer " 前缀
            try {
                Claims claims = jwtUtil.parseAccessToken(token);
                // 可以将用户信息存入 SecurityContext 或 Request Attributes
                String username = claims.getSubject();
                // SecurityContextHolder.getContext().setAuthentication(...);
                request.setAttribute("currentUser", username);
            } catch (RuntimeException e) {
                // Token 无效,返回 401
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                response.getWriter().write("{\"error\":\"Unauthorized\"}");
                return; // 不继续执行后续过滤器/控制器
            }
        }
        // 如果没有 Token 或 Token 有效,继续执行
        filterChain.doFilter(request, response);
    }
}

前端 (Vue - Axios) 实现

Axios 配置: 创建 Axios 实例,并配置请求和响应拦截器。

javascript 复制代码
import axios from 'axios';

const API_BASE_URL = 'http://localhost:8080/api'; // 替换为你的后端地址

const apiClient = axios.create({
    baseURL: API_BASE_URL,
    timeout: 10000,
});

// 请求拦截器:在每个请求头中添加 access_token
apiClient.interceptors.request.use(
    (config) => {
        const accessToken = localStorage.getItem('access_token'); // 或 sessionStorage
        if (accessToken) {
            config.headers.Authorization = `Bearer ${accessToken}`;
        }
        return config;
    },
    (error) => {
        return Promise.reject(error);
    }
);

let isRefreshing = false;
let failedQueue = [];

const processQueue = (error, token = null) => {
    failedQueue.forEach(prom => {
        if (error) {
            prom.reject(error);
        } else {
            prom.resolve(token);
        }
    });

    failedQueue = [];
};

// 响应拦截器:处理 401 错误并尝试刷新 token
apiClient.interceptors.response.use(
    (response) => {
        // 对响应数据做点什么
        return response;
    },
    async (error) => {
        const originalRequest = error.config;

        if (error.response?.status === 401 && !originalRequest._retry) {
            if (isRefreshing) {
                // 如果正在刷新,则将请求加入队列等待
                return new Promise((resolve, reject) => {
                    failedQueue.push({ resolve, reject });
                }).then(token => {
                    originalRequest.headers['Authorization'] = 'Bearer ' + token;
                    return apiClient(originalRequest);
                }).catch(err => {
                    return Promise.reject(err);
                });
            }

            originalRequest._retry = true; // 标记为已重试,防止无限循环
            isRefreshing = true;

            const refreshToken = localStorage.getItem('refresh_token'); // 或 sessionStorage

            if (!refreshToken) {
                // 如果没有 refresh_token,直接跳转登录
                // router.push('/login');
                // 或者触发登出逻辑
                localStorage.removeItem('access_token');
                localStorage.removeItem('refresh_token');
                window.location.href = '/login'; // 简单示例
                return Promise.reject(error);
            }

            try {
                const refreshResponse = await apiClient.post('/auth/refresh', {}, {
                    headers: {
                        Authorization: `Bearer ${refreshToken}`,
                    }
                });

                const newAccessToken = refreshResponse.data.access_token;
                // const newRefreshToken = refreshResponse.data.refresh_token; // 如果后端也返回了新的 refresh_token

                // 更新本地存储的 token
                localStorage.setItem('access_token', newAccessToken);
                // localStorage.setItem('refresh_token', newRefreshToken); // 如果更新了

                // 更新正在请求的 axios 实例的 header
                apiClient.defaults.headers.common['Authorization'] = `Bearer ${newAccessToken}`;

                // 重试原始请求
                originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`;
                processQueue(null, newAccessToken);
                return apiClient(originalRequest);

            } catch (refreshError) {
                console.error('Token refresh failed:', refreshError);
                processQueue(refreshError, null);
                // Refresh token 也失败了,登出用户
                localStorage.removeItem('access_token');
                localStorage.removeItem('refresh_token');
                window.location.href = '/login'; // 简单示例
                return Promise.reject(refreshError);
            } finally {
                isRefreshing = false;
            }
        }

        return Promise.reject(error);
    }
);

export default apiClient;
相关推荐
小徐Chao努力6 小时前
【Langchain4j-Java AI开发】02-模型参数配置与调优
java·开发语言·人工智能
啥都不懂的小小白6 小时前
CyclicBarrier深度解析:Java中的“循环栅栏“同步工具
java·juc·cyclicbarrier
毕设源码-朱学姐6 小时前
【开题答辩全过程】以 基于uni—app的民宿预订系统为例,包含答辩的问题和答案
java·eclipse
bjxiaxueliang6 小时前
一文详解Cpp多线程编程:从传统pthread到现代thread实践指南
java·开发语言·jvm
机智的人猿泰山6 小时前
spring boot 运行测试类时:Error creating bean with name ‘serverEndpointExporter‘ 问题
java·spring boot·后端
爬山算法6 小时前
Hibernate(3)Hibernate的优点是什么?
java·后端·hibernate
秋邱6 小时前
Java面向对象进阶实战:用工厂模式+策略模式优化支付系统
java·bash·策略模式
heartbeat..7 小时前
网络通信核心知识全解析:模型、协议与 TCP 机制
java·网络·网络协议·tcp/ip
q_19132846957 小时前
基于SpringBoot+Vue.js的高校竞赛活动信息平台
vue.js·spring boot·后端·mysql·程序员·计算机毕业设计
我是小邵7 小时前
【踩坑实录】一次 H5 页面在 PC 端的滚动条与轮播图修复全过程(Vue + Vant)
前端·javascript·vue.js