SpringBoot中雙token實(shí)現(xiàn)無(wú)感刷新
一、方案說(shuō)明
1. 核心流程
- ?用戶登錄?
- 提交賬號(hào)密碼 → 服務(wù)端驗(yàn)證 → 返回Access Token(前端存儲(chǔ)) + Refresh Token(HttpOnly Cookie)
- ?業(yè)務(wù)請(qǐng)求?
- 請(qǐng)求頭攜帶Access Token → 服務(wù)端驗(yàn)證有效性 → 有效則返回?cái)?shù)據(jù)
- ?Token過(guò)期處理?
- 若Access Token過(guò)期 → 前端攔截401錯(cuò)誤 → 自動(dòng)用Refresh Token請(qǐng)求新Token → 刷新后重試原請(qǐng)求
- ?Refresh Token失效?
- 清除登錄態(tài) → 跳轉(zhuǎn)登錄頁(yè)
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ī)制:?jiǎn)未问褂煤蟾?,舊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,
});
// 請(qǐng)求攔截器:注入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過(guò)期
http.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;
// 檢測(cè)401錯(cuò)誤且未重試過(guò)
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
// 發(fā)起刷新Token請(qǐng)求
const { accessToken } = await refreshToken();
// 存儲(chǔ)新Token
sessionStorage.setItem('access_token', accessToken);
// 重試原請(qǐng)求
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 } // 自動(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)主頁(yè)
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("無(wú)效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" # 通過(guò)環(huán)境變量注入
access:
expiration: 7200000 # 2小時(shí)(毫秒)
refresh:
expiration: 604800000 # 7天(毫秒)六、數(shù)據(jù)庫(kù)表結(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無(wú)感刷新機(jī)制,具備以下特點(diǎn):
- 完整的前后端代碼示例,可直接集成到項(xiàng)目中
- 遵循安全最佳實(shí)踐(HttpOnly Cookie、短期Token)
- 支持并發(fā)請(qǐng)求處理和Token主動(dòng)吊銷
- 清晰的模塊劃分,易于擴(kuò)展維護(hù)
到此這篇關(guān)于SpringBoot中雙token實(shí)現(xiàn)無(wú)感刷新的文章就介紹到這了,更多相關(guān)SpringBoot 雙token無(wú)感刷新內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- 前端登錄token失效實(shí)現(xiàn)雙Token無(wú)感刷新詳細(xì)步驟
- 雙Token實(shí)現(xiàn)無(wú)感刷新的完整代碼示例
- 雙token無(wú)感刷新nodejs+React詳細(xì)解釋(保姆級(jí)教程)
- node.js實(shí)現(xiàn)雙Token+Cookie存儲(chǔ)+無(wú)感刷新機(jī)制的示例
- 雙Token無(wú)感刷新機(jī)制實(shí)現(xiàn)方式
- 前端雙token無(wú)感刷新圖文詳解
- vue中雙token和無(wú)感刷新token的區(qū)別
- Vue實(shí)現(xiàn)雙token無(wú)感刷新的示例代碼
- Vue3+Vite使用雙token實(shí)現(xiàn)無(wú)感刷新
- SpringBoot+React中雙token實(shí)現(xiàn)無(wú)感刷新
相關(guān)文章
springboot解決Class path contains multiple 
這篇文章主要介紹了springboot解決Class path contains multiple SLF4J bindings問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-07-07
SpringBoot數(shù)據(jù)庫(kù)常見錯(cuò)誤DataIntegrityViolationException的原因及解決方案
在SpringBoot+MyBatis/MyBatis-Plus開發(fā)過(guò)程中,數(shù)據(jù)庫(kù)操作是核心部分之一,然而,開發(fā)者經(jīng)常會(huì)遇到 org.springframework.dao.DataIntegrityViolationException異常本文將通過(guò)兩個(gè)典型案例,深入分析DataIntegrityViolationException的常見原因,并提供完整的解決方案2025-07-07
Springmvc中的轉(zhuǎn)發(fā)重定向和攔截器的示例
本篇文章主要介紹了Springmvc中的轉(zhuǎn)發(fā)重定向和攔截器的示例,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-05-05
mybatis collection關(guān)聯(lián)查詢多個(gè)參數(shù)方式
在使用MyBatis進(jìn)行關(guān)聯(lián)查詢時(shí),往往需要根據(jù)多個(gè)參數(shù)進(jìn)行查詢,例如,使用evtId和businessType作為查詢條件,同時(shí)在resultMap中配置id和businessType1作為結(jié)果映射,這種情況下,可以通過(guò)<sql>標(biāo)簽定義參數(shù)模板,或者使用@Param注解指定參數(shù)名稱2024-10-10
ReentrantLock實(shí)現(xiàn)原理詳解
本文將對(duì)ReentrantLock實(shí)現(xiàn)原理進(jìn)行詳細(xì)的介紹,具有很好的參考價(jià)值,下面跟著小編一起來(lái)看下吧2017-02-02

