本指南扩展了我们的 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;
// Optional: Track refresh token (for additional checks if needed)
private String refreshToken;
// 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
中添加 Redis 配置:
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());
// Store refresh token in Redis
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");
}
// Verify with Redis
String cachedToken = refreshTokenService.getRefreshToken(username);
if (cachedToken == null || !cachedToken.equals(refreshRequest.getRefreshToken())) {
return ResponseEntity.status(403).body("Invalid or expired refresh token");
}
// Generate new access 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
。
-
-
访问安全端点
-
在
Authorization
头中使用accessToken
。
-
-
刷新令牌 (
/auth/refresh
)-
发送
refreshToken
以续订accessToken
。
-
结论
在本指南中,我们使用 Spring Security、JWT(访问令牌和刷新令牌)、Redis 和关系型数据库实现了一个强大的认证系统。以下是关键组件及其作用的总结:
-
访问令牌
-
用于认证 API 请求的短期令牌。
-
安全且高效,用于保护端点。
-
-
刷新令牌
-
长期令牌,安全地存储在 Redis 中以续订访问令牌。
-
允许用户在不频繁登录的情况下无缝访问。
-
-
Redis
-
作为会话和令牌管理的可扩展缓存层。
-
确保快速查找和令牌的自动过期。
-
-
Spring Security
-
提供灵活的框架来保护端点。
-
支持与 JWT 和自定义过滤器的集成。
-
通过结合这些组件,我们实现了:
-
增强的安全性:令牌快速过期,最小化凭证被盗的风险。
-
可扩展性:Redis 为分布式系统提供高性能的会话管理。
-
改进的用户体验:刷新令牌允许用户在不频繁登录的情况下持续访问。
-
可定制性:框架可以轻松扩展以满足特定的业务需求。
这种架构非常适合现代 Web 应用程序,其中安全性、性能和用户便利性至关重要。在此基础之上,你可以构建更高级的功能,例如基于角色的访问控制(RBAC)、多因素认证,甚至 OAuth 2.0 集成,以进一步增强系统。
没有回复内容