spring-boot-security-jwt by giuseppe-trisciuoglio/developer-kit
npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill spring-boot-security-jwt使用 Spring Security 6.x 和 JJWT 库,为 Spring Boot 3.5.x 应用程序提供的全面 JWT(JSON Web Token)认证和授权模式。此技能提供了用于无状态认证、基于角色的访问控制以及与现代化认证提供者集成的生产就绪实现。
JWT 认证为 Spring Boot 应用程序提供了无状态、可扩展的安全性。此技能涵盖了完整的 JWT 生命周期管理,包括令牌生成、验证、刷新策略以及与数据库支持和 OAuth2 认证提供者的集成模式。实现遵循 Spring Security 6.x 最佳实践,采用现代化的 SecurityFilterChain 配置。
在以下场景中使用此技能:
<dependencies>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</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-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<!-- JWT Library -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.6</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
<!-- Database -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
dependencies {
// Spring Security
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server")
// JWT Library
implementation("io.jsonwebtoken:jjwt-api:0.12.6")
implementation("io.jsonwebtoken:jjwt-impl:0.12.6")
implementation("io.jsonwebtoken:jjwt-jackson:0.12.6")
// Database
runtimeOnly("com.h2database:h2")
runtimeOnly("org.postgresql:postgresql")
// Testing
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.security:spring-security-test")
testImplementation("org.testcontainers:junit-jupiter")
}
按照以下步骤在 Spring Boot 中实现 JWT 认证:
在项目中包含 spring-boot-starter-security、spring-boot-starter-oauth2-resource-server 和 JJWT 库(jjwt-api、jjwt-impl、jjwt-jackson)。
在 application.yml 中设置 JWT 密钥、访问令牌过期时间、刷新令牌过期时间和签发者。切勿在版本控制中硬编码密钥。
实现 JwtService,包含生成令牌、提取声明、验证令牌和检查过期的方法。使用 Jwts.builder() 创建令牌。
创建继承自 OncePerRequestFilter 的 JwtAuthenticationFilter。从 Authorization 头或 Cookie 中提取 JWT,验证它,并设置 SecurityContext 认证。
使用 @EnableWebSecurity 和 @EnableMethodSecurity 设置 SecurityConfig。配置无状态会话管理、禁用 CSRF 以及授权规则。
实现 /register、/authenticate、/refresh 和 /logout 端点。在成功认证后返回访问令牌和刷新令牌。
将刷新令牌存储在数据库中,包含过期和撤销状态。为实现增强安全性,实施令牌轮换。
将带有基于角色(hasRole)或基于权限(hasAuthority)检查的 @PreAuthorize 注解应用于受保护的端点。
编写测试用例,涵盖认证成功/失败、授权访问控制和令牌验证场景。
# application.yml
spring:
security:
oauth2:
client:
registration:
google:
client-id: ${GOOGLE_CLIENT_ID}
client-secret: ${GOOGLE_CLIENT_SECRET}
scope: openid, profile, email
jwt:
secret: ${JWT_SECRET:my-very-secret-key-that-is-at-least-256-bits-long}
access-token-expiration: 86400000 # 24 hours in milliseconds
refresh-token-expiration: 604800000 # 7 days in milliseconds
issuer: spring-boot-jwt-example
cookie-name: jwt-token
cookie-secure: false # Set to true in production with HTTPS
cookie-http-only: true
cookie-same-site: lax
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthFilter;
private final AuthenticationProvider authenticationProvider;
private final LogoutHandler logoutHandler;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api-docs/**").permitAll()
.requestMatchers(HttpMethod.GET, "/swagger-ui/**").permitAll()
.anyRequest().authenticated()
)
.authenticationProvider(authenticationProvider)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.logout(logout -> logout
.logoutUrl("/api/auth/logout")
.addLogoutHandler(logoutHandler)
.logoutSuccessHandler((request, response, authentication) ->
SecurityContextHolder.clearContext())
);
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(List.of("*"));
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(List.of("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
@Service
@RequiredArgsConstructor
@Slf4j
public class JwtService {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.access-token-expiration}")
private long accessTokenExpiration;
@Value("${jwt.refresh-token-expiration}")
private long refreshTokenExpiration;
@Value("${jwt.issuer}")
private String issuer;
private final RefreshTokenService refreshTokenService;
/**
* 为用户生成访问令牌
*/
public String generateAccessToken(UserDetails userDetails) {
return generateToken(userDetails, accessTokenExpiration);
}
/**
* 为用户生成刷新令牌
*/
public String generateRefreshToken(UserDetails userDetails) {
return refreshTokenService.createRefreshToken(userDetails.getUsername());
}
/**
* 从 JWT 令牌中提取用户名
*/
public String extractUsername(String token) {
return extractClaims(token).getSubject();
}
/**
* 从 JWT 令牌中提取声明
*/
private Claims extractClaims(String token) {
return Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
}
/**
* 验证 JWT 令牌
*/
public boolean isTokenValid(String token, UserDetails userDetails) {
try {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) &&
!isTokenExpired(token) &&
extractClaims(token).getIssuer().equals(issuer));
} catch (JwtException | IllegalArgumentException e) {
log.debug("Invalid JWT token: {}", e.getMessage());
return false;
}
}
/**
* 检查令牌是否过期
*/
private boolean isTokenExpired(String token) {
return extractClaims(token).getExpiration().before(new Date());
}
/**
* 生成带过期时间的令牌
*/
private String generateToken(UserDetails userDetails, long expiration) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration);
return Jwts.builder()
.setSubject(userDetails.getUsername())
.setIssuer(issuer)
.setIssuedAt(now)
.setExpiration(expiryDate)
.claim("authorities", getAuthorities(userDetails))
.claim("type", "access")
.signWith(getSigningKey())
.compact();
}
/**
* 从密钥获取签名密钥
*/
private SecretKey getSigningKey() {
byte[] keyBytes = secret.getBytes(StandardCharsets.UTF_8);
return Keys.hmacShaKeyFor(keyBytes);
}
/**
* 从用户详情中提取权限
*/
private List<String> getAuthorities(UserDetails userDetails) {
return userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList());
}
}
@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 userEmail;
// 检查 Bearer 令牌
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
// 检查 JWT Cookie
String jwtCookie = WebUtils.getCookie(request, "jwt-token") != null
? WebUtils.getCookie(request, "jwt-token").getValue()
: null;
if (jwtCookie != null) {
jwt = jwtCookie;
userEmail = jwtService.extractUsername(jwt);
if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(userEmail);
if (jwtService.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);
return;
}
jwt = authHeader.substring(7);
userEmail = jwtService.extractUsername(jwt);
if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(userEmail);
if (jwtService.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);
}
}
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthFilter;
private final AuthenticationProvider authenticationProvider;
private final LogoutHandler logoutHandler;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
// 公共端点
.requestMatchers("/api/v1/auth/**").permitAll()
.requestMatchers("/api/v1/oauth2/**").permitAll()
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
.requestMatchers("/health").permitAll()
// 管理员端点
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
// 受保护的端点
.anyRequest().authenticated()
)
.sessionManagement(sess -> sess
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authenticationProvider(authenticationProvider)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.oauth2Login(oauth2 -> oauth2
.loginPage("/oauth2/authorization/google")
.defaultSuccessUrl("/api/v1/auth/oauth2/success", true)
.failureUrl("/api/v1/auth/oauth2/failure")
)
.logout(logout -> logout
.logoutUrl("/api/v1/auth/logout")
.addLogoutHandler(logoutHandler)
.logoutSuccessHandler((request, response, authentication) ->
SecurityContextHolder.clearContext())
);
return http.build();
}
@Bean
public AuthenticationProvider authenticationProvider(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder);
return authProvider;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}
@RestController
@RequestMapping("/api/v1/auth")
@RequiredArgsConstructor
@Slf4j
public class AuthenticationController {
private final AuthenticationService authenticationService;
@PostMapping("/register")
public ResponseEntity<AuthenticationResponse> register(
@Valid @RequestBody RegisterRequest request) {
log.info("Registering new user: {}", request.getEmail());
return ResponseEntity.ok(authenticationService.register(request));
}
@PostMapping("/authenticate")
public ResponseEntity<AuthenticationResponse> authenticate(
@Valid @RequestBody AuthenticationRequest request) {
log.info("Authenticating user: {}", request.getEmail());
AuthenticationResponse response = authenticationService.authenticate(request);
return ResponseEntity.ok()
.header("Set-Cookie", createJwtCookie(response.getAccessToken()))
.body(response);
}
@PostMapping("/refresh")
public ResponseEntity<AuthenticationResponse> refreshToken(
@RequestBody RefreshTokenRequest request) {
log.info("Refreshing token for user");
return ResponseEntity.ok(authenticationService.refreshToken(request));
}
@GetMapping("/me")
public ResponseEntity<UserProfile> getCurrentUser() {
return ResponseEntity.ok(authenticationService.getCurrentUser());
}
private String createJwtCookie(String token) {
return String.format(
"jwt-token=%s; Path=/; HttpOnly; SameSite=Lax; Max-Age=%d",
token,
86400 // 24 hours
);
}
}
@RestController
@RequestMapping("/api/v1/admin")
@RequiredArgsConstructor
@PreAuthorize("hasRole('ADMIN')")
public class AdminController {
private final AdminService adminService;
@GetMapping("/users")
@PreAuthorize("hasAuthority('ADMIN_READ')")
public ResponseEntity<Page<UserResponse>> getAllUsers(Pageable pageable) {
return ResponseEntity.ok(adminService.getAllUsers(pageable));
}
@DeleteMapping("/users/{id}")
@PreAuthorize("hasAuthority('ADMIN_DELETE')")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
adminService.deleteUser(id);
return ResponseEntity.noContent().build();
}
@PostMapping("/users/{id}/roles")
@PreAuthorize("hasAuthority('ADMIN_MANAGE_ROLES')")
public ResponseEntity<UserResponse> assignRole(
@PathVariable Long id,
@Valid @RequestBody AssignRoleRequest request) {
return ResponseEntity.ok(adminService.assignRole(id, request));
}
}
@Service
@RequiredArgsConstructor
public class DocumentService {
@PreAuthorize("hasPermission(#documentId, 'Document', 'READ')")
public Document getDocument(Long documentId) {
return documentRepository.findById(documentId)
.orElseThrow(() -> new DocumentNotFoundException(documentId));
}
@PreAuthorize("hasPermission(#documentId, 'Document', 'WRITE') or hasRole('ADMIN')")
public Document updateDocument(Long documentId, UpdateDocumentRequest request) {
Document document = getDocument(documentId);
document.setContent(request.content());
return documentRepository.save(document);
}
@PreAuthorize("@documentSecurityService.canAccess(#userEmail, #documentId)")
public Document shareDocument(String userEmail, Long documentId) {
// Implementation
}
}
@Component
@RequiredArgsConstructor
public class DocumentPermissionEvaluator implements PermissionEvaluator {
private final DocumentRepository documentRepository;
@Override
public boolean hasPermission(
Authentication authentication,
Object targetDomainObject,
Object permission) {
if (authentication == null || !(targetDomainObject instanceof Document)) {
return false;
}
Document document = (Document) targetDomainObject;
String username = authentication.getName();
String requiredPermission = (String) permission;
// 管理员可以做任何事情
if (hasRole(authentication, "ADMIN")) {
return true;
}
// 所有者可以读写
if (document.getOwner().getUsername().equals(username)) {
return "READ".equals(requiredPermission) || "WRITE".equals(requiredPermission);
}
// 检查共享权限
return document.getSharedWith().stream()
.anyMatch(share -> share.getUser().getUsername().equals(username)
&& share.getPermission().name().equals(requiredPermission));
}
@Override
public boolean hasPermission(
Authentication authentication,
Serializable targetId,
String targetType,
Object permission) {
if (!"Document".equals(targetType)) {
return false;
}
Document document = documentRepository.findById((Long) targetId).orElse(null);
return document != null && hasPermission(authentication, document, permission);
}
private boolean hasRole(Authentication authentication, String role) {
return authentication.getAuthorities().stream()
.anyMatch(auth -> auth.getAuthority().equals("ROLE_" + role));
}
}
@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String username;
@Column(unique = true, nullable = false)
private String email;
@Column(nullable = false)
private String password;
@Builder.Default
@Enumerated(EnumType.STRING)
private Role role = Role.USER;
@Builder.Default
private boolean enabled = true;
@Builder.Default
private boolean accountNonExpired = true;
@Builder.Default
private boolean accountNonLocked = true;
@Builder.Default
private boolean credentialsNonExpired = true;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Set<RefreshToken> refreshTokens = new HashSet<>();
@CreationTimestamp
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}
@Entity
@Table(name = "refresh_tokens")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class RefreshToken {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String token;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@Builder.Default
private boolean revoked = false;
@Builder.Default
private boolean expired = false;
@Column(name = "expiry_date")
private LocalDateTime expiryDate;
@CreationTimestamp
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
}
@SpringBootTest
@AutoConfigureMockMvc
@TestPropertySource(properties = {
"jwt.secret=test-secret-key-for-testing-only",
"jwt.access-token-expiration=3600000"
})
class AuthenticationControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Autowired
private JwtService jwtService;
@Test
void shouldAuthenticateUser() throws Exception {
AuthenticationRequest request = AuthenticationRequest.builder()
.email("test@example.com")
.password("password123")
.build();
mockMvc.perform(post("/api/v1/auth/authenticate")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.accessToken").exists())
.andExpect(jsonPath("$.refreshToken").exists())
.andExpect(jsonPath("$.user.email").value("test@example.com"));
}
@Test
void shouldDenyAccessWithoutToken() throws Exception {
mockMvc.perform(get("/api/v1/admin/users"))
.andExpect(status().isUnauthorized());
}
@Test
@WithMockUser(roles = "ADMIN")
void shouldAllowAdminAccess() throws Exception {
mockMvc.perform(get("/api/v1/admin/users"))
.andExpect(status().isOk());
}
@Test
void shouldValidateJwtToken() throws Exception {
UserDetails userDetails = User.withUsername("test@example.com")
.password("password")
.roles("USER")
.build();
String token = jwtService.generateAccessToken(userDetails);
mockMvc.perform(get("/api/v1/auth/me")
.header("Authorization", "Bearer " + token))
.andExpect(status().isOk());
}
}
@Component
@RequiredArgsConstructor
public class JwtKeyRotationService {
private final SecretKeyRepository keyRepository;
private final CacheManager cacheManager;
@Scheduled(cron = "0 0 0 * * ?") // Daily at midnight
public void rotateKeys() {
SecretKey newKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);
keyRepository.save(new SecretKeyEntity(newKey, LocalDateTime.now()));
cacheManager.getCache("jwt-keys").clear();
}
}
@Service
@RequiredArgsConstructor
public class TokenBlacklistService {
private final RedisTemplate<string, string> redisTemplate;
private static final String BLACKLIST_PREFIX = "blacklist:jwt:";
public void blacklistToken(String token, long expirationTime) {
String tokenId = extractTokenId(token);
redisTemplate.opsForValue().set(
BLACKLIST_PREFIX + tokenId,
"1",
expirationTime,
TimeUnit.MILLISECONDS
);
}
public boolean isBlacklisted(String token) {
String tokenId = extractTokenId(token);
return Boolean.TRUE.equals(redisTemplate.hasKey(BLACKLIST_PREFIX + tokenId));
}
}
HttpOnly、Secure、SameSite// 实现刷新令牌轮换
public class RefreshTokenService {
@Transactional
public String rotateRefreshToken(String oldToken) {
RefreshToken refreshToken = refreshTokenRepository.findByToken(oldToken)
.orElseThrow(() -> new RefreshTokenException("Invalid refresh token"));
// 撤销旧令牌
refreshToken.setRevoked(true);
refreshTokenRepository.save(refreshToken);
// 生成新令牌
return createRefreshToken(refreshToken.getUser().getUsername());
}
}
// 缓存用户详情以避免每次请求都访问数据库
@Service
@RequiredArgsConstructor
public class CachedUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
private final CacheManager cacheManager;
@Override
@Cacheable(value = "users", key = "#username")
public UserDetails loadUserByUsername(String username) {
User user = userRepository.findByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException(username));
return new CustomUserDetails(user);
}
}
@Component
@RequiredArgsConstructor
@Slf4j
public class SecurityAuditService {
@EventListener
public void handleAuthenticationSuccess(AuthenticationSuccessEvent event) {
log.info("Authentication success for user: {}", event.getAuthentication().getName());
// 存储审计事件
}
@EventListener
public void handleAuthenticationFailure(AuthenticationFailureEvent event) {
log.warn("Authentication failure for user: {}", event.getAuthentication().getName());
// 存储安全事件
}
@EventListener
public void handleAuthorizationFailure(AuthorizationFailureEvent event) {
log.warn("Authorization denied for user: {} on resource: {}",
event.getAuthentication().getName(),
event.getConfigAttributes());
}
}
HttpOnly、Secure、SameSiteiss)和受众(aud)声明{
"email": "user@example.com",
"password": "SecurePass123!"
}
{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"tokenType": "Bearer",
"expiresIn": 86400,
"user": {
"id": 1,
"email": "user@example.com",
"role": "USER"
}
}
{
"username": "newuser",
"email": "newuser@example.com",
"password": "SecurePass123!",
"confirmPassword": "SecurePass123!"
}
{
"id": 2,
"username": "newuser",
"email": "newuser@example.com",
"role": "USER",
"createdAt": "2024-01-15T10:30:00Z"
}
curl -X GET http://localhost:8080/api/v1/orders
{
"timestamp": "2024-01-15T10:35:00Z",
"status": 401,
"error": "Unauthorized",
"message": "Full authentication is required to access this resource"
}
curl -X GET http://localhost:8080/api/v1/orders \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
{
"content": [
{
"id": 1,
"orderNumber": "ORD-001",
"status": "COMPLETED",
"total": 99.99
}
],
"pageable": {
"page": 0,
"size": 20,
"total": 1
}
}
{
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"tokenType": "Bearer",
"expiresIn": 86400
}
Comprehensive JWT (JSON Web Token) authentication and authorization patterns for Spring Boot 3.5.x applications using Spring Security 6.x and the JJWT library. This skill provides production-ready implementations for stateless authentication, role-based access control, and integration with modern authentication providers.
JWT authentication enables stateless, scalable security for Spring Boot applications. This skill covers complete JWT lifecycle management including token generation, validation, refresh strategies, and integration patterns with database-backed and OAuth2 authentication providers. Implementations follow Spring Security 6.x best practices with modern SecurityFilterChain configuration.
Use this skill when:
<dependencies>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</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-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<!-- JWT Library -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.6</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
<!-- Database -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
dependencies {
// Spring Security
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server")
// JWT Library
implementation("io.jsonwebtoken:jjwt-api:0.12.6")
implementation("io.jsonwebtoken:jjwt-impl:0.12.6")
implementation("io.jsonwebtoken:jjwt-jackson:0.12.6")
// Database
runtimeOnly("com.h2database:h2")
runtimeOnly("org.postgresql:postgresql")
// Testing
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.security:spring-security-test")
testImplementation("org.testcontainers:junit-jupiter")
}
Follow these steps to implement JWT authentication in Spring Boot:
Include spring-boot-starter-security, spring-boot-starter-oauth2-resource-server, and JJWT library (jjwt-api, jjwt-impl, jjwt-jackson) in your project.
Set JWT secret, access token expiration, refresh token expiration, and issuer in application.yml. Never hardcode secrets in version control.
Implement JwtService with methods to generate tokens, extract claims, validate tokens, and check expiration. Use Jwts.builder() for token creation.
Create JwtAuthenticationFilter extending OncePerRequestFilter. Extract JWT from Authorization header or cookie, validate it, and set SecurityContext authentication.
Set up SecurityConfig with @EnableWebSecurity and @EnableMethodSecurity. Configure stateless session management, CSRF disabled, and authorization rules.
Implement /register, /authenticate, /refresh, and /logout endpoints. Return access and refresh tokens on successful authentication.
Store refresh tokens in database with expiration and revocation status. Implement token rotation for enhanced security.
Apply @PreAuthorize annotations with role-based (hasRole) or permission-based (hasAuthority) checks to protected endpoints.
Write tests for authentication success/failure, authorization access control, and token validation scenarios.
# application.yml
spring:
security:
oauth2:
client:
registration:
google:
client-id: ${GOOGLE_CLIENT_ID}
client-secret: ${GOOGLE_CLIENT_SECRET}
scope: openid, profile, email
jwt:
secret: ${JWT_SECRET:my-very-secret-key-that-is-at-least-256-bits-long}
access-token-expiration: 86400000 # 24 hours in milliseconds
refresh-token-expiration: 604800000 # 7 days in milliseconds
issuer: spring-boot-jwt-example
cookie-name: jwt-token
cookie-secure: false # Set to true in production with HTTPS
cookie-http-only: true
cookie-same-site: lax
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthFilter;
private final AuthenticationProvider authenticationProvider;
private final LogoutHandler logoutHandler;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api-docs/**").permitAll()
.requestMatchers(HttpMethod.GET, "/swagger-ui/**").permitAll()
.anyRequest().authenticated()
)
.authenticationProvider(authenticationProvider)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.logout(logout -> logout
.logoutUrl("/api/auth/logout")
.addLogoutHandler(logoutHandler)
.logoutSuccessHandler((request, response, authentication) ->
SecurityContextHolder.clearContext())
);
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(List.of("*"));
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(List.of("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
@Service
@RequiredArgsConstructor
@Slf4j
public class JwtService {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.access-token-expiration}")
private long accessTokenExpiration;
@Value("${jwt.refresh-token-expiration}")
private long refreshTokenExpiration;
@Value("${jwt.issuer}")
private String issuer;
private final RefreshTokenService refreshTokenService;
/**
* Generate access token for user
*/
public String generateAccessToken(UserDetails userDetails) {
return generateToken(userDetails, accessTokenExpiration);
}
/**
* Generate refresh token for user
*/
public String generateRefreshToken(UserDetails userDetails) {
return refreshTokenService.createRefreshToken(userDetails.getUsername());
}
/**
* Extract username from JWT token
*/
public String extractUsername(String token) {
return extractClaims(token).getSubject();
}
/**
* Extract claims from JWT token
*/
private Claims extractClaims(String token) {
return Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
}
/**
* Validate JWT token
*/
public boolean isTokenValid(String token, UserDetails userDetails) {
try {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) &&
!isTokenExpired(token) &&
extractClaims(token).getIssuer().equals(issuer));
} catch (JwtException | IllegalArgumentException e) {
log.debug("Invalid JWT token: {}", e.getMessage());
return false;
}
}
/**
* Check if token is expired
*/
private boolean isTokenExpired(String token) {
return extractClaims(token).getExpiration().before(new Date());
}
/**
* Generate token with expiration
*/
private String generateToken(UserDetails userDetails, long expiration) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration);
return Jwts.builder()
.setSubject(userDetails.getUsername())
.setIssuer(issuer)
.setIssuedAt(now)
.setExpiration(expiryDate)
.claim("authorities", getAuthorities(userDetails))
.claim("type", "access")
.signWith(getSigningKey())
.compact();
}
/**
* Get signing key from secret
*/
private SecretKey getSigningKey() {
byte[] keyBytes = secret.getBytes(StandardCharsets.UTF_8);
return Keys.hmacShaKeyFor(keyBytes);
}
/**
* Extract authorities from user details
*/
private List<String> getAuthorities(UserDetails userDetails) {
return userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList());
}
}
@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 userEmail;
// Check for Bearer token
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
// Check for JWT cookie
String jwtCookie = WebUtils.getCookie(request, "jwt-token") != null
? WebUtils.getCookie(request, "jwt-token").getValue()
: null;
if (jwtCookie != null) {
jwt = jwtCookie;
userEmail = jwtService.extractUsername(jwt);
if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(userEmail);
if (jwtService.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);
return;
}
jwt = authHeader.substring(7);
userEmail = jwtService.extractUsername(jwt);
if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(userEmail);
if (jwtService.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);
}
}
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthFilter;
private final AuthenticationProvider authenticationProvider;
private final LogoutHandler logoutHandler;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
// Public endpoints
.requestMatchers("/api/v1/auth/**").permitAll()
.requestMatchers("/api/v1/oauth2/**").permitAll()
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
.requestMatchers("/health").permitAll()
// Admin endpoints
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
// Protected endpoints
.anyRequest().authenticated()
)
.sessionManagement(sess -> sess
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authenticationProvider(authenticationProvider)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.oauth2Login(oauth2 -> oauth2
.loginPage("/oauth2/authorization/google")
.defaultSuccessUrl("/api/v1/auth/oauth2/success", true)
.failureUrl("/api/v1/auth/oauth2/failure")
)
.logout(logout -> logout
.logoutUrl("/api/v1/auth/logout")
.addLogoutHandler(logoutHandler)
.logoutSuccessHandler((request, response, authentication) ->
SecurityContextHolder.clearContext())
);
return http.build();
}
@Bean
public AuthenticationProvider authenticationProvider(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder);
return authProvider;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}
@RestController
@RequestMapping("/api/v1/auth")
@RequiredArgsConstructor
@Slf4j
public class AuthenticationController {
private final AuthenticationService authenticationService;
@PostMapping("/register")
public ResponseEntity<AuthenticationResponse> register(
@Valid @RequestBody RegisterRequest request) {
log.info("Registering new user: {}", request.getEmail());
return ResponseEntity.ok(authenticationService.register(request));
}
@PostMapping("/authenticate")
public ResponseEntity<AuthenticationResponse> authenticate(
@Valid @RequestBody AuthenticationRequest request) {
log.info("Authenticating user: {}", request.getEmail());
AuthenticationResponse response = authenticationService.authenticate(request);
return ResponseEntity.ok()
.header("Set-Cookie", createJwtCookie(response.getAccessToken()))
.body(response);
}
@PostMapping("/refresh")
public ResponseEntity<AuthenticationResponse> refreshToken(
@RequestBody RefreshTokenRequest request) {
log.info("Refreshing token for user");
return ResponseEntity.ok(authenticationService.refreshToken(request));
}
@GetMapping("/me")
public ResponseEntity<UserProfile> getCurrentUser() {
return ResponseEntity.ok(authenticationService.getCurrentUser());
}
private String createJwtCookie(String token) {
return String.format(
"jwt-token=%s; Path=/; HttpOnly; SameSite=Lax; Max-Age=%d",
token,
86400 // 24 hours
);
}
}
@RestController
@RequestMapping("/api/v1/admin")
@RequiredArgsConstructor
@PreAuthorize("hasRole('ADMIN')")
public class AdminController {
private final AdminService adminService;
@GetMapping("/users")
@PreAuthorize("hasAuthority('ADMIN_READ')")
public ResponseEntity<Page<UserResponse>> getAllUsers(Pageable pageable) {
return ResponseEntity.ok(adminService.getAllUsers(pageable));
}
@DeleteMapping("/users/{id}")
@PreAuthorize("hasAuthority('ADMIN_DELETE')")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
adminService.deleteUser(id);
return ResponseEntity.noContent().build();
}
@PostMapping("/users/{id}/roles")
@PreAuthorize("hasAuthority('ADMIN_MANAGE_ROLES')")
public ResponseEntity<UserResponse> assignRole(
@PathVariable Long id,
@Valid @RequestBody AssignRoleRequest request) {
return ResponseEntity.ok(adminService.assignRole(id, request));
}
}
@Service
@RequiredArgsConstructor
public class DocumentService {
@PreAuthorize("hasPermission(#documentId, 'Document', 'READ')")
public Document getDocument(Long documentId) {
return documentRepository.findById(documentId)
.orElseThrow(() -> new DocumentNotFoundException(documentId));
}
@PreAuthorize("hasPermission(#documentId, 'Document', 'WRITE') or hasRole('ADMIN')")
public Document updateDocument(Long documentId, UpdateDocumentRequest request) {
Document document = getDocument(documentId);
document.setContent(request.content());
return documentRepository.save(document);
}
@PreAuthorize("@documentSecurityService.canAccess(#userEmail, #documentId)")
public Document shareDocument(String userEmail, Long documentId) {
// Implementation
}
}
@Component
@RequiredArgsConstructor
public class DocumentPermissionEvaluator implements PermissionEvaluator {
private final DocumentRepository documentRepository;
@Override
public boolean hasPermission(
Authentication authentication,
Object targetDomainObject,
Object permission) {
if (authentication == null || !(targetDomainObject instanceof Document)) {
return false;
}
Document document = (Document) targetDomainObject;
String username = authentication.getName();
String requiredPermission = (String) permission;
// Admin can do anything
if (hasRole(authentication, "ADMIN")) {
return true;
}
// Owner can read and write
if (document.getOwner().getUsername().equals(username)) {
return "READ".equals(requiredPermission) || "WRITE".equals(requiredPermission);
}
// Check shared permissions
return document.getSharedWith().stream()
.anyMatch(share -> share.getUser().getUsername().equals(username)
&& share.getPermission().name().equals(requiredPermission));
}
@Override
public boolean hasPermission(
Authentication authentication,
Serializable targetId,
String targetType,
Object permission) {
if (!"Document".equals(targetType)) {
return false;
}
Document document = documentRepository.findById((Long) targetId).orElse(null);
return document != null && hasPermission(authentication, document, permission);
}
private boolean hasRole(Authentication authentication, String role) {
return authentication.getAuthorities().stream()
.anyMatch(auth -> auth.getAuthority().equals("ROLE_" + role));
}
}
@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String username;
@Column(unique = true, nullable = false)
private String email;
@Column(nullable = false)
private String password;
@Builder.Default
@Enumerated(EnumType.STRING)
private Role role = Role.USER;
@Builder.Default
private boolean enabled = true;
@Builder.Default
private boolean accountNonExpired = true;
@Builder.Default
private boolean accountNonLocked = true;
@Builder.Default
private boolean credentialsNonExpired = true;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Set<RefreshToken> refreshTokens = new HashSet<>();
@CreationTimestamp
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}
@Entity
@Table(name = "refresh_tokens")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class RefreshToken {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String token;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@Builder.Default
private boolean revoked = false;
@Builder.Default
private boolean expired = false;
@Column(name = "expiry_date")
private LocalDateTime expiryDate;
@CreationTimestamp
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
}
@SpringBootTest
@AutoConfigureMockMvc
@TestPropertySource(properties = {
"jwt.secret=test-secret-key-for-testing-only",
"jwt.access-token-expiration=3600000"
})
class AuthenticationControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Autowired
private JwtService jwtService;
@Test
void shouldAuthenticateUser() throws Exception {
AuthenticationRequest request = AuthenticationRequest.builder()
.email("test@example.com")
.password("password123")
.build();
mockMvc.perform(post("/api/v1/auth/authenticate")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.accessToken").exists())
.andExpect(jsonPath("$.refreshToken").exists())
.andExpect(jsonPath("$.user.email").value("test@example.com"));
}
@Test
void shouldDenyAccessWithoutToken() throws Exception {
mockMvc.perform(get("/api/v1/admin/users"))
.andExpect(status().isUnauthorized());
}
@Test
@WithMockUser(roles = "ADMIN")
void shouldAllowAdminAccess() throws Exception {
mockMvc.perform(get("/api/v1/admin/users"))
.andExpect(status().isOk());
}
@Test
void shouldValidateJwtToken() throws Exception {
UserDetails userDetails = User.withUsername("test@example.com")
.password("password")
.roles("USER")
.build();
String token = jwtService.generateAccessToken(userDetails);
mockMvc.perform(get("/api/v1/auth/me")
.header("Authorization", "Bearer " + token))
.andExpect(status().isOk());
}
}
@Component
@RequiredArgsConstructor
public class JwtKeyRotationService {
private final SecretKeyRepository keyRepository;
private final CacheManager cacheManager;
@Scheduled(cron = "0 0 0 * * ?") // Daily at midnight
public void rotateKeys() {
SecretKey newKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);
keyRepository.save(new SecretKeyEntity(newKey, LocalDateTime.now()));
cacheManager.getCache("jwt-keys").clear();
}
}
@Service
@RequiredArgsConstructor
public class TokenBlacklistService {
private final RedisTemplate<string, string> redisTemplate;
private static final String BLACKLIST_PREFIX = "blacklist:jwt:";
public void blacklistToken(String token, long expirationTime) {
String tokenId = extractTokenId(token);
redisTemplate.opsForValue().set(
BLACKLIST_PREFIX + tokenId,
"1",
expirationTime,
TimeUnit.MILLISECONDS
);
}
public boolean isBlacklisted(String token) {
String tokenId = extractTokenId(token);
return Boolean.TRUE.equals(redisTemplate.hasKey(BLACKLIST_PREFIX + tokenId));
}
}
HttpOnly, Secure, SameSite// Implement refresh token rotation
public class RefreshTokenService {
@Transactional
public String rotateRefreshToken(String oldToken) {
RefreshToken refreshToken = refreshTokenRepository.findByToken(oldToken)
.orElseThrow(() -> new RefreshTokenException("Invalid refresh token"));
// Revoke old token
refreshToken.setRevoked(true);
refreshTokenRepository.save(refreshToken);
// Generate new token
return createRefreshToken(refreshToken.getUser().getUsername());
}
}
// Cache user details to avoid database hits
@Service
@RequiredArgsConstructor
public class CachedUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
private final CacheManager cacheManager;
@Override
@Cacheable(value = "users", key = "#username")
public UserDetails loadUserByUsername(String username) {
User user = userRepository.findByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException(username));
return new CustomUserDetails(user);
}
}
@Component
@RequiredArgsConstructor
@Slf4j
public class SecurityAuditService {
@EventListener
public void handleAuthenticationSuccess(AuthenticationSuccessEvent event) {
log.info("Authentication success for user: {}", event.getAuthentication().getName());
// Store audit event
}
@EventListener
public void handleAuthenticationFailure(AuthenticationFailureEvent event) {
log.warn("Authentication failure for user: {}", event.getAuthentication().getName());
// Store security event
}
@EventListener
public void handleAuthorizationFailure(AuthorizationFailureEvent event) {
log.warn("Authorization denied for user: {} on resource: {}",
event.getAuthentication().getName(),
event.getConfigAttributes());
}
}
HttpOnly, Secure, SameSiteiss) and audience (aud) claims{
"email": "user@example.com",
"password": "SecurePass123!"
}
{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"tokenType": "Bearer",
"expiresIn": 86400,
"user": {
"id": 1,
"email": "user@example.com",
"role": "USER"
}
}
{
"username": "newuser",
"email": "newuser@example.com",
"password": "SecurePass123!",
"confirmPassword": "SecurePass123!"
}
{
"id": 2,
"username": "newuser",
"email": "newuser@example.com",
"role": "USER",
"createdAt": "2024-01-15T10:30:00Z"
}
curl -X GET http://localhost:8080/api/v1/orders
{
"timestamp": "2024-01-15T10:35:00Z",
"status": 401,
"error": "Unauthorized",
"message": "Full authentication is required to access this resource"
}
curl -X GET http://localhost:8080/api/v1/orders \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
{
"content": [
{
"id": 1,
"orderNumber": "ORD-001",
"status": "COMPLETED",
"total": 99.99
}
],
"pageable": {
"page": 0,
"size": 20,
"total": 1
}
}
{
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"tokenType": "Bearer",
"expiresIn": 86400
}
spring-boot-dependency-injection - Constructor injection patterns used throughoutspring-boot-rest-api-standards - REST API security patterns and error handlingunit-test-security-authorization - Testing Spring Security configurationsspring-data-jpa - User entity and repository patternsspring-boot-actuator - Security monitoring and health endpointsWeekly Installs
448
Repository
GitHub Stars
173
First Seen
Feb 3, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
gemini-cli357
opencode355
codex353
cursor341
github-copilot334
claude-code332
Linux云主机安全托管指南:从SSH加固到HTTPS部署
27,400 周安装