SpringBoot整合JWT實(shí)現(xiàn)登錄認(rèn)證與接口授權(quán)的全流程
一、引言
在分布式系統(tǒng)和前后端分離架構(gòu)中,傳統(tǒng)的基于 Session 的認(rèn)證方式存在跨域難處理、服務(wù)端存儲壓力大等問題。JWT(JSON Web Token) 作為一種輕量級的身份認(rèn)證與授權(quán)方案,憑借其無狀態(tài)、可跨域、易于擴(kuò)展的特性,成為 Spring Boot 項目中實(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 項目的pom.xml(Maven)或build.gradle(Gradle)中引入以下
- Spring Security:提供認(rèn)證與授權(quán)的基礎(chǔ)框架
- JJWT:Java 領(lǐng)域主流的 JWT 工具庫,支持 JWT 的生成、解析與驗證
- Spring Web:用于編寫接口測試認(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:存儲算法類型和令牌類型,如{"alg":"HS256","typ":"JWT"}
- Payload:存儲用戶身份信息(如用戶名、角色)和過期時間等聲明,不建議存放敏感信息
- Signature:通過 Header 指定的算法,結(jié)合密鑰對 Header 和 Payload 進(jìn)行加密,保證令牌不被篡改
認(rèn)證流程: 用戶登錄成功后,服務(wù)端生成 JWT 返回給客戶端;客戶端后續(xù)請求攜帶 JWT,服務(wù)端驗證令牌有效性后完成授權(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();
}
/**
* 驗證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;
}
// 從請求頭獲取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分鐘時,自動刷新
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;
}
// 獲取請求頭中的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) {
// 驗證輸入
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);
// 存儲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) {
// 驗證輸入
if (refreshTokenRequest == null || StringUtils.isEmpty(refreshTokenRequest.getRefreshToken())) {
throw new BadCredentialsException("Invalid refresh token request");
}
String refreshToken = refreshTokenRequest.getRefreshToken();
// 驗證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");
}
// 驗證Redis中存儲的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é)
| 注解名稱 | 核心作用 | 使用場景 | 關(guān)鍵注意事項 |
|---|---|---|---|
| @EnableWebSecurity | 開啟 Spring Security 的 Web 安全功能,加載安全過濾器鏈和相關(guān)配置 | 標(biāo)注在自定義的 Spring Security 配置類上 | 必須搭配@Configuration使用,否則配置不生效 |
@EnableGlobalMethodSecurity | 開啟方法級權(quán)限控制,支持多種權(quán)限注解 | 標(biāo)注在 Security 配置類上,需指定啟用的注解類型 | 常用屬性:prePostEnabled=true(啟用@PreAuthorize等)、securedEnabled=true(啟用@Secured) |
@PreAuthorize | 方法執(zhí)行前校驗權(quán)限,支持 SpEL 表達(dá)式,可實(shí)現(xiàn)復(fù)雜權(quán)限判斷 | 控制器接口方法、服務(wù)層方法的權(quán)限控制(細(xì)粒度權(quán)限) | 依賴@EnableGlobalMethodSecurity(prePostEnabled=true),支持角色、權(quán)限、請求參數(shù)校驗 |
@PostAuthorize | 方法執(zhí)行后校驗權(quán)限,基于方法返回值判斷 | 需根據(jù)返回結(jié)果控制權(quán)限的場景(極少使用,避免方法執(zhí)行產(chǎn)生副作用) | SpEL 表達(dá)式中用returnObject指代方法返回值 |
@Secured | 基于角色的粗粒度權(quán)限控制 | 控制器或服務(wù)層方法的角色校驗 | 需啟用securedEnabled=true,角色名稱必須以ROLE_為前綴 |
@RolesAllowed | JSR-250 規(guī)范注解,基于角色的權(quán)限控制 | 控制器或服務(wù)層方法的多角色授權(quán) | 需啟用jsr250Enabled=true,角色名稱可省略ROLE_前綴(框架自動補(bǔ)充) |
@AuthenticationPrincipal | 直接獲取當(dāng)前認(rèn)證用戶的UserDetails或自定義用戶信息 | 控制器方法參數(shù)中,需獲取當(dāng)前登錄用戶信息時 | 無需手動從SecurityContextHolder獲取,直接注入即可 |
@PreFilter | 方法執(zhí)行前,對集合類型參數(shù)進(jìn)行過濾,僅保留符合權(quán)限的元素 | 數(shù)據(jù)查詢前的參數(shù)過濾(數(shù)據(jù)級權(quán)限控制) | 僅對集合類型參數(shù)生效,SpEL 表達(dá)式用filterObject指代集合元素 |
@PostFilter | 方法執(zhí)行后,對集合類型返回值進(jìn)行過濾,僅保留符合權(quán)限的元素 | 數(shù)據(jù)返回后的結(jié)果過濾(數(shù)據(jù)級權(quán)限控制) | 僅對集合類型返回值生效,依賴@EnableGlobalMethodSecurity啟用 |
總結(jié)
本指南介紹了如何在 Spring Boot 應(yīng)用中實(shí)現(xiàn)基于 JWT 的認(rèn)證授權(quán)功能,包括:
- JWT 令牌的生成與驗證
- 基于 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)限控制等。
以上就是SpringBoot整合JWT實(shí)現(xiàn)登錄認(rèn)證與接口授權(quán)的全流程的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot JWT登錄認(rèn)證與接口授權(quán)的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JavaSE網(wǎng)絡(luò)原理之UDP和TCP原理詳解
TCP、UDP都是屬于運(yùn)輸層的協(xié)議,提供端到端的進(jìn)程之間的邏輯通信,而IP協(xié)議是提供主機(jī)間的邏輯通信,應(yīng)用層規(guī)定應(yīng)用進(jìn)程在通信時所遵循的協(xié)議,這篇文章主要介紹了JavaSE網(wǎng)絡(luò)原理之UDP和TCP原理的相關(guān)資料,需要的朋友可以參考下2026-01-01
MyBatis-Plus 插件擴(kuò)展的實(shí)現(xiàn)
MyBatis-Plus通過插件擴(kuò)展機(jī)制增強(qiáng)功能,基于MyBatis Interceptor攔截器,包括分頁插件、邏輯刪除、SQL性能分析和樂觀鎖等,開發(fā)者可自定義插件以適應(yīng)特定需求,有效地增強(qiáng)SQL執(zhí)行過程的控制和優(yōu)化,同時注意插件使用的性能影響和執(zhí)行順序2024-09-09
springcloud?gateway實(shí)現(xiàn)簡易版灰度路由步驟詳解
這篇文章主要為大家介紹了springcloud?gateway實(shí)現(xiàn)簡易版灰度路由步驟詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-11-11
簡單總結(jié)Java的反射機(jī)制的運(yùn)用
這篇文章主要介紹了Java的反射機(jī)制的運(yùn)用,對一些常用的類作了一些說明,需要的朋友可以參考下2015-11-11

