在现代 Web 应用中,前端与 Spring Boot 后端 的通信通常依赖于 Token 机制来验证用户身份。Token 刷新(Refresh Token)机制广泛用于避免频繁的用户重新登录,同时保障安全性。

本文目标

本篇文章将带你从原理到实践,逐步介绍 Token 刷新 的工作原理以及如何在前端与 Spring Boot 后端 中实现无感刷新。

1. 理解 Token 刷新机制

在现代 Web 应用中,我们通常会使用 JWT(JSON Web Token) 来进行身份验证。JWT 主要包括两种类型的 Token:

  • Access Token:这是一个短期有效的 Token,用于每次请求时进行身份验证。通常有效期较短,几分钟或几小时。
  • Refresh Token:这是一个长期有效的 Token,用于刷新过期的 Access Token。它的有效期较长,通常可以是几天到几个月。

1.1. 工作原理

  1. 用户登录时,后端生成 Access Token 和 Refresh Token 并返回给前端。
  2. 前端将 Access Token 保存在内存或 LocalStorage 中,用于后续的请求。
  3. 当 Access Token 过期时,前端会通过 Refresh Token 向后端请求新的 Access Token
  4. 后端验证 Refresh Token,如果有效,则返回一个新的 Access Token,并且前端重新保存。
  5. Refresh Token 本身的过期时间较长,只有在 Refresh Token 过期时,用户才需要重新登录。

1.2. 为什么需要 Token 刷新机制

  • 提高用户体验:避免用户频繁登录。
  • 增加安全性:Access Token 有较短的有效期,即使被盗用也能减少风险。
  • 降低前端复杂性:前端不需要保存用户的密码等敏感信息,只需处理 Token。

2. 前端实现:无感刷新

前端的工作就是存储和管理这两个 Token,并在 Access Token 过期时 通过 Refresh Token 自动刷新。前端通常使用 Axios 或其他 HTTP 客户端来管理请求。

2.1. 前端流程

  1. 登录后获取 Token
    • 用户登录后,后端返回 Access Token 和 Refresh Token
    • 将 Access Token 保存在 LocalStorage 或 SessionStorage 中,Refresh Token 只保存在 HTTPOnly Cookie 中,增强安全性。
  2. 请求拦截器
    • 使用前端的 请求拦截器 在每个请求前添加 Authorization Header,用于发送 Access Token
  3. 响应拦截器
    • 当收到 401 Unauthorized 响应时,表示 Access Token 已经过期,此时使用 Refresh Token 刷新 Access Token
  4. 刷新 Token
    • 前端向后端发送 Refresh Token 来获取新的 Access Token
    • 如果 Refresh Token 有效,后端返回新的 Access Token,前端更新存储的 Access Token,并重新发送失败的请求。

2.2. 前端代码示例

假设使用 Axios 进行 HTTP 请求:

import axios from 'axios';

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

api.interceptors.request.use(
  (config) => {
    const accessToken = localStorage.getItem('access_token');
    if (accessToken) {
      config.headers['Authorization'] = `Bearer ${accessToken}`;
    }
    return config;
  },
  (error) => Promise.reject(error)
);

api.interceptors.response.use(
  (response) => response,
  async (error) => {
    if (error.response.status === 401) {
      // Token expired, need to refresh the access token
      try {
        const refreshToken = localStorage.getItem('refresh_token');
        const response = await api.post('/auth/refresh', { refreshToken });
        const newAccessToken = response.data.accessToken;
        localStorage.setItem('access_token', newAccessToken);
        
        // Retry the original request
        error.config.headers['Authorization'] = `Bearer ${newAccessToken}`;
        return api(error.config);
      } catch (refreshError) {
        console.error('Token refresh failed');
        // Handle token refresh failure (redirect to login page)
      }
    }
    return Promise.reject(error);
  }
);

export default api;

3. 后端实现:Spring Boot 实现 Token 刷新

在后端,我们使用 Spring Security 和 JWT 实现 Token 刷新。常见的做法是通过过滤器进行认证,创建一个 Token 刷新端点来处理 Refresh Token 的请求。

3.1. 创建 JWT 工具类

首先,我们创建一个工具类,用于生成和验证 JWT Token。

import io.jsonwebtoken.*;
import java.util.Date;

public class JwtUtils {

    private static final String SECRET_KEY = "your_secret_key";
    private static final long EXPIRATION_TIME = 1000 * 60 * 60;  // 1 hour
    private static final long REFRESH_EXPIRATION_TIME = 1000 * 60 * 60 * 24 * 7;  // 7 days

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

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

    public static Claims extractClaims(String token) {
        return Jwts.parser()
                .setSigningKey(SECRET_KEY)
                .parseClaimsJws(token)
                .getBody();
    }

    public static boolean isTokenExpired(String token) {
        Date expiration = extractClaims(token).getExpiration();
        return expiration.before(new Date());
    }

    public static String extractUsername(String token) {
        return extractClaims(token).getSubject();
    }
}

3.2. 创建 Token 刷新控制器

创建一个控制器用于刷新 Access Token

import org.springframework.web.bind.annotation.*;

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

    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
        // Authenticate user and generate tokens
        String username = loginRequest.getUsername();
        String accessToken = JwtUtils.generateAccessToken(username);
        String refreshToken = JwtUtils.generateRefreshToken(username);
        
        // Return tokens
        return ResponseEntity.ok(new AuthResponse(accessToken, refreshToken));
    }

    @PostMapping("/refresh")
    public ResponseEntity<?> refreshToken(@RequestBody RefreshTokenRequest refreshTokenRequest) {
        String refreshToken = refreshTokenRequest.getRefreshToken();
        if (JwtUtils.isTokenExpired(refreshToken)) {
            return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Refresh token expired");
        }
        
        String username = JwtUtils.extractUsername(refreshToken);
        String newAccessToken = JwtUtils.generateAccessToken(username);
        
        return ResponseEntity.ok(new AuthResponse(newAccessToken, refreshToken));  // Return new access token
    }
}

3.3. 创建 DTO

定义用于存储请求和响应的 DTO 类:

public class LoginRequest {
    private String username;
    private String password;
    
    // getters and setters
}

public class RefreshTokenRequest {
    private String refreshToken;
    
    // getters and setters
}

public class AuthResponse {
    private String accessToken;
    private String refreshToken;
    
    public AuthResponse(String accessToken, String refreshToken) {
        this.accessToken = accessToken;
        this.refreshToken = refreshToken;
    }
    
    // getters and setters
}

4. 安全性考虑

  • 存储 Refresh Token:为了提高安全性,最好将 Refresh Token 存储在 HTTPOnly Cookie 中,这样可以防止 JavaScript 访问,从而避免 XSS 攻击。
  • 保护 API:确保只有经过身份验证的请求才能访问刷新 Token 的 API。
  • Token 过期处理:在 Access Token 和 Refresh Token 过期时,必须确保用户被引导到登录界面,要求重新认证。

5. 总结

通过实现 Token 刷新,我们能够提供无感的用户体验,避免用户频繁登录,同时也提高了安全性。在前端,使用 Axios 拦截器可以处理 Access Token 过期后的刷新逻辑;在后端,使用 Spring Boot 和 JWT 实现 Refresh Token 刷新机制,可以有效地提升系统的安全性和可用性。

通过这种方式,我们可以在实际项目中构建更

为健壮、流畅的身份认证系统,保证用户体验的同时,确保系统的安全性。