使用 Spring Security 实现 JWT 和令牌刷新
本指南扩展了 Spring Security 设置,通过添加刷新令牌机制,使用户无需重新认证即可续订访问令牌。我们将把刷新令牌存储在 Redis 中,以实现安全的会话管理,并支持基于数据库的用户认证。
第一步:理解 JWT 和令牌刷新
为什么需要令牌刷新?
- 访问令牌:生命周期较短,以减少被盗风险。
- 刷新令牌:生命周期较长,允许客户端在无需用户重新登录的情况下获取新的访问令牌。
第二步:项目设置
在 pom.xml
中添加以下依赖项:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<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>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
第三步:用户实体
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
private String roles;
private String refreshToken; // Optional: Track refresh token
// Getters and Setters
}
第四步:JWT 工具类
更新 JwtUtil
类以分别处理访问令牌和刷新令牌。
import io.jsonwebtoken.*;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component
public class JwtUtil {
private static final String SECRET_KEY = "accessSecret";
private static final String REFRESH_SECRET_KEY = "refreshSecret";
private static final long ACCESS_EXPIRATION = 1000 * 60 * 15; // 15 minutes
private static final long REFRESH_EXPIRATION = 1000 * 60 * 60 * 24; // 1 day
public String generateAccessToken(String username) {
return Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + ACCESS_EXPIRATION))
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
}
public String generateRefreshToken(String username) {
return Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + REFRESH_EXPIRATION))
.signWith(SignatureAlgorithm.HS256, REFRESH_SECRET_KEY)
.compact();
}
public String extractUsername(String token, boolean isRefreshToken) {
String secretKey = isRefreshToken ? REFRESH_SECRET_KEY : SECRET_KEY;
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token)
.getBody()
.getSubject();
}
public boolean validateToken(String token, boolean isRefreshToken) {
try {
String secretKey = isRefreshToken ? REFRESH_SECRET_KEY : SECRET_KEY;
Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}
}
第五步:Redis 集成用于刷新令牌
配置 Redis
在 application.properties
中添加以下配置:
spring.redis.host=localhost
spring.redis.port=6379
第六步:认证控制器
实现登录和刷新令牌的 API:
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
@RestController
@RequestMapping("/auth")
public class AuthController {
private final CustomUserDetailsService userDetailsService;
private final JwtUtil jwtUtil;
private final RefreshTokenService refreshTokenService;
public AuthController(CustomUserDetailsService userDetailsService, JwtUtil jwtUtil, RefreshTokenService refreshTokenService) {
this.userDetailsService = userDetailsService;
this.jwtUtil = jwtUtil;
this.refreshTokenService = refreshTokenService;
}
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody AuthRequest authRequest) {
// Authenticate user
UserDetails userDetails = userDetailsService.loadUserByUsername(authRequest.getUsername());
if (!userDetails.getPassword().equals(authRequest.getPassword())) {
return ResponseEntity.status(401).body("Invalid credentials");
}
// Generate tokens
String accessToken = jwtUtil.generateAccessToken(userDetails.getUsername());
String refreshToken = jwtUtil.generateRefreshToken(userDetails.getUsername());
refreshTokenService.storeRefreshToken(userDetails.getUsername(), refreshToken);
return ResponseEntity.ok(new AuthResponse(accessToken, refreshToken));
}
@PostMapping("/refresh")
public ResponseEntity<?> refreshToken(@RequestBody RefreshRequest refreshRequest) {
String username = jwtUtil.extractUsername(refreshRequest.getRefreshToken(), true);
if (!jwtUtil.validateToken(refreshRequest.getRefreshToken(), true)) {
return ResponseEntity.status(403).body("Invalid refresh token");
}
String cachedToken = refreshTokenService.getRefreshToken(username);
if (cachedToken == null || !cachedToken.equals(refreshRequest.getRefreshToken())) {
return ResponseEntity.status(403).body("Invalid or expired refresh token");
}
String newAccessToken = jwtUtil.generateAccessToken(username);
return ResponseEntity.ok(new AuthResponse(newAccessToken, refreshRequest.getRefreshToken()));
}
}
DTO 类
public class AuthRequest {
private String username;
private String password;
// Getters and Setters
}
public class AuthResponse {
private String accessToken;
private String refreshToken;
public AuthResponse(String accessToken, String refreshToken) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
}
// Getters
}
public class RefreshRequest {
private String refreshToken;
// Getters and Setters
}
第七步:安全配置
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthenticationFilter jwtFilter) throws Exception {
http.csrf().disable()
.authorizeHttpRequests(auth -> auth
.antMatchers("/auth/**").permitAll()
.anyRequest().authenticated())
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
第八步:测试应用程序
测试流程
-
登录端点 (
/auth/login
)- 发送用户名和密码。
- 接收
accessToken
和refreshToken
。
-
访问安全端点
- 使用
accessToken
作为Authorization
头。
- 使用
-
刷新令牌 (
/auth/refresh
)- 发送
refreshToken
以续订accessToken
。
- 发送
结论
本指南使用 Spring Security、JWT、Redis 和数据库实现了一个高效的认证系统,具备以下特点:
- 访问令牌:短期有效,用于保护 API。
- 刷新令牌:长期有效,存储于 Redis 中,用于续订访问令牌。
- Redis:提供快速查找和自动过期的会话管理。
- Spring Security:灵活的框架,可与 JWT 和自定义过滤器集成。
该架构适合需要高安全性和性能的现代 Web 应用程序,并支持进一步扩展功能(如 RBAC、多因素认证等)。
评论区