SpringBoot+React中雙token實現(xiàn)無感刷新
一、方案說明
1. 核心流程
- ?用戶登錄?
- 提交賬號密碼 → 服務(wù)端驗證 → 返回Access Token(前端存儲) + Refresh Token(HttpOnly Cookie)
- ?業(yè)務(wù)請求?
- 請求頭攜帶Access Token → 服務(wù)端驗證有效性 → 有效則返回數(shù)據(jù)
- ?Token過期處理?
- 若Access Token過期 → 前端攔截401錯誤 → 自動用Refresh Token請求新Token → 刷新后重試原請求
- ?Refresh Token失效?
- 清除登錄態(tài) → 跳轉(zhuǎn)登錄頁
2. 安全設(shè)計
- ?Access Token?
- 存儲:前端內(nèi)存(如Vuex/Redux)或
sessionStorage - 有效期:2小時
- 傳輸:
Authorization: Bearer <token>
- 存儲:前端內(nèi)存(如Vuex/Redux)或
- ?Refresh Token?
- 存儲:
HttpOnly + Secure + SameSite=StrictCookie - 有效期:7天
- 刷新機(jī)制:單次使用后更新,舊Token立即失效
- 存儲:
二、前端實現(xiàn)(React示例)
1. Axios封裝(src/utils/http.js)
import axios from 'axios';
const http = axios.create({
baseURL: process.env.REACT_APP_API_URL,
});
// 請求攔截器:注入Access Token
http.interceptors.request.use(config => {
const accessToken = sessionStorage.getItem('access_token');
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config;
});
// 響應(yīng)攔截器:處理Token過期
http.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;
// 檢測401錯誤且未重試過
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
// 發(fā)起刷新Token請求
const { accessToken } = await refreshToken();
// 存儲新Token
sessionStorage.setItem('access_token', accessToken);
// 重試原請求
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
return http(originalRequest);
} catch (refreshError) {
// 刷新失敗:清除Token,跳轉(zhuǎn)登錄
sessionStorage.removeItem('access_token');
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
// 刷新Token函數(shù)
async function refreshToken() {
const res = await axios.post(
`${process.env.REACT_APP_API_URL}/auth/refresh`,
{},
{ withCredentials: true } // 自動攜帶Cookie
);
return res.data;
}
export default http;2. 登錄邏輯(src/pages/Login.js)
const LoginPage = () => {
const handleSubmit = async (e) => {
e.preventDefault();
try {
const res = await axios.post('/auth/login', {
username: 'user',
password: 'pass'
}, { withCredentials: true });
// 存儲Access Token
sessionStorage.setItem('access_token', res.data.accessToken);
// 跳轉(zhuǎn)主頁
window.location.href = '/';
} catch (err) {
alert('登錄失敗');
}
};
return (
<form onSubmit={handleSubmit}>
{/* 登錄表單 */}
</form>
);
};三、后端實現(xiàn)(Spring Boot)
1. JWT工具類(JwtUtil.java)
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.access.expiration}")
private Long accessExpiration;
@Value("${jwt.refresh.expiration}")
private Long refreshExpiration;
// 生成Access Token
public String generateAccessToken(UserDetails user) {
return buildToken(user, accessExpiration);
}
// 生成Refresh Token
public String generateRefreshToken(UserDetails user) {
return buildToken(user, refreshExpiration);
}
private String buildToken(UserDetails user, Long expiration) {
return Jwts.builder()
.setSubject(user.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(SignatureAlgorithm.HS256, secret)
.compact();
}
// 驗證Token
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
throw new JwtException("Token驗證失敗");
}
}
// 從Token中提取用戶名
public String getUsernameFromToken(String token) {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody()
.getSubject();
}
}2. 認(rèn)證接口(AuthController.java)
@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private JwtUtil jwtUtil;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private RefreshTokenService refreshTokenService;
// 登錄接口
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
UserDetails user = userDetailsService.loadUserByUsername(request.getUsername());
// 密碼驗證
if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
throw new BadCredentialsException("密碼錯誤");
}
// 生成Token
String accessToken = jwtUtil.generateAccessToken(user);
String refreshToken = jwtUtil.generateRefreshToken(user);
// 存儲Refresh Token
refreshTokenService.saveRefreshToken(user.getUsername(), refreshToken);
// 設(shè)置Refresh Token到Cookie
ResponseCookie cookie = ResponseCookie.from("refreshToken", refreshToken)
.httpOnly(true)
.secure(true)
.sameSite("Strict")
.maxAge(jwtUtil.getRefreshExpiration() / 1000)
.path("/auth/refresh")
.build();
return ResponseEntity.ok()
.header(HttpHeaders.SET_COOKIE, cookie.toString())
.body(new AuthResponse(accessToken));
}
// 刷新Token接口
@PostMapping("/refresh")
public ResponseEntity<?> refreshToken(@CookieValue("refreshToken") String refreshToken) {
// 驗證Refresh Token
if (!jwtUtil.validateToken(refreshToken)) {
throw new JwtException("無效Token");
}
String username = jwtUtil.getUsernameFromToken(refreshToken);
// 檢查是否與存儲的Token一致
if (!refreshTokenService.validateRefreshToken(username, refreshToken)) {
throw new JwtException("Token已失效");
}
// 生成新Token
UserDetails user = userDetailsService.loadUserByUsername(username);
String newAccessToken = jwtUtil.generateAccessToken(user);
String newRefreshToken = jwtUtil.generateRefreshToken(user);
// 更新存儲的Refresh Token
refreshTokenService.updateRefreshToken(username, newRefreshToken);
// 返回新Token
ResponseCookie cookie = ResponseCookie.from("refreshToken", newRefreshToken)
.httpOnly(true)
.secure(true)
.sameSite("Strict")
.maxAge(jwtUtil.getRefreshExpiration() / 1000)
.path("/auth/refresh")
.build();
return ResponseEntity.ok()
.header(HttpHeaders.SET_COOKIE, cookie.toString())
.body(new AuthResponse(newAccessToken));
}
}3. Refresh Token服務(wù)(RefreshTokenService.java)
@Service
public class RefreshTokenService {
@Autowired
private RefreshTokenRepository repository;
public void saveRefreshToken(String username, String token) {
RefreshToken refreshToken = new RefreshToken();
refreshToken.setUsername(username);
refreshToken.setToken(token);
refreshToken.setExpiryDate(jwtUtil.getExpirationDateFromToken(token));
repository.save(refreshToken);
}
public boolean validateRefreshToken(String username, String token) {
return repository.findByUsernameAndToken(username, token)
.map(t -> t.getExpiryDate().after(new Date()))
.orElse(false);
}
public void updateRefreshToken(String username, String newToken) {
repository.deleteByUsername(username);
saveRefreshToken(username, newToken);
}
}四、安全配置(SecurityConfig.java)
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationFilter jwtFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/auth/?**?").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
}
}
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
String header = request.getHeader("Authorization");
if (header != null && header.startsWith("Bearer ")) {
String token = header.substring(7);
if (jwtUtil.validateToken(token)) {
String username = jwtUtil.getUsernameFromToken(token);
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(username, null, new ArrayList<>());
SecurityContextHolder.getContext().setAuthentication(auth);
}
}
chain.doFilter(request, response);
}
}五、配置參數(shù)(application.yml)
jwt:
secret: "your-256-bit-secret-key-here" # 通過環(huán)境變量注入
access:
expiration: 7200000 # 2小時(毫秒)
refresh:
expiration: 604800000 # 7天(毫秒)六、數(shù)據(jù)庫表結(jié)構(gòu)(MySQL)
CREATE TABLE refresh_tokens ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(255) NOT NULL, token VARCHAR(512) NOT NULL, expiry_date DATETIME NOT NULL, UNIQUE KEY (username) );
此方案完整實現(xiàn)了雙Token無感刷新機(jī)制,具備以下特點:
- 完整的前后端代碼示例,可直接集成到項目中
- 遵循安全最佳實踐(HttpOnly Cookie、短期Token)
- 支持并發(fā)請求處理和Token主動吊銷
- 清晰的模塊劃分,易于擴(kuò)展維護(hù)
到此這篇關(guān)于SpringBoot+React中雙token實現(xiàn)無感刷新的文章就介紹到這了,更多相關(guān)雙token無感刷新內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java實現(xiàn)FTP文件上傳下載功能的詳細(xì)指南
本文將詳細(xì)解釋如何用Java實現(xiàn)FTP協(xié)議下的文件上傳和下載功能,涵蓋連接設(shè)置、文件操作以及異常處理等方面,介紹了 java.net 和 org.apache.commons.net.ftp 庫,以及如何使用這些庫提供的工具和方法進(jìn)行文件傳輸,需要的朋友可以參考下2025-07-07
java網(wǎng)絡(luò)編程之識別示例 獲取主機(jī)網(wǎng)絡(luò)接口列表
一個客戶端想要發(fā)起一次通信,先決條件就是需要知道運行著服務(wù)器端程序的主機(jī)的IP地址是多少。然后我們才能夠通過這個地址向服務(wù)器發(fā)送信息。2014-01-01
SpringBoot中的6種API請求參數(shù)讀取方式總結(jié)
使用Spring Boot開發(fā)API的時候,讀取請求參數(shù)是服務(wù)端編碼中最基本的一項操作,Spring Boot中也提供了多種機(jī)制來滿足不同的API設(shè)計要求,通過本文,為大家總結(jié)6種常用的請求參數(shù)讀取方式,需要的朋友可以參考下2024-07-07
Java微服務(wù)架構(gòu)中的關(guān)鍵技術(shù)和設(shè)計原則解讀
Java是一種面向?qū)ο蟮母呒壘幊陶Z言,具有跨平臺兼容性、自動內(nèi)存管理等特點,它支持多線程、異常處理,并擁有豐富的標(biāo)準(zhǔn)庫和強(qiáng)大的社區(qū)生態(tài),微服務(wù)架構(gòu)是將應(yīng)用分解為多個小型服務(wù)的設(shè)計風(fēng)格2024-11-11
springboot~nexus項目打包要注意的地方示例代碼詳解
這篇文章主要介紹了springboot~nexus項目打包要注意的地方,本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-07-07

