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;
相关推荐
维天说1 小时前
CLI-Switch 2026年3月版历史设计:Hook、TTY 隔离与 JSON 状态
java·服务器·json
Zane19942 小时前
并发 vs 并行:别再傻傻分不清了,一文讲透 Java 并发编程的第一课
java·后端
sugar__salt2 小时前
Vue3 表单双向绑定与响应式核心 API 技术详解
前端·javascript·vue.js
噢,我明白了3 小时前
java中Excel的导入和导出(EasyExcel)
java·开发语言·excel
郝亚军3 小时前
使用Vue 3和Nginx打包和部署Vue.js项目的一般步骤
前端·vue.js·nginx
吠品3 小时前
Zabbix Web界面误报Server未运行的排查与解决
java·服务器·数据库
啊湘3 小时前
天气查询API接口 按月Token鉴权 实时天气 物联网可用 文档齐全
java·后端·struts
Java内核笔记3 小时前
告别十亿美元的错误 : Spring Boot 4 空安全 (JSpecify) 实战
java·spring boot·后端
糖果店的幽灵3 小时前
langgraph的 MessagesState 解读
java·开发语言·人工智能·windows·langgraph
极光代码工作室4 小时前
基于SpringBoot的在线博客系统
java·springboot·web开发·后端开发