| name | spring-boot-security-jwt |
| description | JWT authentication and authorization patterns for Spring Boot 3.5.x covering token generation with JJWT, Bearer/cookie authentication, database/OAuth2 integration, and RBAC/permission-based access control using Spring Security 6.x. |
| allowed-tools | Read, Write, Bash |
| category | backend |
| tags | ["spring-boot","spring-security","jwt","authentication","authorization","rbac","oauth2","jjwt"] |
| version | 1.0.0 |
Spring Boot JWT Security
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.
Overview
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.
When to Use
Use this skill when:
- Implementing stateless authentication for REST APIs
- Building SPA (Single Page Application) backends with JWT
- Securing microservices with token-based authentication
- Integrating with OAuth2 providers (Google, GitHub, etc.)
- Implementing role-based or permission-based access control
- Setting up JWT refresh token strategies
- Migrating from session-based to token-based authentication
- Building mobile API backends
- Implementing cross-origin authentication with CORS
Prerequisites
- Java 17+ (for records and pattern matching)
- Spring Boot 3.5.x (for Spring Security 6.x integration)
- JJWT library (io.jsonwebtoken) for JWT operations
- Maven or Gradle build system
- Basic understanding of Spring Security concepts
Dependencies
Maven
<dependencies>
<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>
spring-boot-starter-oauth2-resource-server
io.jsonwebtoken
jjwt-api
0.12.6
io.jsonwebtoken
jjwt-impl
0.12.6
runtime
io.jsonwebtoken
jjwt-jackson
0.12.6
runtime
com.h2database
h2
runtime
org.postgresql
postgresql
runtime
org.springframework.boot
spring-boot-starter-test
test
org.springframework.security
spring-security-test
test
org.testcontainers
junit-jupiter
test
org.springframework.security
spring-security-test
test
Gradle
dependencies {
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")
implementation("io.jsonwebtoken:jjwt-api:0.12.6")
implementation("io.jsonwebtoken:jjwt-impl:0.12.6")
implementation("io.jsonwebtoken:jjwt-jackson:0.12.6")
runtimeOnly("com.h2database:h2")
runtimeOnly("org.postgresql:postgresql")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.security:spring-security-test")
testImplementation("org.testcontainers:junit-jupiter")
}
Quick Start
1. Application Configuration
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
refresh-token-expiration: 604800000
issuer: spring-boot-jwt-example
cookie-name: jwt-token
cookie-secure: false
cookie-http-only: true
cookie-same-site: lax
2. Modern Spring Security 6.x Configuration
@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(, , , , ));
configuration.setAllowedHeaders(List.of());
configuration.setAllowCredentials();
();
source.registerCorsConfiguration(, configuration);
source;
}
}
3. JWT Service Implementation
@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());
}
public String extractUsername(String token) {
return extractClaims(token).getSubject();
}
private Claims extractClaims(String token) {
return Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
}
{
{
extractUsername(token);
(username.equals(userDetails.getUsername()) &&
!isTokenExpired(token) &&
extractClaims(token).getIssuer().equals(issuer));
} (JwtException | IllegalArgumentException e) {
log.debug(, e.getMessage());
;
}
}
{
extractClaims(token).getExpiration().before( ());
}
String {
();
(now.getTime() + expiration);
Jwts.builder()
.setSubject(userDetails.getUsername())
.setIssuer(issuer)
.setIssuedAt(now)
.setExpiration(expiryDate)
.claim(, getAuthorities(userDetails))
.claim(, )
.signWith(getSigningKey())
.compact();
}
SecretKey {
[] keyBytes = secret.getBytes(StandardCharsets.UTF_8);
Keys.hmacShaKeyFor(keyBytes);
}
List<String> {
userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList());
}
}
3. JWT Authentication Filter
@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;
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
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() == ) {
userDetailsService.loadUserByUsername(userEmail);
(jwtService.isTokenValid(jwt, userDetails)) {
(
userDetails,
,
userDetails.getAuthorities()
);
authToken.setDetails(
().buildDetails(request)
);
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
}
filterChain.doFilter(request, response);
;
}
jwt = authHeader.substring();
userEmail = jwtService.extractUsername(jwt);
(userEmail != && SecurityContextHolder.getContext().getAuthentication() == ) {
userDetailsService.loadUserByUsername(userEmail);
(jwtService.isTokenValid(jwt, userDetails)) {
(
userDetails,
,
userDetails.getAuthorities()
);
authToken.setDetails(
().buildDetails(request)
);
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
}
}
4. Security Configuration
@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())
);
http.build();
}
AuthenticationProvider {
();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder);
authProvider;
}
PasswordEncoder {
();
}
AuthenticationManager Exception {
config.getAuthenticationManager();
}
}
Authentication Controllers
@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 {
ResponseEntity.ok(authenticationService.getCurrentUser());
}
String {
String.format(
,
token,
);
}
}
Authorization Patterns
Role-Based Access Control (RBAC)
@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));
}
}
Permission-Based Access Control
@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) {
}
}
Custom Permission Evaluator
@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
{
(!.equals(targetType)) {
;
}
documentRepository.findById((Long) targetId).orElse();
document != && hasPermission(authentication, document, permission);
}
{
authentication.getAuthorities().stream()
.anyMatch(auth -> auth.getAuthority().equals( + role));
}
}
Database Entities
@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 = <>();
LocalDateTime createdAt;
LocalDateTime updatedAt;
}
{
Long id;
String token;
User user;
.Default
;
.Default
;
LocalDateTime expiryDate;
LocalDateTime createdAt;
}
Testing JWT Security
@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())
.andExpect(status().isOk());
}
Exception {
User.withUsername()
.password()
.roles()
.build();
jwtService.generateAccessToken(userDetails);
mockMvc.perform(get()
.header(, + token))
.andExpect(status().isOk());
}
}
Best Practices
1. Modern JWT Patterns
Key Rotation Strategy
@Component
@RequiredArgsConstructor
public class JwtKeyRotationService {
private final SecretKeyRepository keyRepository;
private final CacheManager cacheManager;
@Scheduled(cron = "0 0 0 * * ?")
public void rotateKeys() {
SecretKey newKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);
keyRepository.save(new SecretKeyEntity(newKey, LocalDateTime.now()));
cacheManager.getCache("jwt-keys").clear();
}
}
Token Blacklisting
@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));
}
}
2. Security Configuration
- Always use HTTPS in production for JWT token transmission
- Set appropriate cookie flags:
HttpOnly, Secure, SameSite
- Use strong secret keys: minimum 256 bits for HMAC algorithms
- Implement token expiration: Don't use tokens with infinite lifetime
- Validate all inputs: Never trust JWT claims without validation
- Implement key rotation: Regularly rotate signing keys
- Use token blacklisting: For logout and security incidents
2. Token Management
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());
}
}
3. Performance Optimization
@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);
}
}
4. Monitoring and Audit
@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());
}
}
Constraints
1. Token Size Limitations
- JWT tokens should stay under HTTP header size limits (typically 8KB)
- Avoid storing large amounts of data in JWT claims
- Use references instead of embedding complete objects
2. Security Considerations
- Never store sensitive information in JWT tokens
- Implement proper token revocation strategies
- Use different keys for different environments (dev, staging, prod)
- Regularly rotate signing keys
3. Rate Limiting
@RestController
@RequestMapping("/api/v1/auth")
@RequiredArgsConstructor
public class AuthenticationController {
@PostMapping("/authenticate")
@RateLimiter(name = "auth", fallbackMethod = "authenticateFallback")
public ResponseEntity<AuthenticationResponse> authenticate(@RequestBody AuthenticationRequest request) {
}
public ResponseEntity<AuthenticationResponse> authenticateFallback(
AuthenticationRequest request,
CallNotPermittedException exception) {
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).build();
}
}
4. CORS Configuration
@Configuration
public class CorsConfig {
@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;
}
}
Reference Materials
Related Skills
spring-boot-dependency-injection - Constructor injection patterns used throughout
spring-boot-rest-api-standards - REST API security patterns and error handling
unit-test-security-authorization - Testing Spring Security configurations
spring-data-jpa - User entity and repository patterns
spring-boot-actuator - Security monitoring and health endpoints