SpringSecurity JWT令牌实战

SpringSecurity JWT令牌实战

JWT(JSON Web Token)是前后端分离场景下最流行的认证方案。本文实现一套完整的 JWT 认证系统。

项目结构

src/main/java/com/example/
├── config/
│ └── SecurityConfig.java
├── controller/
│ └── AuthController.java
├── dto/
│ ├── LoginRequest.java
│ └── TokenResponse.java
├── entity/
│ └── User.java
├── filter/
│ └── JwtAuthenticationFilter.java
├── repository/
│ └── UserRepository.java
├── service/
│ ├── AuthService.java
│ └── CustomUserDetailsService.java
└── util/
└── JwtUtil.java

依赖

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.12.3</version>
</dependency>
</dependencies>

JWT 工具类

@Component
public class JwtUtil {

@Value("${jwt.secret:mySecretKey}")
private String secret;

@Value("${jwt.expiration:86400000}")
private long expiration; // 24小时

@Value("${jwt.refresh-expiration:604800000}")
private long refreshExpiration; // 7天

private SecretKey getSigningKey() {
return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
}

public String generateToken(UserDetails userDetails) {
return generateToken(new HashMap<>(), userDetails, expiration);
}

public String generateRefreshToken(UserDetails userDetails) {
return generateToken(new HashMap<>(), userDetails, refreshExpiration);
}

public String generateToken(Map<String, Object> claims, UserDetails userDetails, long exp) {
return Jwts.builder()
.claims(claims)
.subject(userDetails.getUsername())
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + exp))
.signWith(getSigningKey())
.compact();
}

public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}

public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}

public boolean isTokenValid(String token, UserDetails userDetails) {
String username = extractUsername(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
}

private boolean isTokenExpired(String token) {
return extractClaim(token, Claims::getExpiration).before(new Date());
}

private Claims extractAllClaims(String token) {
return Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
}
}

安全配置

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

@Autowired
private JwtAuthenticationFilter jwtAuthFilter;

@Autowired
private CustomUserDetailsService userDetailsService;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated()
)
.authenticationProvider(authenticationProvider())
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);

return http.build();
}

@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder());
return provider;
}

@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

JWT 认证过滤器

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

@Autowired
private JwtUtil jwtUtil;

@Autowired
private CustomUserDetailsService userDetailsService;

@Autowired
private TokenBlacklistService tokenBlacklistService;

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {

String authHeader = request.getHeader("Authorization");

if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}

String jwt = authHeader.substring(7);

// 检查黑名单
if (tokenBlacklistService.isBlacklisted(jwt)) {
filterChain.doFilter(request, response);
return;
}

String username = jwtUtil.extractUsername(jwt);

if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);

if (jwtUtil.isTokenValid(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authToken.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}

filterChain.doFilter(request, response);
}
}

认证服务

@Service
public class AuthService {

@Autowired
private AuthenticationManager authenticationManager;

@Autowired
private JwtUtil jwtUtil;

@Autowired
private CustomUserDetailsService userDetailsService;

@Autowired
private TokenBlacklistService tokenBlacklistService;

public TokenResponse login(LoginRequest request) {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(), request.getPassword())
);

UserDetails userDetails = userDetailsService.loadUserByUsername(request.getUsername());
String accessToken = jwtUtil.generateToken(userDetails);
String refreshToken = jwtUtil.generateRefreshToken(userDetails);

return TokenResponse.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.tokenType("Bearer")
.expiresIn(86400)
.build();
}

public TokenResponse refresh(String refreshToken) {
String username = jwtUtil.extractUsername(refreshToken);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);

if (!jwtUtil.isTokenValid(refreshToken, userDetails)) {
throw new RuntimeException("Invalid refresh token");
}

String newAccessToken = jwtUtil.generateToken(userDetails);
String newRefreshToken = jwtUtil.generateRefreshToken(userDetails);

return TokenResponse.builder()
.accessToken(newAccessToken)
.refreshToken(newRefreshToken)
.tokenType("Bearer")
.expiresIn(86400)
.build();
}

public void logout(String token) {
tokenBlacklistService.addToBlacklist(token);
}
}

Token 黑名单(Redis)

@Service
public class TokenBlacklistService {

@Autowired
private StringRedisTemplate redisTemplate;

private static final String BLACKLIST_PREFIX = "blacklist:";

public void addToBlacklist(String token) {
long expiration = getExpirationFromToken(token);
redisTemplate.opsForValue().set(
BLACKLIST_PREFIX + token, "1", expiration, TimeUnit.MILLISECONDS);
}

public boolean isBlacklisted(String token) {
return Boolean.TRUE.equals(redisTemplate.hasKey(BLACKLIST_PREFIX + token));
}

private long getExpirationFromToken(String token) {
// 从 JWT 解析过期时间
return 86400000; // 简化处理
}
}

控制器

@RestController
@RequestMapping("/api/auth")
public class AuthController {

@Autowired
private AuthService authService;

@PostMapping("/login")
public Result<TokenResponse> login(@RequestBody @Valid LoginRequest request) {
return Result.success(authService.login(request));
}

@PostMapping("/refresh")
public Result<TokenResponse> refresh(@RequestHeader("X-Refresh-Token") String refreshToken) {
return Result.success(authService.refresh(refreshToken));
}

@PostMapping("/logout")
public Result<Void> logout(@RequestHeader("Authorization") String authHeader) {
String token = authHeader.substring(7);
authService.logout(token);
return Result.success();
}
}

前端使用

// 登录
const login = async (username, password) => {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await response.json();
localStorage.setItem('token', data.data.accessToken);
localStorage.setItem('refreshToken', data.data.refreshToken);
};

// 请求拦截器
axios.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});

// 响应拦截器(自动刷新)
axios.interceptors.response.use(
response => response,
async error => {
if (error.response.status === 401) {
const refreshToken = localStorage.getItem('refreshToken');
const response = await axios.post('/api/auth/refresh', null, {
headers: { 'X-Refresh-Token': refreshToken }
});
localStorage.setItem('token', response.data.data.accessToken);
return axios(error.config);
}
return Promise.reject(error);
}
);

总结

JWT 认证方案的关键点:

  1. Access Token:短期有效,用于接口认证
  2. Refresh Token:长期有效,用于刷新 Access Token
  3. 黑名单:登出时加入 Redis 黑名单
  4. 无状态:服务端不存储会话,支持水平扩展

这套方案适用于大多数前后端分离项目。


   转载规则


《SpringSecurity JWT令牌实战》 小乐 采用 知识共享署名 4.0 国际许可协议 进行许可。
  目录