前端与 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),并通过以下机制来完成无感刷新:
- 访问 Token:短期有效,用于用户身份验证。
- 刷新 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 刷新功能。
通过这些技术,开发者能够创建一个无缝、流畅的身份认证系统,提供良好的用户体验,同时保障系统的安全性。
发表回复