SpringBoot+React中雙token實(shí)現(xiàn)無感刷新
一、方案說明
1. 核心流程
- ?用戶登錄?
- 提交賬號(hào)密碼 → 服務(wù)端驗(yàn)證 → 返回Access Token(前端存儲(chǔ)) + Refresh Token(HttpOnly Cookie)
- ?業(yè)務(wù)請求?
- 請求頭攜帶Access Token → 服務(wù)端驗(yàn)證有效性 → 有效則返回?cái)?shù)據(jù)
- ?Token過期處理?
- 若Access Token過期 → 前端攔截401錯(cuò)誤 → 自動(dòng)用Refresh Token請求新Token → 刷新后重試原請求
- ?Refresh Token失效?
- 清除登錄態(tài) → 跳轉(zhuǎn)登錄頁
2. 安全設(shè)計(jì)
- ?Access Token?
- 存儲(chǔ):前端內(nèi)存(如Vuex/Redux)或
sessionStorage - 有效期:2小時(shí)
- 傳輸:
Authorization: Bearer <token>
- 存儲(chǔ):前端內(nèi)存(如Vuex/Redux)或
- ?Refresh Token?
- 存儲(chǔ):
HttpOnly + Secure + SameSite=StrictCookie - 有效期:7天
- 刷新機(jī)制:單次使用后更新,舊Token立即失效
- 存儲(chǔ):
二、前端實(shí)現(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錯(cuò)誤且未重試過
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
// 發(fā)起刷新Token請求
const { accessToken } = await refreshToken();
// 存儲(chǔ)新Token
sessionStorage.setItem('access_token', accessToken);
// 重試原請求
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
return http(originalRequest);
} catch (refreshError) {
// 刷新失?。呵宄齌oken,跳轉(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 } // 自動(dòng)攜帶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 });
// 存儲(chǔ)Access Token
sessionStorage.setItem('access_token', res.data.accessToken);
// 跳轉(zhuǎn)主頁
window.location.href = '/';
} catch (err) {
alert('登錄失敗');
}
};
return (
<form onSubmit={handleSubmit}>
{/* 登錄表單 */}
</form>
);
};三、后端實(shí)現(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();
}
// 驗(yàn)證Token
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
throw new JwtException("Token驗(yàn)證失敗");
}
}
// 從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());
// 密碼驗(yàn)證
if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
throw new BadCredentialsException("密碼錯(cuò)誤");
}
// 生成Token
String accessToken = jwtUtil.generateAccessToken(user);
String refreshToken = jwtUtil.generateRefreshToken(user);
// 存儲(chǔ)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) {
// 驗(yàn)證Refresh Token
if (!jwtUtil.validateToken(refreshToken)) {
throw new JwtException("無效Token");
}
String username = jwtUtil.getUsernameFromToken(refreshToken);
// 檢查是否與存儲(chǔ)的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);
// 更新存儲(chǔ)的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小時(shí)(毫秒)
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) );
此方案完整實(shí)現(xiàn)了雙Token無感刷新機(jī)制,具備以下特點(diǎn):
- 完整的前后端代碼示例,可直接集成到項(xiàng)目中
- 遵循安全最佳實(shí)踐(HttpOnly Cookie、短期Token)
- 支持并發(fā)請求處理和Token主動(dòng)吊銷
- 清晰的模塊劃分,易于擴(kuò)展維護(hù)
到此這篇關(guān)于SpringBoot+React中雙token實(shí)現(xiàn)無感刷新的文章就介紹到這了,更多相關(guān)雙token無感刷新內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- 前端登錄token失效實(shí)現(xiàn)雙Token無感刷新詳細(xì)步驟
- SpringBoot中雙token實(shí)現(xiàn)無感刷新
- 雙Token實(shí)現(xiàn)無感刷新的完整代碼示例
- 雙token無感刷新nodejs+React詳細(xì)解釋(保姆級(jí)教程)
- node.js實(shí)現(xiàn)雙Token+Cookie存儲(chǔ)+無感刷新機(jī)制的示例
- 雙Token無感刷新機(jī)制實(shí)現(xiàn)方式
- 前端雙token無感刷新圖文詳解
- vue中雙token和無感刷新token的區(qū)別
- Vue實(shí)現(xiàn)雙token無感刷新的示例代碼
- Vue3+Vite使用雙token實(shí)現(xiàn)無感刷新
相關(guān)文章
JAVA 靜態(tài)代理模式詳解及實(shí)例應(yīng)用
這篇文章主要介紹了JAVA 靜態(tài)代理模式詳解及實(shí)例應(yīng)用的相關(guān)資料,這里舉例說明java 靜態(tài)代理模式該如何使用,幫助大家學(xué)習(xí)參考,需要的朋友可以參考下2016-11-11
Java按周對(duì)事件進(jìn)行分組的實(shí)現(xiàn)示例
本兒主要介紹了Java按周對(duì)事件進(jìn)行分組,java.time包提供了處理日期和時(shí)間的功能,包括獲取某個(gè)日期屬于一年中的第幾周,具有一定的參考價(jià)值,感興趣的可以了解一下2025-08-08
簡單了解spring bean作用域?qū)傩詓ingleton和prototype的區(qū)別
這篇文章主要介紹了簡單了解spring bean作用域?qū)傩詓ingleton和prototype的區(qū)別,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-12-12
SpringBoot 2.6.x整合springfox 3.0報(bào)錯(cuò)問題及解決方案
這篇文章主要介紹了SpringBoot 2.6.x整合springfox 3.0報(bào)錯(cuò)問題及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01
Java設(shè)計(jì)模式之單例模式實(shí)例詳解【懶漢式與餓漢式】
這篇文章主要介紹了Java設(shè)計(jì)模式之單例模式,簡單說明了單例模式的原理并結(jié)合具體實(shí)例形式分析了單例模式中懶漢式與餓漢式的具體實(shí)現(xiàn)與使用技巧,需要的朋友可以參考下2017-09-09
java后臺(tái)如何接收get請求傳過來的數(shù)組
這篇文章主要介紹了java后臺(tái)如何接收get請求傳過來的數(shù)組問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-11-11
springboot+nacos+gateway實(shí)現(xiàn)灰度發(fā)布的實(shí)例詳解
灰度發(fā)布是一種在軟件部署過程中用于平滑過渡的技術(shù),通過引入灰度發(fā)布SDK和配置網(wǎng)關(guān)策略實(shí)現(xiàn),本文就來介紹一下,感興趣的可以了解一下2022-03-03
Java 中的 DataInputStream 介紹_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
DataInputStream 是數(shù)據(jù)輸入流。它繼承于FilterInputStream。接下來通過本文給大家介紹Java 中的 DataInputStream的相關(guān)知識(shí),需要的朋友參考下吧2017-05-05

