Spring?Boot?JWT登錄授權(quán)使用(無感刷新)
一、引言
在分布式系統(tǒng)和前后端分離架構(gòu)中,傳統(tǒng)的基于 Session 的認(rèn)證方式存在跨域難處理、服務(wù)端存儲(chǔ)壓力大等問題。JWT(JSON Web Token) 作為一種輕量級(jí)的身份認(rèn)證與授權(quán)方案,憑借其無狀態(tài)、可跨域、易于擴(kuò)展的特性,成為 Spring Boot 項(xiàng)目中實(shí)現(xiàn)認(rèn)證授權(quán)的主流選擇。本文將從環(huán)境搭建、核心實(shí)現(xiàn)到進(jìn)階優(yōu)化,完整講解 Spring Boot 整合 JWT 實(shí)現(xiàn)登錄認(rèn)證與接口授權(quán)的全流程。
二、技術(shù)棧與環(huán)境準(zhǔn)備
1. 核心依賴
在 Spring Boot 項(xiàng)目的pom.xml(Maven)或build.gradle(Gradle)中引入以下
- Spring Security:提供認(rèn)證與授權(quán)的基礎(chǔ)框架
- JJWT:Java 領(lǐng)域主流的 JWT 工具庫,支持 JWT 的生成、解析與驗(yàn)證
- Spring Web:用于編寫接口測(cè)試認(rèn)證授權(quán)流程
Maven 依賴配置示例:
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>2. 核心概念說明
JWT 結(jié)構(gòu): 由 Header(頭部)、Payload(載荷)、Signature(簽名)三部分組成,以.分隔,例如eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
- Header:存儲(chǔ)算法類型和令牌類型,如{"alg":"HS256","typ":"JWT"}
- Payload:存儲(chǔ)用戶身份信息(如用戶名、角色)和過期時(shí)間等聲明,不建議存放敏感信息
- Signature:通過 Header 指定的算法,結(jié)合密鑰對(duì) Header 和 Payload 進(jìn)行加密,保證令牌不被篡改
認(rèn)證流程: 用戶登錄成功后,服務(wù)端生成 JWT 返回給客戶端;客戶端后續(xù)請(qǐng)求攜帶 JWT,服務(wù)端驗(yàn)證令牌有效性后完成授權(quán)
三、核心功能實(shí)現(xiàn)
1. JWT 工具類封裝
@Component
public class JwtUtils {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.access-token-expire-time}")
private long accessTokenExpireTime;
@Value("${jwt.refresh-token-expire-time}")
private long refreshTokenExpireTime;
/**
* 生成訪問令牌
*/
public String generateAccessToken(String username) {
Map<String, Object> claims = new HashMap<>();
return generateToken(claims, username, accessTokenExpireTime);
}
/**
* 生成刷新令牌
*/
public String generateRefreshToken(String username) {
Map<String, Object> claims = new HashMap<>();
return generateToken(claims, username, refreshTokenExpireTime);
}
/**
* 生成token
*/
private String generateToken(Map<String, Object> claims, String subject, long expireTime) {
SecretKey key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expireTime))
.signWith(key, SignatureAlgorithm.HS512)
.compact();
}
/**
* 從token中獲取用戶名
*/
public String getUsernameFromToken(String token) {
Claims claims = getClaimsFromToken(token);
return claims.getSubject();
}
/**
* 驗(yàn)證token是否有效
*/
public boolean validateToken(String token) {
try {
SecretKey key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}
/**
* 判斷token是否過期
*/
public boolean isTokenExpired(String token) {
Claims claims = getClaimsFromToken(token);
Date expiration = claims.getExpiration();
return expiration.before(new Date());
}
/**
* 從token中獲取claims
*/
private Claims getClaimsFromToken(String token) {
SecretKey key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
}
}2. 實(shí)現(xiàn)認(rèn)證過濾器
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtUtils jwtUtils;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
// 跳過登錄和刷新令牌的接口
String requestURI = request.getRequestURI();
if (requestURI.equals("/api/auth/login") || requestURI.equals("/api/auth/refresh")) {
chain.doFilter(request, response);
return;
}
// 從請(qǐng)求頭獲取token
String token = getTokenFromRequest(request);
if (StringUtils.hasText(token) && jwtUtils.validateToken(token)) {
String username = jwtUtils.getUsernameFromToken(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 設(shè)置認(rèn)證信息到上下文
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
} else {
SecurityContextHolder.clearContext();
}
chain.doFilter(request, response);
}
private String getTokenFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}3. 實(shí)現(xiàn)無感刷新令牌過濾器
創(chuàng)建JwtRefreshFilter實(shí)現(xiàn)令牌的無感刷新:
@Component
public class JwtRefreshFilter extends OncePerRequestFilter {
@Autowired
private JwtUtils jwtUtils;
@Autowired
private UserDetailsService userDetailsService;
// 當(dāng)token剩余有效期小于10分鐘時(shí),自動(dòng)刷新
private static final long REFRESH_THRESHOLD = 10 * 60 * 1000;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 跳過登錄和刷新接口
String requestURI = request.getRequestURI();
if (requestURI.equals("/api/auth/login") || requestURI.equals("/api/auth/refresh")) {
filterChain.doFilter(request, response);
return;
}
// 獲取請(qǐng)求頭中的token
String authorizationHeader = request.getHeader("Authorization");
if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
String token = authorizationHeader.substring(7);
try {
if (jwtUtils.validateToken(token)) {
// 檢查token是否即將過期
long remainingTime = jwtUtils.getTokenRemainingTime(token);
if (remainingTime < REFRESH_THRESHOLD) {
String username = jwtUtils.getUsernameFromToken(token);
String newAccessToken = jwtUtils.generateAccessToken(username);
// 將新token添加到響應(yīng)頭
response.setHeader("Authorization", "Bearer " + newAccessToken);
}
}
} catch (Exception e) {
SecurityContextHolder.clearContext();
}
filterChain.doFilter(request, response);
}
}4. 配置 Spring Security
創(chuàng)建SecurityConfig配置安全規(guī)則:
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtAuthenticationFilter jwtAuthFilter,
JwtRefreshFilter jwtRefreshFilter) throws Exception {
http
.cors().and().csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeHttpRequests()
.antMatchers("/api/auth/**", "/api/public/**").permitAll()
.anyRequest().authenticated()
.and()
.exceptionHandling()
.authenticationEntryPoint((request, response, ex) -> {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.getWriter().write("{\"error\":\"Unauthorized\",\"message\":\"Authentication required\"}");
});
// 添加JWT過濾器
http.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
http.addFilterBefore(jwtRefreshFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}5. 實(shí)現(xiàn)認(rèn)證服務(wù)
創(chuàng)建AuthService接口和實(shí)現(xiàn)類處理登錄和刷新令牌業(yè)務(wù):
public interface AuthService {
LoginResponseDTO login(LoginRequestDTO loginRequest);
LoginResponseDTO refreshToken(RefreshTokenRequestDTO refreshTokenRequest);
}
@Service
public class AuthServiceImpl implements AuthService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtUtils jwtUtils;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${jwt.refresh-token-expire-time}")
private long refreshTokenExpireTime;
@Override
public LoginResponseDTO login(LoginRequestDTO loginRequest) {
// 驗(yàn)證輸入
if (loginRequest == null || StringUtils.isEmpty(loginRequest.getUsername()) ||
StringUtils.isEmpty(loginRequest.getPassword())) {
throw new BadCredentialsException("Invalid login request");
}
try {
// 執(zhí)行認(rèn)證
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(),
loginRequest.getPassword()
)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
// 生成token
String username = loginRequest.getUsername();
String accessToken = jwtUtils.generateAccessToken(username);
String refreshToken = jwtUtils.generateRefreshToken(username);
// 存儲(chǔ)refresh token到Redis
String refreshTokenKey = "refresh_token:" + username;
stringRedisTemplate.opsForValue().set(
refreshTokenKey,
refreshToken,
refreshTokenExpireTime,
TimeUnit.MILLISECONDS
);
// 構(gòu)建響應(yīng)
LoginResponseDTO response = new LoginResponseDTO();
response.setAccessToken(accessToken);
response.setRefreshToken(refreshToken);
response.setExpiresIn(refreshTokenExpireTime);
return response;
} catch (AuthenticationException e) {
throw new BadCredentialsException("Invalid username or password");
}
}
@Override
public LoginResponseDTO refreshToken(RefreshTokenRequestDTO refreshTokenRequest) {
// 驗(yàn)證輸入
if (refreshTokenRequest == null || StringUtils.isEmpty(refreshTokenRequest.getRefreshToken())) {
throw new BadCredentialsException("Invalid refresh token request");
}
String refreshToken = refreshTokenRequest.getRefreshToken();
// 驗(yàn)證refresh token
if (!jwtUtils.validateToken(refreshToken)) {
throw new BadCredentialsException("Invalid refresh token");
}
String username = jwtUtils.getUsernameFromToken(refreshToken);
if (StringUtils.isEmpty(username)) {
throw new BadCredentialsException("Invalid refresh token");
}
// 驗(yàn)證Redis中存儲(chǔ)的refresh token
String storedToken = stringRedisTemplate.opsForValue().get("refresh_token:" + username);
if (storedToken == null || !storedToken.equals(refreshToken)) {
throw new BadCredentialsException("Invalid refresh token");
}
// 生成新的token
String newAccessToken = jwtUtils.generateAccessToken(username);
String newRefreshToken = jwtUtils.generateRefreshToken(username);
// 更新Redis中的refresh token
stringRedisTemplate.opsForValue().set(
"refresh_token:" + username,
newRefreshToken,
refreshTokenExpireTime,
TimeUnit.MILLISECONDS
);
LoginResponseDTO response = new LoginResponseDTO();
response.setAccessToken(newAccessToken);
response.setRefreshToken(newRefreshToken);
response.setExpiresIn(refreshTokenExpireTime);
return response;
}
}四、相關(guān)注解總結(jié)
| 注解名稱 | 核心作用 | 使用場(chǎng)景 | 關(guān)鍵注意事項(xiàng) |
|---|---|---|---|
| @EnableWebSecurity | 開啟 Spring Security 的 Web 安全功能,加載安全過濾器鏈和相關(guān)配置 | 標(biāo)注在自定義的 Spring Security 配置類上 | 必須搭配@Configuration使用,否則配置不生效 |
| @EnableGlobalMethodSecurity | 開啟方法級(jí)權(quán)限控制,支持多種權(quán)限注解 | 標(biāo)注在 Security 配置類上,需指定啟用的注解類型 | 常用屬性:prePostEnabled=true(啟用@PreAuthorize等)、securedEnabled=true(啟用@Secured) |
| @PreAuthorize | 方法執(zhí)行前校驗(yàn)權(quán)限,支持 SpEL 表達(dá)式,可實(shí)現(xiàn)復(fù)雜權(quán)限判斷 | 控制器接口方法、服務(wù)層方法的權(quán)限控制(細(xì)粒度權(quán)限) | 依賴@EnableGlobalMethodSecurity(prePostEnabled=true),支持角色、權(quán)限、請(qǐng)求參數(shù)校驗(yàn) |
| @PostAuthorize | 方法執(zhí)行后校驗(yàn)權(quán)限,基于方法返回值判斷 | 需根據(jù)返回結(jié)果控制權(quán)限的場(chǎng)景(極少使用,避免方法執(zhí)行產(chǎn)生副作用) | SpEL 表達(dá)式中用returnObject指代方法返回值 |
| @Secured | 基于角色的粗粒度權(quán)限控制 | 控制器或服務(wù)層方法的角色校驗(yàn) | 需啟用securedEnabled=true,角色名稱必須以ROLE_為前綴 |
| @RolesAllowed | JSR-250 規(guī)范注解,基于角色的權(quán)限控制 | 控制器或服務(wù)層方法的多角色授權(quán) | 需啟用jsr250Enabled=true,角色名稱可省略ROLE_前綴(框架自動(dòng)補(bǔ)充) |
| @AuthenticationPrincipal | 直接獲取當(dāng)前認(rèn)證用戶的UserDetails或自定義用戶信息 | 控制器方法參數(shù)中,需獲取當(dāng)前登錄用戶信息時(shí) | 無需手動(dòng)從SecurityContextHolder獲取,直接注入即可 |
| @PreFilter | 方法執(zhí)行前,對(duì)集合類型參數(shù)進(jìn)行過濾,僅保留符合權(quán)限的元素 | 數(shù)據(jù)查詢前的參數(shù)過濾(數(shù)據(jù)級(jí)權(quán)限控制) | 僅對(duì)集合類型參數(shù)生效,SpEL 表達(dá)式用filterObject指代集合元素 |
| @PostFilter | 方法執(zhí)行后,對(duì)集合類型返回值進(jìn)行過濾,僅保留符合權(quán)限的元素 | 數(shù)據(jù)返回后的結(jié)果過濾(數(shù)據(jù)級(jí)權(quán)限控制) | 僅對(duì)集合類型返回值生效,依賴@EnableGlobalMethodSecurity啟用 |
總結(jié)
本指南介紹了如何在 Spring Boot 應(yīng)用中實(shí)現(xiàn)基于 JWT 的認(rèn)證授權(quán)功能,包括:
- JWT 令牌的生成與驗(yàn)證
- 基于 Spring Security 的認(rèn)證過濾器
- 令牌刷新機(jī)制與無感刷新實(shí)現(xiàn)
- 完整的登錄與授權(quán)流程
通過這種方式,我們可以實(shí)現(xiàn)無狀態(tài)的認(rèn)證系統(tǒng),適合分布式應(yīng)用和前后端分離架構(gòu)。實(shí)際應(yīng)用中,還可以根據(jù)需要添加更多功能,如令牌撤銷、角色權(quán)限控制等。
到此這篇關(guān)于Spring Boot JWT登錄授權(quán)使用(無感刷新))的文章就介紹到這了,更多相關(guān)Spring Boot JWT登錄授權(quán)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot集成Spring Security用JWT令牌實(shí)現(xiàn)登錄和鑒權(quán)的方法
- spring boot如何基于JWT實(shí)現(xiàn)單點(diǎn)登錄詳解
- SpringBoot+Vue+JWT的前后端分離登錄認(rèn)證詳細(xì)步驟
- SpringBoot使用JWT實(shí)現(xiàn)登錄驗(yàn)證的方法示例
- springboot+jwt+微信小程序授權(quán)登錄獲取token的方法實(shí)例
- SpringBoot JWT實(shí)現(xiàn)token登錄刷新功能
- SpringBoot結(jié)合JWT實(shí)現(xiàn)用戶登錄、注冊(cè)、鑒權(quán)
- SpringBoot實(shí)現(xiàn)登錄校驗(yàn)(JWT令牌)
- SpringBoot+JWT實(shí)現(xiàn)單點(diǎn)登錄完美解決方案
相關(guān)文章
Java編程實(shí)現(xiàn)鄰接矩陣表示稠密圖代碼示例
這篇文章主要介紹了Java編程實(shí)現(xiàn)鄰接矩陣表示稠密圖代碼示例,具有一定參考價(jià)值,需要的朋友可以了解下。2017-11-11
基于java類路徑classpath和包的實(shí)例講解
下面小編就為大家分享一篇基于java類路徑classpath和包的實(shí)例講解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-01-01
Java8 如何移除兩個(gè)相同的List對(duì)象
這篇文章主要介紹了Java8 如何移除兩個(gè)相同的List對(duì)象,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-01-01
application作用域?qū)崿F(xiàn)用戶登錄擠掉之前登錄用戶代碼
這篇文章主要介紹了application作用域?qū)崿F(xiàn)用戶登錄擠掉之前登錄用戶代碼,具有一定參考價(jià)值,需要的朋友可以了解下。2017-11-11
java isInterrupted()判斷線程的實(shí)例講解
在本篇內(nèi)容里小編給大家分享的是一篇關(guān)于java isInterrupted()判斷線程的實(shí)例講解內(nèi)容,有興趣的朋友們可以學(xué)習(xí)下。2021-05-05
基于MyBatis的parameterType傳入?yún)?shù)類型
這篇文章主要介紹了基于MyBatis的parameterType傳入?yún)?shù)類型,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09
springboot中的springSession的存儲(chǔ)和獲取實(shí)現(xiàn)
這篇文章主要介紹了springboot中的springSession的存儲(chǔ)和獲取實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09

