
Spring Boot Security Jwt
- 1.9k installs
- 311 repo stars
- Updated June 22, 2026
- giuseppe-trisciuoglio/developer-kit
spring-boot-security-jwt is an agent skill for Provides JWT authentication and authorization patterns for Spring Boot 3.5.x covering token generation with JJWT, Bearer
About
The spring-boot-security-jwt skill Provides 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. Use when implementing authentication or authorization in Spring Boot applications. It covers access and refresh token generation with configurable expiration. Key workflows include bearer token and HttpOnly cookie authentication strategies. This skill provides implementation patterns for stateless JWT authentication in Spring Boot applications. It covers the complete authentication flow including token generation with JJWT 0.12.6, Bearer/cookie-based authentication, refresh token rotation, and method-level authorization with @PreAuthorize expressions. Key capabilities: - Access and refresh token generation with configurable expiratio Developers invoke spring-boot-security-jwt when the task matches the triggers and reference files in SKILL.md for grounded, stepwise execution. Reference files and progressive disclosure keep context focused while preserving concrete commands, configuration fields, and validation.
- Access and refresh token generation with configurable expiration
- Bearer token and HttpOnly cookie authentication strategies
- Integration with Spring Data JPA and OAuth2 providers
- RBAC with role/permission-based @PreAuthorize rules
- Token revocation and blacklisting for logout/rotation
Spring Boot Security Jwt by the numbers
- 1,913 all-time installs (skills.sh)
- +61 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #292 of 2,209 Security skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
spring-boot-security-jwt capabilities & compatibility
- Capabilities
- access and refresh token generation with configu · bearer token and httponly cookie authentication · integration with spring data jpa and oauth2 prov · rbac with role/permission based @preauthorize ru · token revocation and blacklisting for logout/rot
- Use cases
- security audit · testing · debugging
What spring-boot-security-jwt says it does
allowed-tools: Read, Write, Edit, Bash, Glob, Grep
JWT authentication and authorization patterns for Spring Boot 3.5.x using Spring Security 6.x and JJWT. Covers token generation, validation, refresh strategies, RBAC/ABAC, and OAuth2 integration.
npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill spring-boot-security-jwtAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.9k |
|---|---|
| repo stars | ★ 311 |
| Security audit | 2 / 3 scanners passed |
| Last updated | June 22, 2026 |
| Repository | giuseppe-trisciuoglio/developer-kit ↗ |
What problem does spring-boot-security-jwt solve for developers using the documented workflows?
Provides 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 a
Who is it for?
Developers working with spring-boot-security-jwt patterns described in the skill documentation.
Skip if: Skip when docs are empty or the task is outside the skill documented scope.
When should I use this skill?
Use when Provides JWT authentication and authorization patterns for Spring Boot 3.5.x covering token generation with JJWT, Bearer/cookie authentication, database/OAuth2 integration, and RBA
What you get
Actionable spring-boot-security-jwt guidance grounded in SKILL.md workflows and reference files.
- SecurityFilterChain configuration
- JWT token service
- RBAC authorization rules
By the numbers
- Documents patterns for Spring Boot 3.5.x on Spring Security 6.x
- Covers JJWT token generation plus Bearer and cookie authentication modes
Files
Spring Boot JWT Security
JWT authentication and authorization patterns for Spring Boot 3.5.x using Spring Security 6.x and JJWT. Covers token generation, validation, refresh strategies, RBAC/ABAC, and OAuth2 integration.
Overview
This skill provides implementation patterns for stateless JWT authentication in Spring Boot applications. It covers the complete authentication flow including token generation with JJWT 0.12.6, Bearer/cookie-based authentication, refresh token rotation, and method-level authorization with @PreAuthorize expressions.
Key capabilities:
- Access and refresh token generation with configurable expiration
- Bearer token and HttpOnly cookie authentication strategies
- Integration with Spring Data JPA and OAuth2 providers
- RBAC with role/permission-based
@PreAuthorizerules - Token revocation and blacklisting for logout/rotation
When to Use
Activate when user requests involve:
- "Implement JWT authentication", "secure REST API with tokens"
- "Spring Security 6.x configuration", "SecurityFilterChain setup"
- "Role-based access control", "RBAC", `
@PreAuthorize` - "Refresh token", "token rotation", "token revocation"
- "OAuth2 integration", "social login", "Google/GitHub auth"
- "Stateless authentication", "SPA backend security"
- "JWT filter", "OncePerRequestFilter", "Bearer token"
- "Cookie-based JWT", "HttpOnly cookie"
- "Permission-based access control", "custom PermissionEvaluator"
Quick Reference
Dependencies (JJWT 0.12.6)
| Artifact | Scope |
|---|---|
spring-boot-starter-security | compile |
spring-boot-starter-oauth2-resource-server | compile |
io.jsonwebtoken:jjwt-api:0.12.6 | compile |
io.jsonwebtoken:jjwt-impl:0.12.6 | runtime |
io.jsonwebtoken:jjwt-jackson:0.12.6 | runtime |
spring-security-test | test |
See references/jwt-quick-reference.md for Maven and Gradle snippets.
Key Configuration Properties
| Property | Example Value | Notes |
|---|---|---|
jwt.secret | ${JWT_SECRET} | Min 256 bits, never hardcode |
jwt.access-token-expiration | 900000 | 15 min in milliseconds |
jwt.refresh-token-expiration | 604800000 | 7 days in milliseconds |
jwt.issuer | my-app | Validated on every token |
jwt.cookie-name | jwt-token | For cookie-based auth |
jwt.cookie-http-only | true | Always true in production |
jwt.cookie-secure | true | Always true with HTTPS |
Authorization Annotations
| Annotation | Example |
|---|---|
@PreAuthorize("hasRole('ADMIN')") | Role check |
@PreAuthorize("hasAuthority('USER_READ')") | Permission check |
@PreAuthorize("hasPermission(#id, 'Doc', 'READ')") | Domain object check |
@PreAuthorize("@myService.canAccess(#id)") | Spring bean check |
Instructions
Step 1 — Add Dependencies
Include spring-boot-starter-security, spring-boot-starter-oauth2-resource-server, and the three JJWT artifacts in your build file. See references/jwt-quick-reference.md for exact Maven/Gradle snippets.
Step 2 — Configure application.yml
jwt:
secret: ${JWT_SECRET:change-me-min-32-chars-in-production}
access-token-expiration: 900000
refresh-token-expiration: 604800000
issuer: my-app
cookie-name: jwt-token
cookie-http-only: true
cookie-secure: false # true in productionSee references/jwt-complete-configuration.md for the full properties reference.
Step 3 — Implement JwtService
Core operations: generate access token, generate refresh token, extract username, validate token.
@Service
public class JwtService {
public String generateAccessToken(UserDetails userDetails) {
return Jwts.builder()
.subject(userDetails.getUsername())
.issuer(issuer)
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + accessTokenExpiration))
.claim("authorities", getAuthorities(userDetails))
.signWith(getSigningKey())
.compact();
}
public boolean isTokenValid(String token, UserDetails userDetails) {
try {
String username = extractUsername(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
} catch (JwtException e) {
return false;
}
}
}See references/jwt-complete-configuration.md for the complete JwtService including key management and claim extraction.
Step 4 — Create JwtAuthenticationFilter
Extend OncePerRequestFilter to extract a JWT from the Authorization: Bearer header (or HttpOnly cookie), validate it, and set the SecurityContext.
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
chain.doFilter(request, response);
return;
}
String jwt = authHeader.substring(7);
String username = jwtService.extractUsername(jwt);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtService.isTokenValid(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
chain.doFilter(request, response);
}
}See references/configuration.md for the cookie-based variant.
Step 5 — Configure SecurityFilterChain
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**", "/swagger-ui/**").permitAll()
.anyRequest().authenticated()
)
.authenticationProvider(authenticationProvider)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
}See references/jwt-complete-configuration.md for CORS, logout handler, and OAuth2 login integration.
Step 6 — Create Authentication Endpoints
Expose /register, /authenticate, /refresh, and /logout via @RestController. Return accessToken + refreshToken in the response body (and optionally set an HttpOnly cookie).
See references/examples.md for the complete AuthenticationController and AuthenticationService.
Step 7 — Implement Refresh Token Strategy
Store refresh tokens in the database with user_id, expiry_date, revoked, and expired columns. On /refresh, verify the stored token, revoke it, and issue a new pair (token rotation).
See references/token-management.md for RefreshToken entity, rotation logic, and Redis-based blacklisting.
Step 8 — Add Authorization Rules
Use @EnableMethodSecurity and @PreAuthorize annotations for fine-grained control:
@PreAuthorize("hasRole('ADMIN')")
public Page<UserResponse> getAllUsers(Pageable pageable) { ... }
@PreAuthorize("hasPermission(#documentId, 'Document', 'READ')")
public Document getDocument(Long documentId) { ... }See references/authorization-patterns.md for RBAC entity model, PermissionEvaluator, and ABAC patterns.
Step 9 — Write Security Tests
@SpringBootTest
@AutoConfigureMockMvc
class AuthControllerTest {
@Test
void shouldDenyAccessWithoutToken() throws Exception {
mockMvc.perform(get("/api/orders"))
.andExpect(status().isUnauthorized());
}
@Test
@WithMockUser(roles = "ADMIN")
void shouldAllowAdminAccess() throws Exception {
mockMvc.perform(get("/api/admin/users"))
.andExpect(status().isOk());
}
}See references/testing.md and references/jwt-testing-guide.md for full test suites, Testcontainers setup, and a security test checklist.
Best Practices
Token Security
- Use minimum 256-bit secret keys — load from environment variables, never hardcode
- Set short access token lifetimes (15 min); use refresh tokens for longer sessions
- Implement token rotation: revoke old refresh token when issuing a new one
- Use
jti(JWT ID) claim for blacklisting on logout
Cookie vs Bearer Header
- Prefer HttpOnly cookies for browser clients (XSS-safe)
- Use
Authorization: Bearerheader for mobile/API clients - Set
Secure,SameSite=LaxorStricton cookies in production
Spring Security 6.x
- Use
SecurityFilterChainbean — never extendWebSecurityConfigurerAdapter - Disable CSRF only for stateless APIs; keep it enabled for session-based flows
- Use
@EnableMethodSecurityinstead of deprecated@EnableGlobalMethodSecurity - Validate
issandaudclaims; reject tokens from untrusted issuers
Performance
- Cache
UserDetailswith@Cacheableto avoid DB lookup on every request - Cache signing key derivation (avoid re-computing HMAC key per request)
- Use Redis for refresh token storage at scale
What NOT to Do
- Do not store sensitive data (passwords, PII) in JWT claims — claims are only signed, not encrypted
- Do not issue tokens with infinite lifetime
- Do not accept tokens without validating signature and expiration
- Do not share signing keys across environments
Examples
Basic Authentication Flow
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
@PostMapping("/authenticate")
public ResponseEntity<AuthResponse> authenticate(
@RequestBody LoginRequest request) {
return ResponseEntity.ok(authService.authenticate(request));
}
@PostMapping("/refresh")
public ResponseEntity<AuthResponse> refresh(@RequestBody RefreshRequest request) {
return ResponseEntity.ok(authService.refreshToken(request.refreshToken()));
}
@PostMapping("/logout")
public ResponseEntity<Void> logout() {
authService.logout();
return ResponseEntity.ok().build();
}
}JWT Authorization on Controller Method
@RestController
@RequestMapping("/api/admin")
@PreAuthorize("hasRole('ADMIN')")
public class AdminController {
@GetMapping("/users")
public ResponseEntity<List<UserResponse>> getAllUsers() {
return ResponseEntity.ok(adminService.getAllUsers());
}
}See references/examples.md for complete entity models and service implementations.
References
| File | Content |
|---|---|
| references/jwt-quick-reference.md | Dependencies, minimal service, common patterns |
| references/jwt-complete-configuration.md | Full config: properties, SecurityFilterChain, JwtService, OAuth2 RS |
| references/configuration.md | JWT config beans, CORS, CSRF, error handling, session options |
| references/examples.md | Complete application setup: controllers, services, entities |
| references/authorization-patterns.md | RBAC/ABAC entity model, PermissionEvaluator, SpEL expressions |
| references/token-management.md | Refresh token entity, rotation, blacklisting with Redis |
| references/testing.md | Unit and MockMvc tests, test utilities |
| references/jwt-testing-guide.md | Testcontainers, load testing, security test checklist |
| references/security-hardening.md | Security headers, HSTS, rate limiting, audit logging |
| references/performance-optimization.md | Caffeine cache config, async validation, connection pooling |
| references/oauth2-integration.md | Google/GitHub OAuth2 login, OAuth2UserService |
| references/microservices-security.md | Inter-service JWT propagation, resource server config |
| references/migration-spring-security-6x.md | Migration from Spring Security 5.x |
| references/troubleshooting.md | Common errors, debugging tips |
Constraints and Warnings
Security Constraints
- JWT tokens are signed but not encrypted — do not include sensitive data in claims
- Always validate
exp,iss, andaudclaims before trusting the token - Signing keys must be at least 256 bits; never use weak keys in production
- Load secrets from environment variables or secure vaults, never from config files
- SameSite cookie attribute is essential for CSRF protection in cookie-based flows
Spring Security 6.x Constraints
WebSecurityConfigurerAdapteris removed — useSecurityFilterChainbeans only@EnableGlobalMethodSecurityis deprecated — use@EnableMethodSecurity- Lambda DSL is required for
HttpSecurityconfiguration (no method chaining) WebSecurityConfigurerAdapter.order()replaced by@Orderon@Configurationclasses
Token Constraints
- Access tokens should expire in 5-15 minutes for security
- Refresh tokens should be stored server-side (DB or Redis), never in localStorage
- Implement token blacklisting for immediate revocation on logout
jticlaim is required for token blacklisting to work correctly
Related Skills
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 endpoints
#!/bin/bash
# JWT Key Generation Script
# This script generates RSA keys for JWT signing and verification
set -e
# Configuration
KEY_SIZE=${KEY_SIZE:-2048}
KEY_ALIAS=${KEY_ALIAS:-jwt}
KEYSTORE_PASSWORD=${KEYSTORE_PASSWORD:-changeit}
PRIVATE_KEY_PASSWORD=${PRIVATE_KEY_PASSWORD:-changeit}
OUTPUT_DIR=${OUTPUT_DIR:-./keys}
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo -e "${GREEN}=== JWT Key Generation ===${NC}"
echo
# Create output directory
mkdir -p "$OUTPUT_DIR"
# Generate RSA key pair
echo "Generating $KEY_SIZE-bit RSA key pair..."
keytool -genkeypair \
-alias "$KEY_ALIAS" \
-keyalg RSA \
-keysize "$KEY_SIZE" \
-validity 3650 \
-keypass "$PRIVATE_KEY_PASSWORD" \
-storepass "$KEYSTORE_PASSWORD" \
-keystore "$OUTPUT_DIR/jwt.jks" \
-dname "CN=JWT Key, OU=Security, O=My Company, L=City, ST=State, C=US"
echo -e "${GREEN}✅ Key pair generated successfully${NC}"
# Extract public key
echo "Extracting public key..."
keytool -exportcert \
-alias "$KEY_ALIAS" \
-storepass "$KEYSTORE_PASSWORD" \
-keystore "$OUTPUT_DIR/jwt.jks" \
-rfc \
-file "$OUTPUT_DIR/jwt-public.cer"
echo -e "${GREEN}✅ Public key extracted${NC}"
# Convert to PEM format
echo "Converting to PEM format..."
openssl x509 \
-inform DER \
-outform PEM \
-in "$OUTPUT_DIR/jwt-public.cer" \
-out "$OUTPUT_DIR/jwt-public.pem"
echo -e "${GREEN}✅ PEM certificate created${NC}"
# Generate JWK Set
echo "Generating JWK Set..."
cat > "$OUTPUT_DIR/jwk-set.json" << EOF
{
"keys": [
{
"kty": "RSA",
"use": "sig",
"alg": "RS256",
"kid": "$KEY_ALIAS",
"n": "$(openssl x509 -in "$OUTPUT_DIR/jwt-public.pem" -pubkey -noout | openssl rsa -pubin -outform DER 2>/dev/null | openssl base64 -A | tr -d '=' | tr '/+' '_-' | sed 's/\//_/g' | sed 's/+/-/g')",
"e": "AQAB"
}
]
}
EOF
echo -e "${GREEN}✅ JWK Set generated${NC}"
# Generate Spring Security properties
echo "Generating Spring Security properties..."
cat > "$OUTPUT_DIR/application-security.properties" << EOF
# JWT Configuration
jwt.key-store=classpath:jwt.jks
jwt.key-store-password=$KEYSTORE_PASSWORD
jwt.key-alias=$KEY_ALIAS
jwt.private-key-password=$PRIVATE_KEY_PASSWORD
# Alternative: Public key configuration
jwt.public-key-location=classpath:jwt-public.pem
# JWK Set configuration (for distributed systems)
jwt.jwk-set-uri=https://auth.myapp.com/.well-known/jwks.json
EOF
echo -e "${GREEN}✅ Spring properties generated${NC}"
# Display generated files
echo
echo -e "${GREEN}Generated files:${NC}"
ls -la "$OUTPUT_DIR"
# Display key information
echo
echo "Key Information:"
echo "- Key Size: $KEY_SIZE bits"
echo "- Algorithm: RSA"
echo "- Validity: 10 years"
echo "- Keystore: $OUTPUT_DIR/jwt.jks"
echo "- Public Certificate: $OUTPUT_DIR/jwt-public.cer"
echo "- PEM Certificate: $OUTPUT_DIR/jwt-public.pem"
echo "- JWK Set: $OUTPUT_DIR/jwk-set.json"
# Security warning
echo
echo -e "${YELLOW}⚠️ IMPORTANT SECURITY NOTES:${NC}"
echo "1. Change the default passwords before production use"
echo "2. Store the keystore file securely (don't commit to version control)"
echo "3. Use environment variables or secret management in production"
echo "4. Consider using a cloud KMS (AWS KMS, Azure Key Vault, etc.)"
echo "5. Implement key rotation strategy"
# Instructions for usage
echo
echo -e "${GREEN}Usage Instructions:${NC}"
echo "1. Copy jwt.jks to your application's classpath"
echo "2. Add the properties to your application.properties"
echo "3. Or use the JWK Set for distributed authentication"
echo
echo "Example configuration:"
echo "```yaml"
echo "jwt:"
echo " key-store: classpath:jwt.jks"
echo " key-store-password: \${JWT_KEYSTORE_PASSWORD}"
echo " key-alias: jwt"
echo "```"
# Cleanup temporary files
rm -f "$OUTPUT_DIR/jwt-public.cer"
echo
echo -e "${GREEN}✅ Key generation complete!${NC}"Authorization Patterns and Strategies
Role-Based Access Control (RBAC)
Hierarchical Role Structure
@Entity
@Table(name = "roles")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String name;
private String description;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "role_hierarchy",
joinColumns = @JoinColumn(name = "child_role_id"),
inverseJoinColumns = @JoinColumn(name = "parent_role_id")
)
private Set<Role> parentRoles = new HashSet<>();
@ManyToMany(mappedBy = "parentRoles")
private Set<Role> childRoles = new HashSet<>();
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "role_permissions",
joinColumns = @JoinColumn(name = "role_id"),
inverseJoinColumns = @JoinColumn(name = "permission_id")
)
private Set<Permission> permissions = new HashSet<>();
public Set<Permission> getAllPermissions() {
Set<Permission> allPermissions = new HashSet<>(permissions);
// Recursively collect permissions from parent roles
for (Role parentRole : parentRoles) {
allPermissions.addAll(parentRole.getAllPermissions());
}
return allPermissions;
}
public boolean hasPermission(String permissionName) {
return getAllPermissions().stream()
.anyMatch(permission -> permission.getName().equals(permissionName));
}
}Custom Role Hierarchy Voter
@Component
public class RoleHierarchyVoter implements AccessDecisionVoter<Object> {
private final RoleHierarchy roleHierarchy;
public RoleHierarchyVoter(RoleHierarchy roleHierarchy) {
this.roleHierarchy = roleHierarchy;
}
@Override
public boolean supports(ConfigAttribute attribute) {
return attribute.getAttribute() != null &&
attribute.getAttribute().startsWith("ROLE_");
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
@Override
public int vote(Authentication authentication, Object object,
Collection<ConfigAttribute> attributes) {
Collection<? extends GrantedAuthority> authorities = roleHierarchy
.getReachableGrantedAuthorities(authentication.getAuthorities());
for (ConfigAttribute attribute : attributes) {
if (authorities.contains(new SimpleGrantedAuthority(attribute.getAttribute()))) {
return ACCESS_GRANTED;
}
}
return ACCESS_ABSTAIN;
}
}
@Configuration
public class RoleHierarchyConfig {
@Bean
public RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
String hierarchy =
"ROLE_ADMIN > ROLE_MANAGER\n" +
"ROLE_MANAGER > ROLE_USER\n" +
"ROLE_MANAGER > ROLE_SUPPORT\n" +
"ROLE_SUPPORT > ROLE_READONLY";
roleHierarchy.setHierarchy(hierarchy);
return roleHierarchy;
}
}Method-Level Security with Roles
@Service
@PreAuthorize("hasRole('USER')")
public class DocumentService {
@PreAuthorize("hasRole('ADMIN')")
public void deleteAllDocuments() {
// Only administrators can delete all documents
}
@PreAuthorize("hasAnyRole('ADMIN', 'MANAGER')")
public void approveDocument(Long documentId) {
// Admins and managers can approve documents
}
@PreAuthorize("hasRole('USER')")
public List<Document> getMyDocuments() {
// Regular users can view their own documents
}
@PreAuthorize("hasRole('MANAGER')")
@PostAuthorize("returnObject.owner.id == authentication.principal.id or hasRole('ADMIN')")
public Document getDocumentForApproval(Long documentId) {
// Managers can view documents for approval, admins can view any
return documentRepository.findById(documentId)
.orElseThrow(() -> new DocumentNotFoundException(documentId));
}
}Permission-Based Access Control
Permission Entity with Resource Types
@Entity
@Table(name = "permissions")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Permission {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String name;
private String description;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private ResourceType resourceType;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private ActionType action;
// Permission templates for dynamic permissions
private String template;
// Conditions for conditional permissions
@Lob
private String conditions;
}
public enum ResourceType {
USER("user", "User Management"),
DOCUMENT("document", "Document Management"),
ORDER("order", "Order Processing"),
PRODUCT("product", "Product Catalog"),
INVOICE("invoice", "Invoice Management"),
REPORT("report", "Reporting"),
SYSTEM("system", "System Administration");
private final String code;
private final String description;
ResourceType(String code, String description) {
this.code = code;
this.description = description;
}
}
public enum ActionType {
CREATE("create"),
READ("read"),
UPDATE("update"),
DELETE("delete"),
APPROVE("approve"),
REJECT("reject"),
EXPORT("export"),
IMPORT("import");
private final String code;
ActionType(String code) {
this.code = code;
}
}Custom Permission Evaluator
@Component("permissionEvaluator")
public class CustomPermissionEvaluator implements PermissionEvaluator {
private final PermissionService permissionService;
public boolean hasPermission(Authentication authentication,
Object targetDomainObject, Object permission) {
if (authentication == null || !authentication.isAuthenticated()) {
return false;
}
User user = (User) authentication.getPrincipal();
String permissionName = permission.toString();
// Check direct permissions
if (user.hasPermission(permissionName)) {
return true;
}
// Check resource-specific permissions
if (targetDomainObject != null) {
return checkResourcePermission(user, targetDomainObject, permissionName);
}
return false;
}
@Override
public boolean hasPermission(Authentication authentication,
Serializable targetId, String targetType, Object permission) {
if (authentication == null || !authentication.isAuthenticated()) {
return false;
}
User user = (User) authentication.getPrincipal();
String permissionName = permission.toString();
return permissionService.hasResourcePermission(user, targetId, targetType, permissionName);
}
private boolean checkResourcePermission(User user, Object resource, String permission) {
// Extract resource information
String resourceType = extractResourceType(resource);
Serializable resourceId = extractResourceId(resource);
// Check ownership
if (isOwner(user, resource) && hasOwnershipPermission(permission)) {
return true;
}
// Check department/organizational permissions
if (isInSameDepartment(user, resource) && hasDepartmentPermission(permission)) {
return true;
}
// Check global permissions
return permissionService.hasGlobalPermission(user, resourceType, permission);
}
private boolean hasOwnershipPermission(String permission) {
return permission.endsWith("_OWN") || permission.endsWith("_MY");
}
private boolean hasDepartmentPermission(String permission) {
return permission.endsWith("_DEPT") || permission.endsWith("_ORG");
}
}Dynamic Permission Builder
@Service
public class PermissionBuilderService {
public Set<String> buildPermissions(User user) {
Set<String> permissions = new HashSet<>();
// Role-based permissions
user.getRoles().forEach(role -> {
permissions.add("ROLE_" + role.getName());
role.getPermissions().forEach(permission -> {
permissions.add(permission.getName());
});
});
// User-specific permissions
user.getUserPermissions().forEach(userPermission -> {
if (isPermissionValid(userPermission)) {
permissions.add(buildPermissionString(userPermission));
}
});
// Dynamic permissions based on user attributes
addDynamicPermissions(user, permissions);
return permissions;
}
private void addDynamicPermissions(User user, Set<String> permissions) {
// Department-based permissions
if (user.getDepartment() != null) {
permissions.add("DEPT_" + user.getDepartment().getCode() + "_READ");
}
// Location-based permissions
if (user.getLocation() != null) {
permissions.add("LOCATION_" + user.getLocation().getCode() + "_ACCESS");
}
// Project-based permissions
user.getProjectMemberships().forEach(membership -> {
permissions.add("PROJECT_" + membership.getProject().getId() + "_MEMBER");
if (membership.getRole() == ProjectRole.MANAGER) {
permissions.add("PROJECT_" + membership.getProject().getId() + "_MANAGER");
}
});
}
private String buildPermissionString(UserPermission userPermission) {
return String.format("%s_%s_%s",
userPermission.getResourceType(),
userPermission.getAction(),
userPermission.getScope());
}
}Attribute-Based Access Control (ABAC)
Policy-Based Authorization
@Component
public class PolicyBasedAccessDecisionManager implements AccessDecisionManager {
private final PolicyRepository policyRepository;
private final PolicyEvaluationService evaluationService;
@Override
public void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
if (configAttributes.isEmpty()) {
return;
}
for (ConfigAttribute attribute : configAttributes) {
if (supports(attribute)) {
String policyName = attribute.getAttribute();
Policy policy = policyRepository.findByName(policyName)
.orElseThrow(() -> new PolicyNotFoundException(policyName));
if (!evaluationService.evaluate(policy, authentication, object)) {
throw new AccessDeniedException(
"Access denied by policy: " + policyName);
}
}
}
}
@Override
public boolean supports(ConfigAttribute attribute) {
return attribute.getAttribute() != null &&
attribute.getAttribute().startsWith("POLICY_");
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}
@Service
public class PolicyEvaluationService {
public boolean evaluate(Policy policy, Authentication authentication, Object resource) {
// Build evaluation context
EvaluationContext context = EvaluationContext.builder()
.subject(extractSubjectInfo(authentication))
.resource(extractResourceInfo(resource))
.environment(extractEnvironmentInfo())
.build();
// Evaluate policy rules
return policy.getRules().stream()
.allMatch(rule -> evaluateRule(rule, context));
}
private boolean evaluateRule(PolicyRule rule, EvaluationContext context) {
// Implement rule evaluation logic using SPEL or custom evaluator
StandardEvaluationContext spe1Context = new StandardEvaluationContext(context);
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(rule.getCondition());
return expression.getValue(spe1Context, Boolean.class);
}
}ABAC Policy Definitions
@Entity
@Table(name = "policies")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Policy {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String name;
private String description;
@Enumerated(EnumType.STRING)
private PolicyEffect effect; // PERMIT or DENY
@OneToMany(mappedBy = "policy", cascade = CascadeType.ALL, orphanRemoval = true)
@OrderBy("priority ASC")
private List<PolicyRule> rules = new ArrayList<>();
@Column(columnDefinition = "TEXT")
private String targetCondition; // SPEL expression for target matching
public enum PolicyEffect {
PERMIT, DENY
}
}
@Entity
@Table(name = "policy_rules")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class PolicyRule {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "policy_id")
private Policy policy;
private String description;
@Column(columnDefinition = "TEXT")
private String condition; // SPEL expression
private int priority;
@Enumerated(EnumType.STRING)
private RuleType type;
public enum RuleType {
SUBJECT, RESOURCE, ENVIRONMENT, COMPOSITE
}
}Time-Based Access Control
Time-Restricted Permissions
@Component
public class TimeBasedPermissionEvaluator {
public boolean hasTimeBasedPermission(Authentication authentication,
Object permission, Instant accessTime) {
User user = (User) authentication.getPrincipal();
// Check business hours
if (!isWithinBusinessHours(accessTime)) {
return user.hasPermission("AFTER_HOURS_ACCESS");
}
// Check time-based restrictions
return user.getTimeRestrictions().stream()
.noneMatch(restriction -> isRestricted(restriction, accessTime));
}
private boolean isWithinBusinessHours(Instant accessTime) {
ZonedDateTime zdt = accessTime.atZone(ZoneId.systemDefault());
DayOfWeek dayOfWeek = zdt.getDayOfWeek();
int hour = zdt.getHour();
// Monday to Friday, 9 AM to 6 PM
return dayOfWeek != DayOfWeek.SATURDAY &&
dayOfWeek != DayOfWeek.SUNDAY &&
hour >= 9 && hour < 18;
}
}
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("@timeBasedPermissionEvaluator.hasTimeBasedPermission(authentication, 'READ', T(java.time.Instant).now())")
public @interface TimeRestrictedAccess {
String value() default "READ";
}Expiration-Based Access
@Entity
@Table(name = "access_grants")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AccessGrant {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
private User user;
@Column(nullable = false)
private String resource;
@Column(nullable = false)
private String permission;
@Column(nullable = false)
private Instant grantedAt;
@Column(nullable = false)
private Instant expiresAt;
private String grantedBy;
@Column(columnDefinition = "TEXT")
private String reason;
public boolean isValid() {
return Instant.now().isBefore(expiresAt);
}
}
@Service
public class AccessGrantService {
@Transactional
public AccessGrant grantTemporaryAccess(User user, String resource,
String permission, Duration duration, String grantedBy, String reason) {
AccessGrant grant = AccessGrant.builder()
.user(user)
.resource(resource)
.permission(permission)
.grantedAt(Instant.now())
.expiresAt(Instant.now().plus(duration))
.grantedBy(grantedBy)
.reason(reason)
.build();
return accessGrantRepository.save(grant);
}
public boolean hasTemporaryAccess(User user, String resource, String permission) {
return accessGrantRepository
.findByUserAndResourceAndPermissionAndExpiresAtAfter(
user, resource, permission, Instant.now())
.isPresent();
}
}Location-Based Access Control
IP Address Restrictions
@Component
public class LocationBasedAccessControl {
private final IpRangeRepository ipRangeRepository;
public boolean isAccessAllowedFromIp(Authentication authentication, String ipAddress) {
User user = (User) authentication.getPrincipal();
// Check if user has location restrictions
if (!user.hasLocationRestrictions()) {
return true;
}
// Check IP against allowed ranges
List<IpRange> allowedRanges = ipRangeRepository.findByUser(user);
return allowedRanges.stream()
.anyMatch(range -> isInRange(ipAddress, range));
}
private boolean isInRange(String ipAddress, IpRange ipRange) {
try {
InetAddress address = InetAddress.getByName(ipAddress);
InetAddress networkAddress = InetAddress.getByName(ipRange.getNetworkAddress());
int prefixLength = ipRange.getPrefixLength();
byte[] addressBytes = address.getAddress();
byte[] networkBytes = networkAddress.getAddress();
int fullPrefix = prefixLength / 8;
int partialPrefix = prefixLength % 8;
for (int i = 0; i < fullPrefix; i++) {
if (addressBytes[i] != networkBytes[i]) {
return false;
}
}
if (partialPrefix > 0) {
byte mask = (byte) (0xFF << (8 - partialPrefix));
if ((addressBytes[fullPrefix] & mask) != (networkBytes[fullPrefix] & mask)) {
return false;
}
}
return true;
} catch (Exception e) {
return false;
}
}
}Organizational Access Control
Department-Based Security
@Entity
@Table(name = "departments")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Department {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String code;
private String name;
private String description;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_department_id")
private Department parentDepartment;
@OneToMany(mappedBy = "parentDepartment")
private List<Department> childDepartments = new ArrayList<>();
private int level;
// Get all child departments recursively
public List<Department> getAllChildDepartments() {
List<Department> allChildren = new ArrayList<>();
for (Department child : childDepartments) {
allChildren.add(child);
allChildren.addAll(child.getAllChildDepartments());
}
return allChildren;
}
}
@Service
public class DepartmentSecurityService {
public boolean canAccessDepartmentData(User user, Department targetDepartment) {
// Users can access their own department
if (user.getDepartment().equals(targetDepartment)) {
return true;
}
// Check parent department access
if (canAccessParentDepartment(user, targetDepartment)) {
return true;
}
// Check child department access
if (canAccessChildDepartments(user, targetDepartment)) {
return true;
}
return false;
}
private boolean canAccessParentDepartment(User user, Department department) {
Department current = department.getParentDepartment();
while (current != null) {
if (current.equals(user.getDepartment()) &&
user.hasPermission("DEPT_CHILDREN_ACCESS")) {
return true;
}
current = current.getParentDepartment();
}
return false;
}
private boolean canAccessChildDepartments(User user, Department department) {
return user.getDepartment().getAllChildDepartments().contains(department) &&
user.hasPermission("DEPT_PARENT_ACCESS");
}
}JWT Security Configuration Reference
This document provides comprehensive configuration options for JWT security in Spring Boot applications using JJWT library and Spring Security 6.x.
Table of Contents
1. Application Properties 2. JWT Configuration Beans 3. Security Filter Chain Options 4. Token Validation Configuration 5. Key Management 6. CORS and CSRF Configuration 7. Session Management 8. Error Handling Configuration 9. Performance Configuration 10. Monitoring and Audit Configuration
Application Properties
Complete JWT Configuration (application.yml)
# JWT Configuration
jwt:
# Token settings
secret: ${JWT_SECRET:my-very-secret-key-that-is-at-least-256-bits-long-for-hmac-sha256}
access-token-expiration: 900000 # 15 minutes in milliseconds
refresh-token-expiration: 604800000 # 7 days in milliseconds
issuer: ${JWT_ISSUER:spring-boot-jwt-app}
audience: ${JWT_AUDIENCE:spring-boot-client}
# Cookie settings
cookie-name: jwt-token
cookie-secure: ${JWT_COOKIE_SECURE:false} # Set to true in production with HTTPS
cookie-http-only: true
cookie-same-site: lax # strict, lax, or none
cookie-domain: ${JWT_COOKIE_DOMAIN:} # Optional domain
cookie-path: /
cookie-max-age: 86400 # 24 hours
# Token validation
validate-issuer: true
validate-audience: false
validate-expiration: true
clock-skew-seconds: 60 # Allow 60 seconds clock skew
# Refresh token settings
refresh-token-limit: 5 # Max active refresh tokens per user
refresh-token-rotation-enabled: true
refresh-token-cleanup-enabled: true
refresh-token-cleanup-cron: "0 0 2 * * ?" # Daily at 2 AM
# Security settings
blacklist-enabled: true
blacklist-cleanup-enabled: true
blacklist-cleanup-cron: "0 0 3 * * ?" # Daily at 3 AM
# Spring Security Configuration
spring:
security:
oauth2:
client:
registration:
google:
client-id: ${GOOGLE_CLIENT_ID}
client-secret: ${GOOGLE_CLIENT_SECRET}
scope: openid, profile, email
redirect-uri: "{baseUrl}/login/oauth2/code/google"
client-name: Google
github:
client-id: ${GITHUB_CLIENT_ID}
client-secret: ${GITHUB_CLIENT_SECRET}
scope: user:email
redirect-uri: "{baseUrl}/login/oauth2/code/github"
client-name: GitHub
provider:
google:
authorization-uri: https://accounts.google.com/o/oauth2/v2/auth
token-uri: https://oauth2.googleapis.com/token
user-info-uri: https://www.googleapis.com/oauth2/v2/userinfo
github:
authorization-uri: https://github.com/login/oauth/authorize
token-uri: https://github.com/login/oauth/access_token
user-info-uri: https://api.github.com/user
# Session configuration (if needed)
session:
store-type: none # Use stateless sessions
timeout: 30m # Session timeout
jdbc:
initialize-schema: always
# CORS configuration
web:
cors:
allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost:3000,http://localhost:8080}
allowed-methods: GET,POST,PUT,DELETE,OPTIONS
allowed-headers: "*"
allow-credentials: true
max-age: 3600
# Logging configuration
logging:
level:
org.springframework.security: DEBUG
io.jsonwebtoken: DEBUG
com.example.security: DEBUG
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n"
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
# Management endpoints for monitoring
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: when_authorized
security:
enabled: trueJWT Configuration Beans
JWT Service Configuration
@Configuration
@RequiredArgsConstructor
public class JwtConfig {
@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;
@Value("${jwt.audience:}")
private String audience;
@Value("${jwt.validate-issuer:true}")
private boolean validateIssuer;
@Value("${jwt.validate-audience:false}")
private boolean validateAudience;
@Value("${jwt.clock-skew-seconds:60}")
private int clockSkewSeconds;
@Bean
public JwtService jwtService(RefreshTokenService refreshTokenService) {
return new JwtService(
secret,
accessTokenExpiration,
refreshTokenExpiration,
issuer,
audience,
validateIssuer,
validateAudience,
clockSkewSeconds,
refreshTokenService
);
}
@Bean
public JwtParser jwtParser() {
return Jwts.parser()
.verifyWith(getSigningKey())
.requireIssuer(issuer)
.setAllowedClockSkewSeconds(clockSkewSeconds)
.build();
}
@Bean
public SecretKey getSigningKey() {
byte[] keyBytes = Decoders.BASE64.decode(
Base64.getEncoder().encodeToString(secret.getBytes())
);
return Keys.hmacShaKeyFor(keyBytes);
}
@Bean
public ClaimsSetExtractor claimsSetExtractor() {
return new DefaultClaimsSetExtractor(
issuer,
audience,
Duration.ofMillis(accessTokenExpiration)
);
}
}Custom JWT Parser with Validation
@Configuration
public class JwtParserConfig {
@Bean
public JwtParser jwtParser(SecretKey signingKey, JwtProperties jwtProperties) {
JwtParserBuilder parser = Jwts.parser()
.verifyWith(signingKey)
.setAllowedClockSkewSeconds(jwtProperties.getClockSkewSeconds());
// Add required claims
if (jwtProperties.isValidateIssuer()) {
parser.requireIssuer(jwtProperties.getIssuer());
}
if (jwtProperties.isValidateAudience() &&
StringUtils.hasText(jwtProperties.getAudience())) {
parser.requireAudience(jwtProperties.getAudience());
}
return parser.build();
}
@Bean
public JwtValidator jwtValidator(JwtParser jwtParser) {
return new DefaultJwtValidator(jwtParser);
}
}Configuration Properties Class
@ConfigurationProperties(prefix = "jwt")
@Data
@Validated
public class JwtProperties {
/**
* JWT secret key for HMAC signing
*/
@NotBlank
@Size(min = 32, message = "JWT secret must be at least 32 characters")
private String secret;
/**
* Access token expiration in milliseconds
*/
@Min(60000) // Minimum 1 minute
private long accessTokenExpiration = 900000; // 15 minutes
/**
* Refresh token expiration in milliseconds
*/
@Min(3600000) // Minimum 1 hour
private long refreshTokenExpiration = 604800000; // 7 days
/**
* JWT issuer
*/
@NotBlank
private String issuer;
/**
* JWT audience
*/
private String audience;
/**
* Validate issuer claim
*/
private boolean validateIssuer = true;
/**
* Validate audience claim
*/
private boolean validateAudience = false;
/**
* Clock skew in seconds for token validation
*/
@Min(0)
private int clockSkewSeconds = 60;
/**
* Cookie configuration
*/
private CookieProperties cookie = new CookieProperties();
/**
* Refresh token configuration
*/
private RefreshTokenProperties refreshToken = new RefreshTokenProperties();
/**
* Blacklist configuration
*/
private BlacklistProperties blacklist = new BlacklistProperties();
@Data
public static class CookieProperties {
private String name = "jwt-token";
private boolean secure = false;
private boolean httpOnly = true;
private String sameSite = "lax";
private String domain;
private String path = "/";
private int maxAge = 86400;
}
@Data
public static class RefreshTokenProperties {
private int limit = 5;
private boolean rotationEnabled = true;
private boolean cleanupEnabled = true;
private String cleanupCron = "0 0 2 * * ?";
}
@Data
public static class BlacklistProperties {
private boolean enabled = true;
private boolean cleanupEnabled = true;
private String cleanupCron = "0 0 3 * * ?";
}
}Security Filter Chain Options
Advanced Security Configuration
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class AdvancedSecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final AuthenticationProvider authenticationProvider;
private final JwtAuthenticationEntryPoint authenticationEntryPoint;
private final CustomAccessDeniedHandler accessDeniedHandler;
private final SecurityCorsConfigurationSource corsConfigurationSource;
private final LogoutHandler logoutHandler;
private final SecurityContextRepository securityContextRepository;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
// CORS Configuration
.cors(cors -> cors.configurationSource(corsConfigurationSource))
// CSRF Configuration
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.ignoringRequestMatchers("/api/auth/**", "/api/public/**")
.sessionAuthenticationStrategy(new NullSessionAuthenticationStrategy())
)
// Security Headers
.headers(headers -> headers
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'")
)
.frameOptions().deny()
.httpStrictTransportSecurity(hsts -> hsts
.maxAgeInSeconds(31536000)
.includeSubdomains(true)
.preload(true)
)
.permissionsPolicy(permissions -> permissions
.policy("camera=(), microphone=(), geolocation=()")
)
.referrerPolicy(ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN)
)
// Session Management
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.sessionAuthenticationStrategy(sessionAuthenticationStrategy())
.maximumSessions(10)
.maxSessionsPreventsLogin(false)
.sessionRegistry(sessionRegistry())
)
// Exception Handling
.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler)
)
// Request Authorization
.authorizeHttpRequests(auth -> auth
// Public endpoints
.requestMatchers("/api/auth/**", "/api/public/**", "/health", "/actuator/health").permitAll()
// Admin endpoints
.requestMatchers("/api/admin/**").hasRole("ADMIN")
// API endpoints with specific permissions
.requestMatchers(HttpMethod.GET, "/api/users/**").hasAuthority("USER_READ")
.requestMatchers(HttpMethod.POST, "/api/users/**").hasAuthority("USER_WRITE")
.requestMatchers(HttpMethod.PUT, "/api/users/**").hasAuthority("USER_WRITE")
.requestMatchers(HttpMethod.DELETE, "/api/users/**").hasAuthority("USER_DELETE")
// OAuth2 endpoints
.requestMatchers("/oauth2/**", "/login/oauth2/**").permitAll()
// Actuator endpoints
.requestMatchers("/actuator/**").hasRole("ADMIN")
// All other requests require authentication
.anyRequest().authenticated()
)
// OAuth2 Resource Server
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.decoder(jwtDecoder())
.jwtAuthenticationConverter(jwtAuthenticationConverter())
)
.accessDeniedHandler(accessDeniedHandler)
.authenticationEntryPoint(authenticationEntryPoint)
)
// OAuth2 Login
.oauth2Login(oauth2 -> oauth2
.authorizationEndpoint(authorization -> authorization
.baseUri("/oauth2/authorization")
)
.redirectionEndpoint(redirection -> redirection
.baseUri("/login/oauth2/code/*")
)
.userInfoEndpoint(userInfo -> userInfo
.userService(oAuth2UserService())
)
.successHandler(oAuth2AuthenticationSuccessHandler())
.failureHandler(oAuth2AuthenticationFailureHandler())
)
// Authentication Providers
.authenticationProvider(authenticationProvider)
// Filters
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(securityContextFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(auditLoggingFilter(), UsernamePasswordAuthenticationFilter.class)
// Logout Configuration
.logout(logout -> logout
.logoutUrl("/api/auth/logout")
.addLogoutHandler(securityContextLogoutHandler())
.addLogoutHandler(logoutHandler)
.addLogoutHandler(cookieClearingLogoutHandler())
.logoutSuccessHandler((request, response, authentication) ->
response.setStatus(HttpStatus.NO_CONTENT.value()))
.deleteCookies("JSESSIONID", "jwt-token")
.clearAuthentication(true)
.invalidateHttpSession(true)
)
// Security Context Repository
.securityContext(securityContextRepository)
.build();
}
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withSecretKey(getSigningKey())
.signatureAlgorithm(SignatureAlgorithm.HS256)
.build();
}
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter();
authoritiesConverter.setAuthorityPrefix("ROLE_");
authoritiesConverter.setAuthoritiesClaimName("authorities");
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
converter.setPrincipalClaimName("sub");
converter.setPrincipalAttributeName("sub");
return converter;
}
@Bean
public OAuth2UserService<OAuth2UserRequest, OAuth2User> oAuth2UserService() {
DefaultOAuth2UserService delegate = new DefaultOAuth2UserService();
return new CustomOAuth2UserService(delegate);
}
@Bean
public AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler() {
return new OAuth2AuthenticationSuccessHandler(jwtService);
}
@Bean
public AuthenticationFailureHandler oAuth2AuthenticationFailureHandler() {
return new OAuth2AuthenticationFailureHandler();
}
@Bean
public SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new CompositeSessionAuthenticationStrategy(Arrays.asList(
new RegisterSessionAuthenticationStrategy(sessionRegistry()),
new CsrfAuthenticationStrategy()
));
}
@Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
@Bean
public SecurityContextRepository securityContextRepository() {
return new JwtSecurityContextRepository(jwtService, userDetailsService);
}
@Bean
public Filter securityContextFilter() {
return new SecurityContextPersistenceFilter(securityContextRepository());
}
@Bean
public Filter auditLoggingFilter() {
return new AuditLoggingFilter();
}
@Bean
public LogoutHandler securityContextLogoutHandler() {
return new SecurityContextLogoutHandler();
}
@Bean
public LogoutHandler cookieClearingLogoutHandler() {
return new CookieClearingLogoutHandler("JSESSIONID", "jwt-token");
}
}Token Validation Configuration
Custom JWT Validator
@Component
@RequiredArgsConstructor
public class CustomJwtValidator implements JwtValidator {
private final JwtParser jwtParser;
private final BlacklistedTokenService blacklistedTokenService;
private final JwtProperties jwtProperties;
@Override
public ValidationResult validate(String token) {
try {
// Check if token is blacklisted
if (jwtProperties.getBlacklist().isEnabled()) {
String jti = extractClaim(token, "jti");
if (blacklistedTokenService.isBlacklisted(jti)) {
return ValidationResult.error("Token is blacklisted");
}
}
// Parse and validate token
Claims claims = jwtParser.parseSignedClaims(token).getPayload();
// Additional custom validations
return validateCustomClaims(claims);
} catch (ExpiredJwtException e) {
return ValidationResult.error("Token has expired");
} catch (UnsupportedJwtException e) {
return ValidationResult.error("Token is unsupported");
} catch (MalformedJwtException e) {
return ValidationResult.error("Token is malformed");
} catch (SecurityException e) {
return ValidationResult.error("Token signature validation failed");
} catch (IllegalArgumentException e) {
return ValidationResult.error("Token is invalid");
} catch (JwtException e) {
return ValidationResult.error("JWT processing failed: " + e.getMessage());
}
}
private ValidationResult validateCustomClaims(Claims claims) {
// Validate issuer
if (jwtProperties.isValidateIssuer() &&
!claims.getIssuer().equals(jwtProperties.getIssuer())) {
return ValidationResult.error("Invalid issuer");
}
// Validate audience
if (jwtProperties.isValidateAudience()) {
List<String> audiences = claims.getAudience();
if (audiences == null || audiences.isEmpty() ||
!audiences.contains(jwtProperties.getAudience())) {
return ValidationResult.error("Invalid audience");
}
}
// Validate token type
String tokenType = claims.get("type", String.class);
if (tokenType == null || !tokenType.equals("access")) {
return ValidationResult.error("Invalid token type");
}
return ValidationResult.success();
}
private String extractClaim(String token, String claimName) {
try {
Claims claims = Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
return claims.get(claimName, String.class);
} catch (JwtException e) {
return null;
}
}
@Value("${jwt.secret}")
private String secret;
private SecretKey getSigningKey() {
byte[] keyBytes = Decoders.BASE64.decode(
Base64.getEncoder().encodeToString(secret.getBytes())
);
return Keys.hmacShaKeyFor(keyBytes);
}
}
@Data
@AllArgsConstructor
public class ValidationResult {
private boolean valid;
private String errorMessage;
public static ValidationResult success() {
return new ValidationResult(true, null);
}
public static ValidationResult error(String message) {
return new ValidationResult(false, message);
}
}Key Management
Asymmetric Key Configuration
@Configuration
@ConditionalOnProperty(name = "jwt.algorithm", havingValue = "RSA")
public class AsymmetricJwtConfig {
@Value("${jwt.public-key}")
private String publicKeyString;
@Value("${jwt.private-key}")
private String privateKeyString;
@Bean
public RSAPublicKey publicKey() throws Exception {
return (RSAPublicKey) KeyFactory.getInstance("RSA")
.generatePublic(new X509EncodedKeySpec(
Base64.getDecoder().decode(publicKeyString)
));
}
@Bean
public RSAPrivateKey privateKey() throws Exception {
return (RSAPrivateKey) KeyFactory.getInstance("RSA")
.generatePrivate(new PKCS8EncodedKeySpec(
Base64.getDecoder().decode(privateKeyString)
));
}
@Bean
public JwtDecoder jwtDecoder(RSAPublicKey publicKey) {
return NimbusJwtDecoder.withPublicKey(publicKey)
.signatureAlgorithm(SignatureAlgorithm.RS256)
.build();
}
@Bean
public JwtEncoder jwtEncoder(RSAPrivateKey privateKey) {
RSASSASigner rsaSigner = new RSASSASigner(privateKey);
return new NimbusJwtEncoder(
new ImmutableJWEHeader(JWSAlgorithm.RS256),
rsaSigner
);
}
}Key Rotation Support
@Service
@RequiredArgsConstructor
@Slf4j
public class KeyRotationService {
private final KeyRepository keyRepository;
private final Map<String, KeyPair> activeKeys = new ConcurrentHashMap<>();
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
@PostConstruct
public void initialize() {
loadActiveKeys();
scheduleKeyRotation();
}
@Scheduled(cron = "${jwt.key-rotation.cron:0 0 0 1 * ?}") // Monthly
public void rotateKeys() {
try {
log.info("Starting JWT key rotation");
// Generate new key pair
KeyPair newKeyPair = generateKeyPair();
// Save new key
JwtKey newKey = JwtKey.builder()
.keyId(UUID.randomUUID().toString())
.publicKey(Base64.getEncoder().encodeToString(newKeyPair.getPublic().getEncoded()))
.privateKey(Base64.getEncoder().encodeToString(newKeyPair.getPrivate().getEncoded()))
.algorithm("RS256")
.createdAt(Instant.now())
.isActive(true)
.build();
// Deactivate old keys
keyRepository.deactivateAllKeys();
// Save new key
keyRepository.save(newKey);
// Update active keys cache
loadActiveKeys();
log.info("JWT key rotation completed successfully");
} catch (Exception e) {
log.error("JWT key rotation failed", e);
}
}
public KeyPair getCurrentKeyPair() {
return activeKeys.values().iterator().next();
}
public KeyPair getKeyPair(String keyId) {
return activeKeys.get(keyId);
}
private void loadActiveKeys() {
List<JwtKey> activeJwtKeys = keyRepository.findByIsActiveTrue();
activeKeys.clear();
for (JwtKey key : activeJwtKeys) {
try {
KeyPair keyPair = restoreKeyPair(key);
activeKeys.put(key.getKeyId(), keyPair);
} catch (Exception e) {
log.error("Failed to restore key pair for keyId: {}", key.getKeyId(), e);
}
}
if (activeKeys.isEmpty()) {
log.warn("No active keys found, generating new key pair");
rotateKeys();
}
}
private KeyPair generateKeyPair() throws NoSuchAlgorithmException {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
return keyPairGenerator.generateKeyPair();
}
private KeyPair restoreKeyPair(JwtKey jwtKey) throws Exception {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
byte[] publicKeyBytes = Base64.getDecoder().decode(jwtKey.getPublicKey());
X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(publicKeyBytes);
RSAPublicKey publicKey = (RSAPublicKey) keyFactory.generatePublic(publicKeySpec);
byte[] privateKeyBytes = Base64.getDecoder().decode(jwtKey.getPrivateKey());
PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
RSAPrivateKey privateKey = (RSAPrivateKey) keyFactory.generatePrivate(privateKeySpec);
return new KeyPair(publicKey, privateKey);
}
private void scheduleKeyRotation() {
scheduler.scheduleAtFixedRate(
this::rotateKeys,
1, // Initial delay
30, // Period (days)
TimeUnit.DAYS
);
}
}CORS and CSRF Configuration
Advanced CORS Configuration
@Configuration
public class CorsConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
// Allowed origins (configure based on environment)
configuration.setAllowedOriginPatterns(Arrays.asList(
"http://localhost:*",
"https://*.yourdomain.com"
));
// Allowed HTTP methods
configuration.setAllowedMethods(Arrays.asList(
"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"
));
// Allowed headers
configuration.setAllowedHeaders(Arrays.asList(
"Authorization",
"Content-Type",
"X-Requested-With",
"Accept",
"Origin",
"Access-Control-Request-Method",
"Access-Control-Request-Headers"
));
// Exposed headers
configuration.setExposedHeaders(Arrays.asList(
"X-Total-Count",
"X-Page-Count",
"X-Current-Page"
));
// Allow credentials
configuration.setAllowCredentials(true);
// Max age for pre-flight requests
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", configuration);
source.registerCorsConfiguration("/oauth2/**", configuration);
return source;
}
}Custom CSRF Configuration
@Configuration
public class CsrfConfig {
@Bean
public CsrfTokenRepository csrfTokenRepository() {
CookieCsrfTokenRepository repository = CookieCsrfTokenRepository.withHttpOnlyFalse();
repository.setCookieName("XSRF-TOKEN");
repository.setHeaderName("X-XSRF-TOKEN");
repository.setCookieHttpOnly(false);
repository.setCookiePath("/");
// Set secure flag in production
if (isProductionEnvironment()) {
repository.setCookieSecure(true);
}
return repository;
}
@Bean
public CsrfTokenRequestHandler csrfTokenRequestHandler() {
return new CsrfTokenRequestAttributeHandler();
}
@Bean
public CsrfTokenRequestHandler spaCsrfTokenRequestHandler() {
return new SpaCsrfTokenRequestHandler();
}
private boolean isProductionEnvironment() {
String[] activeProfiles = Environment.getActiveProfiles();
return Arrays.asList(activeProfiles).contains("prod");
}
}
// SPA CSRF handler for single-page applications
public class SpaCsrfTokenRequestHandler extends CsrfTokenRequestAttributeHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
Supplier<CsrfToken> csrfToken) {
String csrfTokenValue = csrfToken.get().getToken();
response.setHeader("X-CSRF-TOKEN", csrfTokenValue);
response.setHeader("Access-Control-Expose-Headers", "X-CSRF-TOKEN");
}
}This configuration reference provides comprehensive options for setting up JWT security in Spring Boot applications with various security features, key management strategies, and advanced configurations for production environments.
Spring Security JWT Implementation Examples
Complete Application Setup
Application Main Class
@SpringBootApplication
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
@EnableJpaRepositories(basePackages = "com.example.security.repository")
@EntityScan(basePackages = "com.example.security.model")
public class SecurityApplication {
public static void main(String[] args) {
SpringApplication.run(SecurityApplication.class, args);
}
@Bean
public CommandLineRunner initData(UserRepository userRepository,
RoleRepository roleRepository,
PermissionRepository permissionRepository,
PasswordEncoder passwordEncoder) {
return args -> {
// Create permissions
Permission readPermission = permissionRepository.save(
new Permission("USER_READ", "Read user information"));
Permission writePermission = permissionRepository.save(
new Permission("USER_WRITE", "Write user information"));
Permission deletePermission = permissionRepository.save(
new Permission("USER_DELETE", "Delete user information"));
Permission adminPermission = permissionRepository.save(
new Permission("ADMIN", "Full administrative access"));
// Create roles
Role userRole = roleRepository.save(new Role("USER"));
Role adminRole = roleRepository.save(new Role("ADMIN"));
Role managerRole = roleRepository.save(new Role("MANAGER"));
// Assign permissions to roles
userRole.getPermissions().addAll(Set.of(readPermission));
managerRole.getPermissions().addAll(Set.of(readPermission, writePermission));
adminRole.getPermissions().addAll(Set.of(readPermission, writePermission, deletePermission, adminPermission));
roleRepository.saveAll(List.of(userRole, adminRole, managerRole));
// Create users
User user = new User("user@example.com", passwordEncoder.encode("password"));
user.setRoles(Set.of(userRole));
user.setEnabled(true);
User admin = new User("admin@example.com", passwordEncoder.encode("admin"));
admin.setRoles(Set.of(adminRole));
admin.setEnabled(true);
User manager = new User("manager@example.com", passwordEncoder.encode("manager"));
manager.setRoles(Set.of(managerRole));
manager.setEnabled(true);
userRepository.saveAll(List.of(user, admin, manager));
};
}
}Domain Models
@Entity
@Table(name = "users")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String email;
@Column(nullable = false)
private String password;
private String firstName;
private String lastName;
@Column(name = "phone_number")
private String phoneNumber;
@Column(nullable = false)
private boolean enabled = true;
@Column(nullable = false)
private boolean accountNonExpired = true;
@Column(nullable = false)
private boolean accountNonLocked = true;
@Column(nullable = false)
private boolean credentialsNonExpired = true;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
private Set<Role> roles = new HashSet<>();
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
private List<RefreshToken> refreshTokens = new ArrayList<>();
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
private List<UserSession> sessions = new ArrayList<>();
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles.stream()
.flatMap(role -> {
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getName()));
authorities.addAll(role.getPermissions().stream()
.map(permission -> new SimpleGrantedAuthority(permission.getName()))
.collect(Collectors.toList()));
return authorities.stream();
})
.collect(Collectors.toList());
}
@Override
public String getUsername() {
return email;
}
public String getFullName() {
return String.format("%s %s", firstName, lastName).trim();
}
public boolean hasPermission(String permission) {
return getAuthorities().stream()
.anyMatch(auth -> auth.getAuthority().equals(permission));
}
public boolean hasRole(String role) {
return roles.stream()
.anyMatch(r -> r.getName().equals(role));
}
}
@Entity
@Table(name = "roles")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String name;
private String description;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "role_permissions",
joinColumns = @JoinColumn(name = "role_id"),
inverseJoinColumns = @JoinColumn(name = "permission_id")
)
private Set<Permission> permissions = new HashSet<>();
}
@Entity
@Table(name = "permissions")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Permission {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String name;
private String description;
@Column(name = "resource_type")
private String resourceType;
}Authentication Controller
Complete Auth Controller
@RestController
@RequestMapping("/api/auth")
@Validated
@Slf4j
public class AuthController {
private final AuthenticationManager authenticationManager;
private final JwtTokenService tokenService;
private final RefreshTokenService refreshTokenService;
private final UserService userService;
private final AuthenticationEventListener eventListener;
@PostMapping("/login")
public ResponseEntity<LoginResponse> login(
@Valid @RequestBody LoginRequest request,
HttpServletRequest httpRequest) {
log.info("Login attempt for user: {}", request.email());
try {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.email(),
request.password()
)
);
SecurityContextHolder.getContext()
.setAuthentication(authentication);
User user = (User) authentication.getPrincipal();
// Generate tokens
AccessTokenResponse accessToken = tokenService.generateAccessToken(user);
RefreshTokenResponse refreshToken = refreshTokenService.createRefreshToken(user);
// Track device and location
String deviceInfo = extractDeviceInfo(httpRequest);
String ipAddress = extractIpAddress(httpRequest);
userService.recordLogin(user, deviceInfo, ipAddress);
// Publish authentication success event
eventListener.publishAuthenticationSuccess(user, httpRequest);
LoginResponse response = new LoginResponse(
accessToken.token(),
accessToken.expiresAt(),
refreshToken.token(),
refreshToken.expiresAt(),
user.getId(),
user.getEmail(),
user.getFullName(),
user.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList())
);
return ResponseEntity.ok()
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken.token())
.body(response);
} catch (BadCredentialsException e) {
log.warn("Failed login attempt for user: {}", request.email());
throw new AuthenticationFailedException("Invalid credentials");
}
}
@PostMapping("/refresh")
public ResponseEntity<RefreshTokenResponse> refreshToken(
@Valid @RequestBody RefreshTokenRequest request) {
RefreshTokenResponse response = refreshTokenService.refreshToken(request);
return ResponseEntity.ok(response);
}
@PostMapping("/logout")
@PreAuthorize("isAuthenticated()")
public ResponseEntity<MessageResponse> logout(
@RequestHeader(value = "Authorization", required = false) String authorization,
HttpServletRequest request) {
String token = extractTokenFromHeader(authorization);
String jti = tokenService.extractTokenClaim(token, "jti");
// Invalidate refresh token
refreshTokenService.revokeRefreshTokenByJti(jti);
// Record logout
User user = (User) SecurityContextHolder.getContext()
.getAuthentication().getPrincipal();
userService.recordLogout(user, extractIpAddress(request));
// Clear security context
SecurityContextHolder.clearContext();
return ResponseEntity.ok(new MessageResponse("Logged out successfully"));
}
@PostMapping("/logout-all")
@PreAuthorize("isAuthenticated()")
public ResponseEntity<MessageResponse> logoutAllSessions(
Authentication authentication) {
User user = (User) authentication.getPrincipal();
refreshTokenService.revokeAllRefreshTokens(user);
SecurityContextHolder.clearContext();
return ResponseEntity.ok(new MessageResponse("Logged out from all devices"));
}
@GetMapping("/me")
@PreAuthorize("isAuthenticated()")
public ResponseEntity<UserProfileResponse> getCurrentUser(
Authentication authentication) {
User user = (User) authentication.getPrincipal();
UserProfileResponse response = new UserProfileResponse(
user.getId(),
user.getEmail(),
user.getFullName(),
user.getPhoneNumber(),
user.getRoles().stream()
.map(Role::getName)
.collect(Collectors.toSet()),
user.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toSet())
);
return ResponseEntity.ok(response);
}
@PostMapping("/change-password")
@PreAuthorize("isAuthenticated()")
public ResponseEntity<MessageResponse> changePassword(
@Valid @RequestBody ChangePasswordRequest request,
Authentication authentication) {
User user = (User) authentication.getPrincipal();
userService.changePassword(user, request);
// Invalidate all sessions except current
refreshTokenService.revokeAllRefreshTokensExceptCurrent(user, request.currentPassword());
return ResponseEntity.ok(new MessageResponse("Password changed successfully"));
}
private String extractTokenFromHeader(String authorization) {
if (authorization != null && authorization.startsWith("Bearer ")) {
return authorization.substring(7);
}
throw new IllegalArgumentException("Invalid authorization header");
}
private String extractDeviceInfo(HttpServletRequest request) {
String userAgent = request.getHeader("User-Agent");
// Parse user agent to extract browser and OS information
// Implementation depends on your requirements
return userAgent;
}
private String extractIpAddress(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
return xForwardedFor.split(",")[0].trim();
}
return request.getRemoteAddr();
}
}Registration Controller
@RestController
@RequestMapping("/api/register")
@Validated
public class RegistrationController {
private final UserService userService;
private final EmailService emailService;
@PostMapping
public ResponseEntity<MessageResponse> register(
@Valid @RequestBody RegistrationRequest request,
UriComponentsBuilder uriBuilder) {
// Check if user already exists
if (userService.existsByEmail(request.email())) {
throw new UserAlreadyExistsException("Email already registered");
}
// Create new user
User user = userService.createUser(request);
// Send verification email
String verificationToken = userService.generateEmailVerificationToken(user);
emailService.sendVerificationEmail(user, verificationToken);
URI location = uriBuilder.path("/api/users/{id}")
.buildAndExpand(user.getId())
.toUri();
return ResponseEntity.created(location)
.body(new MessageResponse("User registered successfully. Please check your email for verification."));
}
@PostMapping("/verify-email")
public ResponseEntity<MessageResponse> verifyEmail(
@Valid @RequestBody EmailVerificationRequest request) {
User user = userService.verifyEmail(request.token());
return ResponseEntity.ok(new MessageResponse("Email verified successfully"));
}
@PostMapping("/resend-verification")
public ResponseEntity<MessageResponse> resendVerification(
@Valid @RequestBody ResendVerificationRequest request) {
User user = userService.findByEmail(request.email());
if (user.isEmailVerified()) {
throw new EmailAlreadyVerifiedException("Email already verified");
}
String verificationToken = userService.generateEmailVerificationToken(user);
emailService.sendVerificationEmail(user, verificationToken);
return ResponseEntity.ok(new MessageResponse("Verification email sent"));
}
}Service Layer Implementation
JWT Token Service
@Service
@Transactional
@Slf4j
public class JwtTokenService {
private final JwtEncoder jwtEncoder;
private final JwtDecoder jwtDecoder;
private final JwtClaimsService claimsService;
private final BlacklistedTokenRepository blacklistedTokenRepository;
public JwtTokenService(JwtEncoder jwtEncoder,
JwtDecoder jwtDecoder,
JwtClaimsService claimsService,
BlacklistedTokenRepository blacklistedTokenRepository) {
this.jwtEncoder = jwtEncoder;
this.jwtDecoder = jwtDecoder;
this.claimsService = claimsService;
this.blacklistedTokenRepository = blacklistedTokenRepository;
}
public AccessTokenResponse generateAccessToken(User user) {
JwtClaimsSet claims = claimsService.createAccessTokenClaims(user);
String tokenValue = jwtEncoder.encode(
JwtEncoderParameters.from(claims)).getTokenValue();
return new AccessTokenResponse(
tokenValue,
claims.getExpiresAt().toEpochMilli(),
claims.getIssuedAt().toEpochMilli(),
claims.getClaimAsString("type")
);
}
public String extractTokenClaim(String token, String claimName) {
try {
Jwt jwt = jwtDecoder.decode(token);
return jwt.getClaimAsString(claimName);
} catch (JwtException e) {
throw new InvalidTokenException("Invalid token", e);
}
}
public boolean isTokenValid(String token) {
try {
// Check if token is blacklisted
String jti = extractTokenClaim(token, "jti");
if (blacklistedTokenRepository.existsByTokenId(jti)) {
return false;
}
// Decode and validate token
Jwt jwt = jwtDecoder.decode(token);
return jwt.getExpiresAt() != null &&
Instant.now().isBefore(jwt.getExpiresAt());
} catch (JwtException e) {
return false;
}
}
public void blacklistToken(String token) {
String jti = extractTokenClaim(token, "jti");
Instant expiresAt = Instant.ofEpochMilli(
Long.parseLong(extractTokenClaim(token, "exp")));
BlacklistedToken blacklistedToken = new BlacklistedToken(
jti, token, expiresAt);
blacklistedTokenRepository.save(blacklistedToken);
}
@Scheduled(fixedRate = 3600000) // Every hour
public void cleanupExpiredBlacklistedTokens() {
List<BlacklistedToken> expiredTokens = blacklistedTokenRepository
.findByExpiresAtBefore(Instant.now());
blacklistedTokenRepository.deleteAll(expiredTokens);
log.info("Cleaned up {} expired blacklisted tokens", expiredTokens.size());
}
}Refresh Token Service
@Service
@Transactional
@Slf4j
public class RefreshTokenService {
private final JwtTokenService jwtTokenService;
private final JwtClaimsService claimsService;
private final RefreshTokenRepository refreshTokenRepository;
private final UserRepository userRepository;
@Value("${jwt.refresh-token-expiration:P7D}")
private Duration refreshTokenExpiration;
public RefreshTokenResponse createRefreshToken(User user) {
// Revoke existing refresh tokens if too many
long activeTokens = refreshTokenRepository.countByUserAndExpiresAtAfter(user, Instant.now());
if (activeTokens >= 5) {
refreshTokenRepository.deleteOldestByUser(user);
}
JwtClaimsSet claims = claimsService.createRefreshTokenClaims(user);
String tokenValue = jwtTokenService.encodeToken(claims);
RefreshToken refreshToken = new RefreshToken(
tokenValue,
user,
claims.getExpiresAt(),
claims.getClaimAsString("sessionId"),
claims.getClaimAsString("jti")
);
refreshToken = refreshTokenRepository.save(refreshToken);
return new RefreshTokenResponse(
refreshToken.getToken(),
refreshToken.getExpiresAt().toEpochMilli()
);
}
public RefreshTokenResponse refreshToken(RefreshTokenRequest request) {
String refreshTokenValue = request.refreshToken();
// Validate refresh token
RefreshToken refreshToken = refreshTokenRepository
.findByToken(refreshTokenValue)
.orElseThrow(() -> new InvalidTokenException("Refresh token not found"));
if (refreshToken.isExpired()) {
refreshTokenRepository.delete(refreshToken);
throw new ExpiredTokenException("Refresh token expired");
}
if (!refreshToken.isActive()) {
throw new InvalidTokenException("Refresh token has been revoked");
}
User user = refreshToken.getUser();
if (!user.isEnabled() || !user.isAccountNonLocked()) {
throw new AccountDisabledException("Account is disabled or locked");
}
// Generate new access token
AccessTokenResponse accessToken = jwtTokenService.generateAccessToken(user);
// Optional: Rotate refresh token
if (shouldRotateRefreshToken(refreshToken)) {
refreshTokenRepository.delete(refreshToken);
return createRefreshToken(user);
}
return new RefreshTokenResponse(
accessToken.token(),
accessToken.expiresAt(),
refreshToken.getToken(),
refreshToken.getExpiresAt().toEpochMilli()
);
}
public void revokeRefreshToken(String token) {
refreshTokenRepository.findByToken(token)
.ifPresent(refreshToken -> {
refreshToken.setRevoked(true);
refreshToken.setRevokedAt(Instant.now());
refreshTokenRepository.save(refreshToken);
});
}
public void revokeRefreshTokenByJti(String jti) {
refreshTokenRepository.findByTokenId(jti)
.ifPresent(refreshToken -> {
refreshToken.setRevoked(true);
refreshToken.setRevokedAt(Instant.now());
refreshTokenRepository.save(refreshToken);
});
}
public void revokeAllRefreshTokens(User user) {
List<RefreshToken> tokens = refreshTokenRepository
.findByUserAndRevokedFalse(user);
tokens.forEach(token -> {
token.setRevoked(true);
token.setRevokedAt(Instant.now());
});
refreshTokenRepository.saveAll(tokens);
}
private boolean shouldRotateRefreshToken(RefreshToken refreshToken) {
// Rotate refresh token if older than 3 days
return refreshToken.getCreatedAt()
.isBefore(Instant.now().minus(3, ChronoUnit.DAYS));
}
@Scheduled(fixedRate = 86400000) // Daily
public void cleanupExpiredTokens() {
Instant cutoff = Instant.now().minus(7, ChronoUnit.DAYS);
List<RefreshToken> expiredTokens = refreshTokenRepository
.findByExpiresAtBefore(cutoff);
refreshTokenRepository.deleteAll(expiredTokens);
log.info("Cleaned up {} expired refresh tokens", expiredTokens.size());
}
}User Service
@Service
@Transactional
@Slf4j
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final RoleRepository roleRepository;
public User createUser(RegistrationRequest request) {
User user = User.builder()
.email(request.email())
.password(passwordEncoder.encode(request.password()))
.firstName(request.firstName())
.lastName(request.lastName())
.phoneNumber(request.phoneNumber())
.enabled(true)
.emailVerified(false)
.build();
// Assign default role
Role userRole = roleRepository.findByName("USER")
.orElseThrow(() -> new IllegalStateException("Default USER role not found"));
user.setRoles(Set.of(userRole));
return userRepository.save(user);
}
public void changePassword(User user, ChangePasswordRequest request) {
// Validate current password
if (!passwordEncoder.matches(request.currentPassword(), user.getPassword())) {
throw new InvalidPasswordException("Current password is incorrect");
}
// Validate new password
if (!request.newPassword().equals(request.confirmPassword())) {
throw new PasswordMismatchException("New passwords do not match");
}
// Update password
user.setPassword(passwordEncoder.encode(request.newPassword()));
userRepository.save(user);
// Force login from other devices
// This would trigger refresh token invalidation
}
public void recordLogin(User user, String deviceInfo, String ipAddress) {
UserLogin login = UserLogin.builder()
.user(user)
.loginAt(Instant.now())
.ipAddress(ipAddress)
.userAgent(deviceInfo)
.build();
user.addLogin(login);
userRepository.save(user);
}
public void recordLogout(User user, String ipAddress) {
Optional<UserLogin> lastLogin = user.getLogins().stream()
.filter(login -> login.getLogoutAt() == null)
.findFirst();
lastLogin.ifPresent(login -> {
login.setLogoutAt(Instant.now());
login.setLogoutIpAddress(ipAddress);
userRepository.save(user);
});
}
public String generateEmailVerificationToken(User user) {
String token = UUID.randomUUID().toString();
user.setEmailVerificationToken(token);
user.setEmailVerificationTokenExpiry(Instant.now().plus(24, ChronoUnit.HOURS));
userRepository.save(user);
return token;
}
@Transactional
public User verifyEmail(String token) {
User user = userRepository.findByEmailVerificationToken(token)
.orElseThrow(() -> new InvalidTokenException("Invalid verification token"));
if (user.getEmailVerificationTokenExpiry().isBefore(Instant.now())) {
throw new ExpiredTokenException("Verification token expired");
}
user.setEmailVerified(true);
user.setEmailVerificationToken(null);
user.setEmailVerificationTokenExpiry(null);
return userRepository.save(user);
}
}Advanced Security Configuration
Complete Security Configuration
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationEntryPoint authenticationEntryPoint;
private final JwtAccessDeniedHandler accessDeniedHandler;
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final CustomAuthenticationProvider authenticationProvider;
private final LogoutHandler logoutHandler;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.ignoringRequestMatchers("/api/auth/**", "/api/public/**"))
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling(exception -> exception
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler))
.headers(headers -> headers
.frameOptions().deny()
.contentTypeOptions().and()
.httpStrictTransportSecurity(hstsConfig -> hstsConfig
.maxAgeInSeconds(31536000)
.includeSubdomains(true))
.cacheControl())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**", "/api/public/**", "/actuator/health").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/manager/**").hasAnyRole("MANAGER", "ADMIN")
.requestMatchers("/api/users/me").authenticated()
.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.decoder(jwtDecoder())
.jwtAuthenticationConverter(jwtAuthenticationConverter())))
.authenticationProvider(authenticationProvider)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.logout(logout -> logout
.logoutUrl("/api/auth/logout")
.addLogoutHandler(logoutHandler)
.logoutSuccessHandler((request, response, authentication) ->
response.setStatus(HttpStatus.NO_CONTENT.value())))
.build();
}
@Bean
public JwtDecoder jwtDecoder() {
// Custom decoder with validation
return new CustomJwtDecoder(nimbusJwtDecoder(), jwtClaimsValidator());
}
@Bean
public NimbusJwtDecoder nimbusJwtDecoder() {
return NimbusJwtDecoder.withPublicKey(rsaPublicKey()).build();
}
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter();
authoritiesConverter.setAuthorityPrefix("ROLE_");
authoritiesConverter.setAuthoritiesClaimName("roles");
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
converter.setPrincipalClaimName("sub");
return converter;
}
}JWT Complete Configuration Guide
This guide consolidates all JWT configuration patterns for Spring Boot 3.5.x with Spring Security 6.x, covering JJWT library integration, Spring Security OAuth2 resource server configuration, and production-ready security settings.
Table of Contents
1. Application Properties 2. Security Configuration 3. JWT Service Configuration 4. OAuth2 Resource Server 5. Advanced Configuration 6. Performance Optimization 7. Troubleshooting
Application Properties
Basic JWT Configuration
# application.yml
jwt:
# Signing key (minimum 256 bits for HS256)
secret: ${JWT_SECRET:your-256-bit-secret-key-here-at-least-32-characters}
# Token expiration times
access-token-expiration: 900000 # 15 minutes in milliseconds
refresh-token-expiration: 604800000 # 7 days in milliseconds
# JWT issuer
issuer: ${JWT_ISSUER:your-application-name}
# Cookie settings for token storage
cookie:
name: ${JWT_COOKIE_NAME:jwt-token}
secure: ${JWT_COOKIE_SECURE:true} # true in production with HTTPS
http-only: true
same-site: ${JWT_COOKIE_SAME_SITE:strict}
max-age: ${JWT_COOKIE_MAX_AGE:86400}
domain: ${JWT_COOKIE_DOMAIN:your-domain.com}
path: /
# Spring Security OAuth2 Resource Server
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: ${JWT_ISSUER_URI:https://your-auth-server.com}
jwk-set-uri: ${JWT_JWK_SET_URI:https://your-auth-server.com/.well-known/jwks.json}
public-key-location: ${JWT_PUBLIC_KEY_LOCATION:classpath:public.pem}Environment-Specific Configuration
---
# Development profile
spring:
config:
activate:
on-profile: dev
jwt:
cookie:
secure: false
same-site: lax
logging:
level:
io.jsonwebtoken: DEBUG
org.springframework.security: DEBUG
---
# Production profile
spring:
config:
activate:
on-profile: prod
jwt:
cookie:
secure: true
same-site: strict
domain: api.yourdomain.com
secret: ${JWT_SECRET} # Must be provided via environment variableSecurity Configuration
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;
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.exceptionHandling(ex ->
ex.authenticationEntryPoint(jwtAuthenticationEntryPoint)
)
.authorizeHttpRequests(authz -> authz
// Public endpoints
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/actuator/health").permitAll()
// Swagger/OpenAPI
.requestMatchers(HttpMethod.GET, "/api-docs/**").permitAll()
.requestMatchers(HttpMethod.GET, "/swagger-ui/**").permitAll()
.requestMatchers(HttpMethod.GET, "/swagger-ui.html").permitAll()
// Admin endpoints
.requestMatchers("/api/admin/**").hasRole("ADMIN")
// All other endpoints require authentication
.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(getAllowedOrigins());
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(List.of("*"));
configuration.setAllowCredentials(true);
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
private List<String> getAllowedOrigins() {
return List.of(
"http://localhost:3000",
"http://localhost:4200",
"https://yourdomain.com"
);
}
}JWT Authentication Filter
@Component
@RequiredArgsConstructor
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
private final TokenBlacklistService blacklistService;
@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 ")) {
filterChain.doFilter(request, response);
return;
}
jwt = authHeader.substring(7);
// Check if token is blacklisted
if (blacklistService.isBlacklisted(jwt)) {
log.warn("Blacklisted JWT token detected");
filterChain.doFilter(request, response);
return;
}
try {
userEmail = jwtService.extractUsername(jwt);
} catch (JwtException e) {
log.error("Invalid JWT token: {}", e.getMessage());
filterChain.doFilter(request, response);
return;
}
if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.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);
}
}JWT Service Configuration
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 SecretKeyRepository secretKeyRepository;
private final CacheManager cacheManager;
public String generateToken(UserDetails userDetails) {
return generateToken(new HashMap<>(), userDetails);
}
public String generateToken(Map<String, Object> extraClaims, UserDetails userDetails) {
return buildToken(extraClaims, userDetails, accessTokenExpiration);
}
public String generateRefreshToken(UserDetails userDetails) {
return buildToken(new HashMap<>(), userDetails, refreshTokenExpiration);
}
private String buildToken(
Map<String, Object> extraClaims,
UserDetails userDetails,
long expiration
) {
SecretKey signingKey = getCurrentSigningKey();
return Jwts.builder()
.setClaims(extraClaims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.setIssuer(issuer)
.setId(UUID.randomUUID().toString())
.claim("authorities", userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList()))
.signWith(signingKey, SignatureAlgorithm.HS256)
.compact();
}
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
public boolean isTokenValid(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername())) && !isTokenExpired(token);
}
private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
private Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
private Claims extractAllClaims(String token) {
SecretKey signingKey = getCurrentSigningKey();
return Jwts.parserBuilder()
.setSigningKey(signingKey)
.requireIssuer(issuer)
.build()
.parseClaimsJws(token)
.getBody();
}
private SecretKey getCurrentSigningKey() {
return secretKeyRepository.findCurrentKey()
.map(SecretKeyEntity::getKey)
.orElseGet(() -> {
SecretKey newKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);
secretKeyRepository.save(new SecretKeyEntity(newKey, LocalDateTime.now()));
return newKey;
});
}
}Key Rotation Configuration
@Service
@RequiredArgsConstructor
@Slf4j
public class JwtKeyRotationService {
private final SecretKeyRepository keyRepository;
private final CacheManager cacheManager;
private final ApplicationEventPublisher eventPublisher;
@Value("${jwt.key-rotation.enabled:true}")
private boolean keyRotationEnabled;
@Value("${jwt.key-rotation.cron:0 0 0 * * ?}")
private String rotationCron;
@Scheduled(cron = "${jwt.key-rotation.cron}")
public void rotateKeys() {
if (!keyRotationEnabled) {
log.info("JWT key rotation is disabled");
return;
}
try {
SecretKey newKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);
SecretKeyEntity keyEntity = new SecretKeyEntity(newKey, LocalDateTime.now());
keyRepository.save(keyEntity);
// Clear cache
cacheManager.getCache("jwt-keys").clear();
// Publish key rotation event
eventPublisher.publishEvent(new KeyRotatedEvent(this, keyEntity.getId()));
log.info("JWT signing key rotated successfully");
} catch (Exception e) {
log.error("Failed to rotate JWT signing key", e);
}
}
public SecretKey getCurrentSigningKey() {
return keyRepository.findCurrentKey()
.map(SecretKeyEntity::getKey)
.orElseThrow(() -> new IllegalStateException("No signing key available"));
}
}OAuth2 Resource Server
Pure Resource Server Configuration
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class ResourceServerConfig {
@Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
private String issuerUri;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/actuator/health").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtDecoder(jwtDecoder())
)
);
return http.build();
}
@Bean
public JwtDecoder jwtDecoder() {
return JwtDecoders.fromIssuerLocation(issuerUri);
}
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter();
authoritiesConverter.setAuthorityPrefix("ROLE_");
authoritiesConverter.setAuthoritiesClaimName("roles");
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
return converter;
}
}Custom JWT Decoder
@Component
@RequiredArgsConstructor
public class CustomJwtDecoder implements JwtDecoder {
private final NimbusJwtDecoder nimbusJwtDecoder;
private final TokenBlacklistService blacklistService;
@Override
public Jwt decode(String token) throws JwtException {
// Check blacklist
if (blacklistService.isBlacklisted(token)) {
throw new BadJwtException("Token has been blacklisted");
}
// Decode token
Jwt jwt = nimbusJwtDecoder.decode(token);
// Custom validation
validateCustomClaims(jwt);
return jwt;
}
private void validateCustomClaims(Jwt jwt) {
// Add custom claim validation logic
Map<String, Object> claims = jwt.getClaims();
// Example: Validate tenant claim
if (!claims.containsKey("tenant_id")) {
throw new BadJwtException("Missing tenant_id claim");
}
// Example: Validate IP address
String tokenIp = (String) claims.get("ip_address");
if (tokenIp != null && !tokenIp.equals(getCurrentIpAddress())) {
throw new BadJwtException("Token IP mismatch");
}
}
}Advanced Configuration
Token Blacklisting
@Service
@RequiredArgsConstructor
public class TokenBlacklistService {
private final RedisTemplate<String, String> redisTemplate;
private static final String BLACKLIST_PREFIX = "blacklist:jwt:";
@Value("${jwt.blacklist.enabled:true}")
private boolean blacklistEnabled;
public void blacklistToken(String token) {
if (!blacklistEnabled) {
return;
}
try {
String tokenId = extractTokenId(token);
long expirationTime = calculateRemainingTime(token);
redisTemplate.opsForValue().set(
BLACKLIST_PREFIX + tokenId,
"1",
expirationTime,
TimeUnit.MILLISECONDS
);
log.info("Token blacklisted: {}", tokenId);
} catch (Exception e) {
log.error("Failed to blacklist token", e);
}
}
public boolean isBlacklisted(String token) {
if (!blacklistEnabled) {
return false;
}
try {
String tokenId = extractTokenId(token);
return Boolean.TRUE.equals(redisTemplate.hasKey(BLACKLIST_PREFIX + tokenId));
} catch (Exception e) {
log.error("Failed to check token blacklist", e);
return false;
}
}
private String extractTokenId(String token) {
// Extract JTI claim or generate from token hash
return DigestUtils.md5DigestAsHex(token.getBytes());
}
private long calculateRemainingTime(String token) {
// Parse token and calculate remaining time
try {
Jwt jwt = JwtHelper.decode(token);
Map<String, Object> claims = jwt.getClaims();
Long exp = (Long) claims.get("exp");
if (exp != null) {
return exp * 1000 - System.currentTimeMillis();
}
} catch (Exception e) {
log.error("Failed to calculate token expiration", e);
}
return 0;
}
}Rate Limiting
@Configuration
@EnableCaching
public class RateLimitConfig {
@Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager("login-attempts", "jwt-requests");
}
}
@Component
@RequiredArgsConstructor
public class JwtRateLimitService {
private final CacheManager cacheManager;
@Value("${jwt.rate-limit.enabled:true}")
private boolean rateLimitEnabled;
@Value("${jwt.rate-limit.max-attempts:5}")
private int maxAttempts;
@Value("${jwt.rate-limit.time-window:300000}") // 5 minutes
private long timeWindow;
public boolean isRateLimited(String identifier) {
if (!rateLimitEnabled) {
return false;
}
Cache cache = cacheManager.getCache("jwt-requests");
String key = "rate-limit:" + identifier;
AtomicInteger attempts = cache.get(key, AtomicInteger.class);
if (attempts == null) {
attempts = new AtomicInteger(0);
cache.put(key, attempts);
}
int currentAttempts = attempts.incrementAndGet();
if (currentAttempts >= maxAttempts) {
log.warn("Rate limit exceeded for identifier: {}", identifier);
return true;
}
return false;
}
}Performance Optimization
JWT Parsing Optimization
@Service
@RequiredArgsConstructor
@Slf4j
public class OptimizedJwtService {
private final CacheManager cacheManager;
private final SecretKeyRepository keyRepository;
@Cacheable(value = "jwt-parsing", key = "#token")
public Claims parseToken(String token) {
SecretKey key = getCurrentSigningKey();
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
}
@Cacheable(value = "signing-keys", key = "'current'")
public SecretKey getCurrentSigningKey() {
return keyRepository.findCurrentKey()
.map(SecretKeyEntity::getKey)
.orElseThrow(() -> new IllegalStateException("No signing key available"));
}
public String generateTokenOptimized(UserDetails userDetails) {
// Pre-calculate common claims
Map<String, Object> claims = new HashMap<>();
claims.put("authorities", userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList()));
claims.put("user_id", ((User) userDetails).getId());
claims.put("email", ((User) userDetails).getEmail());
return buildToken(claims, userDetails, accessTokenExpiration);
}
}Connection Pool Configuration
# application.yml
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 5
idle-timeout: 300000
max-lifetime: 1200000
connection-timeout: 20000
validation-timeout: 5000
leak-detection-threshold: 60000
redis:
lettuce:
pool:
max-active: 20
max-idle: 10
min-idle: 5
max-wait: 5000msTroubleshooting
Common Configuration Issues
1. Invalid Key Length
Error: The signing key's size is 184 bits which is not secure enough for the HS256 algorithm.
Solution: Use a key of at least 256 bits (32 characters) for HS256.2. Clock Skew Issues
// Add clock skew tolerance
Jwts.parserBuilder()
.setAllowedClockSkewSeconds(60) // 60 seconds tolerance
.build()
.parseClaimsJws(token);3. Issuer Mismatch
// Always set and validate issuer
Jwts.parserBuilder()
.requireIssuer("your-app-name")
.build()
.parseClaimsJws(token);Debug Configuration
# application.yml
logging:
level:
io.jsonwebtoken: DEBUG
org.springframework.security: DEBUG
org.springframework.security.oauth2: DEBUG
# Enable request/response logging
spring:
mvc:
log-request-details: trueHealth Check Endpoint
@Component
public class JwtHealthIndicator implements HealthIndicator {
private final JwtService jwtService;
@Override
public Health health() {
try {
// Test JWT signing and parsing
String testToken = jwtService.generateTestToken();
boolean isValid = jwtService.validateToken(testToken);
if (isValid) {
return Health.up()
.withDetail("jwt", "Service is working")
.build();
} else {
return Health.down()
.withDetail("jwt", "Token validation failed")
.build();
}
} catch (Exception e) {
return Health.down()
.withDetail("jwt", "Service error: " + e.getMessage())
.build();
}
}
}References
Spring Security JWT Skill Structure
spring-boot-security-jwt/
├── SKILL.md # Main skill documentation
├── README.md # User-friendly overview and quick start
├── structure.md # This file - skill structure documentation
│
├── references/ # Comprehensive reference documentation
│ ├── jwt-configuration.md # Complete JWT setup and configuration
│ ├── examples.md # Real-world implementation examples
│ ├── authorization-patterns.md # RBAC, ABAC, and permission models
│ ├── token-management.md # Refresh tokens, rotation, and revocation
│ ├── microservices-security.md # Inter-service authentication
│ ├── oauth2-integration.md # OAuth2 provider integration
│ ├── testing-jwt-security.md # Unit and integration testing
│ ├── performance-optimization.md # Caching and performance tuning
│ ├── security-hardening.md # Security best practices
│ └── troubleshooting.md # Common issues and solutions
│
├── scripts/ # Utility scripts
│ └── test-jwt-setup.sh # Automated testing script
│
└── assets/ # Supporting assets and tools
└── generate-jwt-keys.sh # Key generation utilitySkill Coverage
Core Features (SKILL.md)
- JWT generation and validation with multiple algorithms (RSA, HMAC, ECDSA)
- Bearer token and cookie-based authentication patterns
- Database-backed and OAuth2 provider integration
- Both RBAC and permission-based access control
- Advanced patterns: refresh tokens, token rotation, multi-tenancy
Configuration (jwt-configuration.md)
- Comprehensive dependency management
- Key configuration for RSA, HMAC, and ECDSA
- Custom claims and validation
- Application properties and YAML configurations
- Key rotation support
Implementation Examples (examples.md)
- Complete application setup
- Domain models (User, Role, Permission entities)
- Authentication and registration controllers
- Service layer implementations
- Advanced security configuration
Authorization Patterns (authorization-patterns.md)
- Hierarchical role structures
- Custom permission evaluators
- Attribute-Based Access Control (ABAC)
- Time-based access control
- Location-based restrictions
- Organizational access control
Token Management (token-management.md)
- Secure refresh token storage
- Token rotation strategies
- Token blacklisting
- Session management
- Distributed security events
Microservices Security (microservices-security.md)
- Inter-service authentication
- API gateway patterns
- Distributed token validation
- Service mesh integration (Istio)
- Circuit breaker patterns
OAuth2 Integration (oauth2-integration.md)
- Multi-provider support
- Custom OAuth2 user service
- Token exchange patterns
- Delegated authorization
- Client registration API
Testing (testing-jwt-security.md)
- Unit testing JWT services
- Integration tests with Testcontainers
- Security testing with OWASP ZAP
- Performance testing
- Custom security test utilities
Performance Optimization (performance-optimization.md)
- Caching strategies for token validation
- Database optimization
- Async processing
- Monitoring and metrics
- Resource optimization
Security Hardening (security-hardening.md)
- Secure configuration practices
- Attack prevention (brute force, XSS, SQL injection)
- Key management best practices
- Security monitoring
- GDPR compliance features
Troubleshooting (troubleshooting.md)
- Common JWT token problems
- Authentication flow debugging
- Configuration validation
- Quick fix scripts
- Debugging checklist
Integration Points
This skill integrates with:
1. Spring Boot Actuator - For security monitoring and health checks 2. Spring Data JPA - For user and token persistence 3. Spring Cache - For token validation caching 4. JUnit Testing - For comprehensive security testing 5. LangChain4j - For AI-powered security analysis (if needed)
Quick Start Flow
1. Review README.md for overview 2. Generate keys using ./assets/generate-jwt-keys.sh 3. Implement basic JWT using SKILL.md quick start 4. Reference jwt-configuration.md for detailed setup 5. Use examples.md for implementation patterns 6. Apply security-hardening.md for production 7. Run tests with ./scripts/test-jwt-setup.sh 8. Monitor with troubleshooting.md as needed
Key Benefits
1. Comprehensive Coverage: From basic JWT to advanced microservices patterns 2. Production Ready: Includes security hardening and performance optimization 3. Well Documented: Each aspect has detailed reference documentation 4. Practical Examples: Real-world code implementations 5. Testing Included: Automated test suite and testing strategies 6. Troubleshooting Guide: Common issues and solutions
Maintenance
This skill should be updated when:
- New Spring Security features are released
- New security vulnerabilities are discovered
- Performance optimizations are identified
- Community feedback suggests improvements
- Integration patterns evolve
Related skills
Forks & variants (1)
Spring Boot Security Jwt has 1 known copy in the catalog totaling 21 installs. They canonicalize to this original listing.
- giuseppe-trisciuoglio - 21 installs
How it compares
Use spring-boot-security-jwt when you need end-to-end Spring Security 6 filter wiring rather than isolated JJWT encode/decode snippets.
FAQ
Who is spring-boot-security-jwt for?
Developers and software engineers working with spring-boot-security-jwt patterns described in the skill documentation.
When should I use spring-boot-security-jwt?
When Provides JWT authentication and authorization patterns for Spring Boot 3.5.x covering token generation with JJWT, Bearer/cookie authentication, database/OAuth2 integration, and RBA.
Is spring-boot-security-jwt safe to install?
Review the Security Audits panel on this page before installing in production.