前端与 Spring Boot 后端无感 Token 刷新 – 从原理到全栈实践

在现代 Web 开发中,随着前后端分离架构的普及,Token 认证已成为常见的身份验证方式。为了确保用户在一段时间内不会频繁地登录,通常使用 JWT (JSON Web Token) 来维护用户身份。然而,JWT 有一个问题:过期时间。当 Token 过期时,用户需要重新登录。为了提升用户体验,我们通常实现 无感刷新机制,即当 Token 快要过期时,系统自动刷新 Token,而不会打扰到用户。本文将从原理、实现及全栈开发的角度,全面介绍如何实现 前端与 Spring Boot 后端的无感 Token 刷新

一、无感 Token 刷新原理

JWT(JSON Web Token) 是一种轻量级的认证标准,它包含三个部分:

  • 头部(Header):描述 Token 的类型和签名算法。
  • 有效载荷(Payload):包含了用户的身份信息和一些自定义的数据(如 Token 的过期时间)。
  • 签名(Signature):确保 Token 内容没有被篡改。

JWT 的有效期通常较短,这样可以避免长时间使用同一个 Token 而存在的安全隐患。当 Token 过期时,用户需要重新认证。

无感刷新 是指在用户的 Token 即将过期时,系统会通过刷新 Token(即生成一个新的 JWT)来延长会话,而无需用户重新登录。为了实现这一功能,我们通常引入一个 刷新 Token(Refresh Token),并通过以下机制来完成无感刷新:

  1. 访问 Token:短期有效,用于用户身份验证。
  2. 刷新 Token:长期有效,用于刷新访问 Token,一般在访问 Token 过期后才会用到。

二、前端与 Spring Boot 后端的实现步骤

1. 前端部分:管理 Token 和刷新机制

前端的关键任务是管理 访问 Token 和 刷新 Token,并在需要时发起 Token 刷新请求。

(1) 存储 Token

通常,我们将 JWT 存储在 localStorage 或 sessionStorage 中,而刷新 Token 可以存储在 HttpOnly Cookie 中,以提高安全性。这样,刷新 Token 就不会暴露给 JavaScript。

// 存储 Token
localStorage.setItem('accessToken', accessToken);
document.cookie = `refreshToken=${refreshToken}; HttpOnly; Path=/;`;
(2) Token 自动刷新

每当用户的请求返回 401(未授权)时,说明 Token 已过期,前端需要向后端发送请求,使用 刷新 Token 来获取新的 访问 Token

// 封装请求方法
const requestWithAuth = async (url, options = {}) => {
  let accessToken = localStorage.getItem('accessToken');

  // 添加 Token 到请求头
  options.headers = {
    ...options.headers,
    Authorization: `Bearer ${accessToken}`,
  };

  try {
    const response = await fetch(url, options);
    if (response.status === 401) {
      // 如果 Token 过期,尝试刷新 Token
      const refreshToken = getCookie('refreshToken');
      const refreshResponse = await fetch('/api/refresh', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ refreshToken }),
      });

      if (refreshResponse.ok) {
        const data = await refreshResponse.json();
        // 更新 Token 并重新发起原请求
        localStorage.setItem('accessToken', data.accessToken);
        options.headers.Authorization = `Bearer ${data.accessToken}`;
        return fetch(url, options);
      } else {
        // 如果刷新 Token 失败,则跳转到登录页
        window.location.href = '/login';
      }
    }
    return response;
  } catch (error) {
    console.error('Error with request:', error);
  }
};
(3) 自动调用 Token 刷新接口

可以在全局的请求拦截器中处理 Token 刷新逻辑,例如在 Axios 的请求拦截器中:

axios.interceptors.response.use(
  response => response,
  async error => {
    if (error.response.status === 401) {
      const refreshToken = getCookie('refreshToken');
      const refreshResponse = await axios.post('/api/refresh', { refreshToken });

      if (refreshResponse.status === 200) {
        const { accessToken } = refreshResponse.data;
        localStorage.setItem('accessToken', accessToken);

        // 重试原始请求
        error.config.headers['Authorization'] = `Bearer ${accessToken}`;
        return axios(error.config);
      }
    }
    return Promise.reject(error);
  }
);

2. 后端部分:Spring Boot 实现 Token 刷新

后端的主要任务是提供一个接口,用于验证 刷新 Token,并生成新的 访问 Token

(1) 安全配置

首先,在 Spring Boot 后端中使用 Spring Security 配合 JWT 来进行身份验证。

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/login", "/refresh").permitAll()
                .anyRequest().authenticated()
            .and()
            .addFilter(new JwtAuthenticationFilter(authenticationManager()))
            .addFilter(new JwtAuthorizationFilter(authenticationManager()));
    }
}
(2) 生成和验证 JWT Token

创建一个 JwtTokenProvider 类,用于生成和解析 JWT Token。

@Component
public class JwtTokenProvider {

    private String secretKey = "secret";
    private long accessTokenValidity = 3600000; // 1小时
    private long refreshTokenValidity = 86400000; // 1天

    public String generateAccessToken(String username) {
        return Jwts.builder()
            .setSubject(username)
            .setExpiration(new Date(System.currentTimeMillis() + accessTokenValidity))
            .signWith(SignatureAlgorithm.HS512, secretKey)
            .compact();
    }

    public String generateRefreshToken(String username) {
        return Jwts.builder()
            .setSubject(username)
            .setExpiration(new Date(System.currentTimeMillis() + refreshTokenValidity))
            .signWith(SignatureAlgorithm.HS512, secretKey)
            .compact();
    }

    public Claims getClaims(String token) {
        return Jwts.parser()
            .setSigningKey(secretKey)
            .parseClaimsJws(token)
            .getBody();
    }

    public String getUsername(String token) {
        return getClaims(token).getSubject();
    }

    public boolean isTokenExpired(String token) {
        return getClaims(token).getExpiration().before(new Date());
    }

    public boolean validateToken(String token) {
        return !isTokenExpired(token);
    }
}
(3) 刷新 Token 接口

后端需要提供一个刷新 Token 的接口,该接口会校验传递过来的 刷新 Token 是否有效。如果有效,则返回新的 访问 Token

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

    @Autowired
    private JwtTokenProvider jwtTokenProvider;

    @PostMapping("/refresh")
    public ResponseEntity<?> refreshToken(@RequestBody Map<String, String> request) {
        String refreshToken = request.get("refreshToken");
        if (jwtTokenProvider.validateToken(refreshToken)) {
            String username = jwtTokenProvider.getUsername(refreshToken);
            String newAccessToken = jwtTokenProvider.generateAccessToken(username);
            return ResponseEntity.ok(new HashMap<String, String>() {{
                put("accessToken", newAccessToken);
            }});
        } else {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid refresh token");
        }
    }
}
(4) 配置刷新 Token 安全性

为了安全地存储和使用刷新 Token,通常将它存储在 HttpOnly Cookie 中,以防止它被 JavaScript 获取。

Cookie refreshTokenCookie = new Cookie("refreshToken", refreshToken);
refreshTokenCookie.setHttpOnly(true);
refreshTokenCookie.setPath("/");
response.addCookie(refreshTokenCookie);

三、总结

实现前端与 Spring Boot 后端的无感 Token 刷新不仅提高了用户体验,还确保了应用的安全性。通过结合 JWT刷新 Token 和 HttpOnly Cookie,可以在不打扰用户的情况下自动刷新 Token。当访问 Token 过期时,前端会自动使用 刷新 Token 获取新的 访问 Token,从而避免用户频繁登录。

  • 前端部分:通过拦截器、请求管理和 Cookie 存储来实现 Token 管理和无感刷新。
  • 后端部分:通过 Spring Security、JWT Token Provider 和刷新 Token 接口实现 Token 刷新功能。

通过这些技术,开发者能够创建一个无缝、流畅的身份认证系统,提供良好的用户体验,同时保障系统的安全性。