前言
JWT(JSON Web Token)是一种流行的无状态认证方式。本文将介绍 Spring Boot 集成 JWT 的完整实现,包括 Token 生成、验证、刷新和双 Token 机制。
JWT 基础
1. JWT 结构
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
- Header - 算法和 Token 类型
- Payload - 声明(用户信息)
- Signature - 签名
2. 添加依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
JWT 工具类
1. 创建 JwtService
package com.example.demo.security;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Service
@RequiredArgsConstructor
public class JwtService {
@Value("${app.jwt.secret}")
private String jwtSecret;
@Value("${app.jwt.expiration}")
private long jwtExpiration;
@Value("${app.jwt.refresh-expiration}")
private long refreshExpiration;
/**
* 从 Token 提取用户名
*/
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
/**
* 提取所有声明
*/
public Claims extractAllClaims(String token) {
return Jwts.parser()
.verifyWith(getSignInKey())
.build()
.parseSignedClaims(token)
.getPayload();
}
/**
* 提取单个声明
*/
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
/**
* 生成 Token
*/
public String generateToken(UserDetails userDetails) {
return generateToken(new HashMap<>(), userDetails);
}
/**
* 生成带额外声明的 Token
*/
public String generateToken(Map<String, Object> extraClaims, UserDetails userDetails) {
Instant now = Instant.now();
return Jwts.builder()
.claims(extraClaims)
.subject(userDetails.getUsername())
.issuedAt(Date.from(now))
.expiration(Date.from(now.plus(jwtExpiration, ChronoUnit.MILLIS)))
.signWith(getSignInKey())
.compact();
}
/**
* 生成刷新 Token
*/
public String generateRefreshToken(UserDetails userDetails) {
Instant now = Instant.now();
return Jwts.builder()
.subject(userDetails.getUsername())
.issuedAt(Date.from(now))
.expiration(Date.from(now.plus(refreshExpiration, ChronoUnit.MILLIS)))
.signWith(getSignInKey())
.compact();
}
/**
* 验证 Token 是否有效
*/
public boolean isTokenValid(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername())) && !isTokenExpired(token);
}
/**
* 检查 Token 是否过期
*/
private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
/**
* 提取过期时间
*/
private Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
/**
* 获取签名密钥
*/
private SecretKey getSignInKey() {
byte[] keyBytes = jwtSecret.getBytes(StandardCharsets.UTF_8);
return Keys.hmacShaKeyFor(keyBytes);
}
}
2. 配置文件
app:
jwt:
secret: your-256-bit-secret-key-here-change-in-production
expiration: 3600000 # 1 小时
refresh-expiration: 604800000 # 7 天
JWT 认证过滤器
1. 创建过滤器
package com.example.demo.security;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain
) throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
final String jwt;
final String username;
// 检查 Token
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
jwt = authHeader.substring(7);
try {
username = jwtService.extractUsername(jwt);
// 验证 Token 并设置认证
if (username != null &&
SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails =
userDetailsService.loadUserByUsername(username);
if (jwtService.isTokenValid(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authToken.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
} catch (JwtException e) {
// Token 无效,继续过滤
logger.warn("JWT 无效:" + e.getMessage());
}
filterChain.doFilter(request, response);
}
}
2. 配置安全过滤器链
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated()
)
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.exceptionHandling(ex -> ex
.authenticationEntryPoint(new JwtAuthenticationEntryPoint())
.accessDeniedHandler(new JwtAccessDeniedHandler())
)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
认证接口
1. 认证请求/响应
@Data
public class AuthRequest {
@NotBlank(message = "用户名不能为空")
private String username;
@NotBlank(message = "密码不能为空")
private String password;
}
@Data
@AllArgsConstructor
public class AuthResponse {
private String accessToken;
private String refreshToken;
private String tokenType = "Bearer";
private long expiresIn;
}
@Data
@AllArgsConstructor
public class RefreshTokenRequest {
@NotBlank(message = "刷新 Token 不能为空")
private String refreshToken;
}
2. 认证控制器
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthenticationManager authenticationManager;
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
/**
* 登录
*/
@PostMapping("/login")
public R<AuthResponse> login(@Valid @RequestBody AuthRequest request) {
// 认证
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(),
request.getPassword()
)
);
// 加载用户
UserDetails userDetails =
userDetailsService.loadUserByUsername(request.getUsername());
// 生成 Token
String accessToken = jwtService.generateToken(userDetails);
String refreshToken = jwtService.generateRefreshToken(userDetails);
return R.ok(new AuthResponse(
accessToken,
refreshToken,
"Bearer",
3600
));
}
/**
* 刷新 Token
*/
@PostMapping("/refresh")
public R<AuthResponse> refresh(@Valid @RequestBody RefreshTokenRequest request) {
String refreshToken = request.getRefreshToken();
try {
String username = jwtService.extractUsername(refreshToken);
if (username != null) {
UserDetails userDetails =
userDetailsService.loadUserByUsername(username);
if (jwtService.isTokenValid(refreshToken, userDetails)) {
String accessToken = jwtService.generateToken(userDetails);
return R.ok(new AuthResponse(
accessToken,
refreshToken,
"Bearer",
3600
));
}
}
return R.error(401, "刷新 Token 无效");
} catch (JwtException e) {
return R.error(401, "刷新 Token 已过期");
}
}
/**
* 登出
*/
@PostMapping("/logout")
public R<Void> logout() {
// 无状态认证,客户端删除 Token 即可
return R.ok();
}
}
3. 异常处理
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException
) throws IOException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"message\":\"未授权访问\"}");
}
}
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(
HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException
) throws IOException {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":403,\"message\":\"无权访问\"}");
}
}
双 Token 机制
1. Token 刷新策略
@Service
@RequiredArgsConstructor
public class TokenRefreshService {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
/**
* 刷新 Access Token
*/
public String refreshAccessToken(String refreshToken) {
try {
String username = jwtService.extractUsername(refreshToken);
if (username == null) {
throw new BusinessException("TOKEN_INVALID", "刷新 Token 无效");
}
UserDetails userDetails =
userDetailsService.loadUserByUsername(username);
if (!jwtService.isTokenValid(refreshToken, userDetails)) {
throw new BusinessException("TOKEN_EXPIRED", "刷新 Token 已过期");
}
return jwtService.generateToken(userDetails);
} catch (JwtException e) {
throw new BusinessException("TOKEN_INVALID", "刷新 Token 无效");
}
}
}
2. Token 黑名单
@Service
@RequiredArgsConstructor
public class TokenBlacklistService {
private final RedisTemplate<String, Object> redisTemplate;
/**
* 将 Token 加入黑名单
*/
public void blacklist(String token, long expireTime) {
redisTemplate.opsForValue().set(
"token:blacklist:" + token,
"blacklisted",
expireTime,
TimeUnit.MILLISECONDS
);
}
/**
* 检查 Token 是否在黑名单中
*/
public boolean isBlacklisted(String token) {
return Boolean.TRUE.equals(
redisTemplate.hasKey("token:blacklist:" + token)
);
}
}
3. 登出时加入黑名单
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
private final TokenBlacklistService blacklistService;
private final JwtService jwtService;
@PostMapping("/logout")
public R<Void> logout(
@RequestHeader("Authorization") String authorization
) {
if (authorization != null && authorization.startsWith("Bearer ")) {
String token = authorization.substring(7);
Date expiration = jwtService.extractClaim(token, Claims::getExpiration);
long expireTime = expiration.getTime() - System.currentTimeMillis();
if (expireTime > 0) {
blacklistService.blacklist(token, expireTime);
}
}
return R.ok();
}
}
最佳实践
1. 安全配置
app:
jwt:
# 使用环境变量
secret: ${JWT_SECRET:your-256-bit-secret-key}
expiration: 3600000 # 1 小时
refresh-expiration: 604800000 # 7 天
issuer: demo-api
audience: demo-client
2. Token 存储
// 前端存储
localStorage.setItem('refreshToken', refreshToken); // 长期存储
sessionStorage.setItem('accessToken', accessToken); // 会话存储
3. 自动刷新
// 前端自动刷新
let refreshTimer: NodeJS.Timeout;
function startRefreshTimer() {
refreshTimer = setTimeout(async () => {
const refreshToken = localStorage.getItem('refreshToken');
const response = await fetch('/api/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken })
});
const data = await response.json();
sessionStorage.setItem('accessToken', data.accessToken);
startRefreshTimer();
}, 50 * 60 * 1000); // 50 分钟刷新
}
4. 拦截器
// Axios 拦截器
axios.interceptors.request.use(config => {
const token = sessionStorage.getItem('accessToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
axios.interceptors.response.use(
response => response,
async error => {
if (error.response?.status === 401) {
// Token 过期,尝试刷新
const refreshToken = localStorage.getItem('refreshToken');
if (refreshToken) {
const response = await axios.post('/api/auth/refresh', {
refreshToken
});
sessionStorage.setItem('accessToken', response.data.accessToken);
// 重试原请求
const originalConfig = error.config;
originalConfig.headers.Authorization =
`Bearer ${response.data.accessToken}`;
return axios(originalConfig);
}
}
return Promise.reject(error);
}
);
总结
JWT 认证要点:
- ✅ Token 生成 - generateToken、generateRefreshToken
- ✅ Token 验证 - JwtAuthenticationFilter
- ✅ 双 Token 机制 - Access Token + Refresh Token
- ✅ Token 刷新 - 自动刷新、手动刷新
- ✅ Token 黑名单 - Redis 实现
- ✅ 最佳实践 - 安全存储、自动刷新
JWT 是无状态认证的理想选择。