SP
Security
Spring security v1.0.0
Spring Security
Overview
Spring Security provides comprehensive security services for Java applications including authentication, authorization, and protection against common exploits. This skill covers securing REST APIs, OAuth2/OIDC integration, method-level security, and security testing.
Key Concepts
Security Filter Chain
┌─────────────────────────────────────────────────────────────┐
│ Spring Security Filter Chain │
├─────────────────────────────────────────────────────────────┤
│ │
│ HTTP Request │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ DisableEncodeUrlFilter │ │
│ │ WebAsyncManagerIntegrationFilter │ │
│ │ SecurityContextHolderFilter │ │
│ │ HeaderWriterFilter │ │
│ │ CorsFilter │ │
│ │ CsrfFilter │ │
│ │ LogoutFilter │ │
│ │ OAuth2AuthorizationRequestRedirectFilter │ │
│ │ OAuth2LoginAuthenticationFilter │ │
│ │ UsernamePasswordAuthenticationFilter │ │
│ │ BasicAuthenticationFilter │ │
│ │ BearerTokenAuthenticationFilter │ │
│ │ RequestCacheAwareFilter │ │
│ │ SecurityContextHolderAwareRequestFilter │ │
│ │ AnonymousAuthenticationFilter │ │
│ │ SessionManagementFilter │ │
│ │ ExceptionTranslationFilter │ │
│ │ AuthorizationFilter │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Application │
│ │
└─────────────────────────────────────────────────────────────┘
Best Practices
1. Use Method Security
Apply @PreAuthorize at the service layer for fine-grained control.
2. Externalize Security Configuration
Don’t hardcode roles or permissions in code.
3. Always Use HTTPS
Configure HTTPS and HSTS in production.
4. Implement Proper Session Management
Use stateless sessions for APIs, configure timeouts for web apps.
5. Log Security Events
Audit authentication attempts and authorization failures.
Code Examples
Example 1: JWT Authentication Configuration
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
private String issuerUri;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
// Disable CSRF for stateless API
.csrf(csrf -> csrf.disable())
// CORS configuration
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
// Stateless session
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// Authorization rules
.authorizeHttpRequests(auth -> auth
// Public endpoints
.requestMatchers("/actuator/health", "/actuator/info").permitAll()
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/docs/**", "/swagger-ui/**").permitAll()
// Role-based access
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.DELETE).hasRole("ADMIN")
// Scope-based access
.requestMatchers("/api/orders/**").hasAuthority("SCOPE_orders")
// All other requests require authentication
.anyRequest().authenticated()
)
// JWT validation
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(jwtAuthenticationConverter())
.decoder(jwtDecoder())
)
)
// Security headers
.headers(headers -> headers
.contentSecurityPolicy(csp ->
csp.policyDirectives("default-src 'self'"))
.frameOptions(frame -> frame.deny())
.httpStrictTransportSecurity(hsts -> hsts
.includeSubDomains(true)
.maxAgeInSeconds(31536000))
)
// Exception handling
.exceptionHandling(ex -> ex
.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
.accessDeniedHandler(accessDeniedHandler())
)
.build();
}
@Bean
public JwtDecoder jwtDecoder() {
NimbusJwtDecoder decoder = JwtDecoders.fromIssuerLocation(issuerUri);
// Custom validation
OAuth2TokenValidator<Jwt> validator = new DelegatingOAuth2TokenValidator<>(
JwtValidators.createDefaultWithIssuer(issuerUri),
new AudienceValidator("order-service"),
new JwtTimestampValidator()
);
decoder.setJwtValidator(validator);
return decoder;
}
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter();
authoritiesConverter.setAuthorityPrefix("ROLE_");
authoritiesConverter.setAuthoritiesClaimName("roles");
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(jwt -> {
Collection<GrantedAuthority> authorities = authoritiesConverter.convert(jwt);
// Add scope authorities
String scopes = jwt.getClaimAsString("scope");
if (scopes != null) {
Collection<GrantedAuthority> scopeAuthorities = Arrays.stream(scopes.split(" "))
.map(s -> new SimpleGrantedAuthority("SCOPE_" + s))
.collect(Collectors.toList());
authorities.addAll(scopeAuthorities);
}
return authorities;
});
return converter;
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("https://app.company.com"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("*"));
config.setExposedHeaders(List.of("X-Request-Id"));
config.setAllowCredentials(true);
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return source;
}
@Bean
public AccessDeniedHandler accessDeniedHandler() {
return (request, response, accessDeniedException) -> {
log.warn("Access denied: {} for {}",
accessDeniedException.getMessage(),
request.getRequestURI());
response.setStatus(HttpStatus.FORBIDDEN.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getWriter().write("""
{"error": "ACCESS_DENIED", "message": "Insufficient permissions"}
""");
};
}
}
// Custom audience validator
public class AudienceValidator implements OAuth2TokenValidator<Jwt> {
private final String expectedAudience;
public AudienceValidator(String expectedAudience) {
this.expectedAudience = expectedAudience;
}
@Override
public OAuth2TokenValidatorResult validate(Jwt jwt) {
List<String> audiences = jwt.getAudience();
if (audiences != null && audiences.contains(expectedAudience)) {
return OAuth2TokenValidatorResult.success();
}
OAuth2Error error = new OAuth2Error(
"invalid_token",
"Required audience not found: " + expectedAudience,
null
);
return OAuth2TokenValidatorResult.failure(error);
}
}
Example 2: Method-Level Security
@Service
@PreAuthorize("isAuthenticated()") // Class-level default
public class OrderService {
private final OrderRepository orderRepository;
// Role-based authorization
@PreAuthorize("hasRole('ADMIN') or hasRole('ORDER_MANAGER')")
public List<Order> getAllOrders() {
return orderRepository.findAll();
}
// Permission check with SpEL
@PreAuthorize("hasPermission(#orderId, 'Order', 'read')")
public Order getOrder(OrderId orderId) {
return orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
}
// Custom security service
@PreAuthorize("@orderSecurity.canModify(#orderId, authentication)")
public Order updateOrder(OrderId orderId, UpdateOrderCommand command) {
Order order = getOrder(orderId);
order.update(command);
return orderRepository.save(order);
}
// SpEL with principal
@PreAuthorize("#command.customerId == authentication.principal.customerId")
public Order createOrder(CreateOrderCommand command) {
return orderRepository.save(Order.create(command));
}
// Post-authorize (check after method execution)
@PostAuthorize("returnObject.customerId == authentication.principal.customerId")
public Order getOrderPostAuth(OrderId orderId) {
return orderRepository.findById(orderId).orElseThrow();
}
// Post-filter collection
@PostFilter("filterObject.customerId == authentication.principal.customerId")
public List<Order> getMyOrders() {
return orderRepository.findAll();
}
// Combining conditions
@PreAuthorize("""
hasRole('ADMIN') or
(hasRole('CUSTOMER') and @orderSecurity.isOwner(#orderId, authentication))
""")
public void cancelOrder(OrderId orderId) {
Order order = getOrder(orderId);
order.cancel();
orderRepository.save(order);
}
}
// Custom security service for complex authorization
@Component("orderSecurity")
@RequiredArgsConstructor
public class OrderSecurityService {
private final OrderRepository orderRepository;
public boolean canModify(OrderId orderId, Authentication authentication) {
if (isAdmin(authentication)) {
return true;
}
String customerId = extractCustomerId(authentication);
return orderRepository.existsByIdAndCustomerId(orderId, customerId)
&& !isOrderCompleted(orderId);
}
public boolean isOwner(OrderId orderId, Authentication authentication) {
String customerId = extractCustomerId(authentication);
return orderRepository.existsByIdAndCustomerId(orderId, customerId);
}
private boolean isAdmin(Authentication authentication) {
return authentication.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));
}
private String extractCustomerId(Authentication authentication) {
if (authentication.getPrincipal() instanceof Jwt jwt) {
return jwt.getClaimAsString("customer_id");
}
throw new IllegalStateException("Unknown principal type");
}
}
Example 3: Custom Permission Evaluator
@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {
private final OrderRepository orderRepository;
private final ResourceAccessRepository accessRepository;
@Override
public boolean hasPermission(Authentication auth, Object targetDomainObject,
Object permission) {
if (targetDomainObject == null) {
return false;
}
String permissionName = permission.toString();
if (targetDomainObject instanceof Order order) {
return hasOrderPermission(auth, order, permissionName);
}
return false;
}
@Override
public boolean hasPermission(Authentication auth, Serializable targetId,
String targetType, Object permission) {
return switch (targetType) {
case "Order" -> hasOrderPermission(auth, (OrderId) targetId, permission.toString());
case "Product" -> hasProductPermission(auth, (ProductId) targetId, permission.toString());
default -> false;
};
}
private boolean hasOrderPermission(Authentication auth, OrderId orderId, String permission) {
// Admin can do anything
if (hasRole(auth, "ADMIN")) {
return true;
}
// Check resource-specific access
String userId = getUserId(auth);
return accessRepository.hasAccess(userId, "Order", orderId.toString(), permission);
}
private boolean hasRole(Authentication auth, String role) {
return auth.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_" + role));
}
}
@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {
@Bean
public MethodSecurityExpressionHandler methodSecurityExpressionHandler(
CustomPermissionEvaluator permissionEvaluator) {
DefaultMethodSecurityExpressionHandler handler =
new DefaultMethodSecurityExpressionHandler();
handler.setPermissionEvaluator(permissionEvaluator);
return handler;
}
}
Example 4: Security Context Utilities
@Component
public class SecurityContextHelper {
public Optional<String> getCurrentUserId() {
return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication())
.filter(Authentication::isAuthenticated)
.map(this::extractUserId);
}
public String requireCurrentUserId() {
return getCurrentUserId()
.orElseThrow(() -> new UnauthorizedException("No authenticated user"));
}
public Set<String> getCurrentRoles() {
return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication())
.map(auth -> auth.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.filter(a -> a.startsWith("ROLE_"))
.map(a -> a.substring(5))
.collect(Collectors.toSet()))
.orElse(Set.of());
}
public boolean hasRole(String role) {
return getCurrentRoles().contains(role);
}
public boolean hasAnyRole(String... roles) {
Set<String> currentRoles = getCurrentRoles();
return Arrays.stream(roles).anyMatch(currentRoles::contains);
}
private String extractUserId(Authentication authentication) {
Object principal = authentication.getPrincipal();
if (principal instanceof Jwt jwt) {
return jwt.getSubject();
} else if (principal instanceof UserDetails userDetails) {
return userDetails.getUsername();
} else if (principal instanceof String s) {
return s;
}
throw new IllegalStateException("Unknown principal type: " + principal.getClass());
}
// For async operations - capture context
public Runnable wrapWithSecurityContext(Runnable runnable) {
SecurityContext context = SecurityContextHolder.getContext();
return () -> {
try {
SecurityContextHolder.setContext(context);
runnable.run();
} finally {
SecurityContextHolder.clearContext();
}
};
}
}
// Auditing with security context
@Component
public class SecurityAuditorAware implements AuditorAware<String> {
private final SecurityContextHelper securityHelper;
@Override
public Optional<String> getCurrentAuditor() {
return securityHelper.getCurrentUserId();
}
}
Example 5: Security Testing
@WebMvcTest(OrderController.class)
@Import(SecurityConfig.class)
class OrderControllerSecurityTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private OrderService orderService;
@Test
void shouldRejectUnauthenticated() throws Exception {
mockMvc.perform(get("/api/orders"))
.andExpect(status().isUnauthorized());
}
@Test
@WithMockUser(roles = "CUSTOMER")
void shouldAllowAuthenticatedUser() throws Exception {
when(orderService.getMyOrders()).thenReturn(List.of());
mockMvc.perform(get("/api/orders"))
.andExpect(status().isOk());
}
@Test
@WithMockUser(roles = "CUSTOMER")
void shouldDenyAdminEndpoint() throws Exception {
mockMvc.perform(get("/api/admin/orders"))
.andExpect(status().isForbidden());
}
@Test
@WithMockUser(roles = "ADMIN")
void shouldAllowAdminEndpoint() throws Exception {
when(orderService.getAllOrders()).thenReturn(List.of());
mockMvc.perform(get("/api/admin/orders"))
.andExpect(status().isOk());
}
@Test
void shouldAcceptValidJwt() throws Exception {
String token = generateTestToken("user123", List.of("ROLE_CUSTOMER"));
mockMvc.perform(get("/api/orders")
.header("Authorization", "Bearer " + token))
.andExpect(status().isOk());
}
@Test
void shouldRejectExpiredJwt() throws Exception {
String expiredToken = generateExpiredToken();
mockMvc.perform(get("/api/orders")
.header("Authorization", "Bearer " + expiredToken))
.andExpect(status().isUnauthorized());
}
}
// Custom annotation for JWT tests
@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithMockJwtSecurityContextFactory.class)
public @interface WithMockJwt {
String subject() default "test-user";
String[] roles() default {"CUSTOMER"};
String[] scopes() default {"orders"};
}
public class WithMockJwtSecurityContextFactory
implements WithSecurityContextFactory<WithMockJwt> {
@Override
public SecurityContext createSecurityContext(WithMockJwt annotation) {
SecurityContext context = SecurityContextHolder.createEmptyContext();
Jwt jwt = Jwt.withTokenValue("mock-token")
.header("alg", "RS256")
.subject(annotation.subject())
.claim("roles", List.of(annotation.roles()))
.claim("scope", String.join(" ", annotation.scopes()))
.issuedAt(Instant.now())
.expiresAt(Instant.now().plusHours(1))
.build();
Collection<GrantedAuthority> authorities =
Arrays.stream(annotation.roles())
.map(r -> new SimpleGrantedAuthority("ROLE_" + r))
.collect(Collectors.toList());
JwtAuthenticationToken authentication =
new JwtAuthenticationToken(jwt, authorities);
context.setAuthentication(authentication);
return context;
}
}
// Usage
@Test
@WithMockJwt(subject = "customer-123", roles = {"CUSTOMER"}, scopes = {"orders", "products"})
void shouldAllowWithCustomJwt() throws Exception {
mockMvc.perform(get("/api/orders"))
.andExpect(status().isOk());
}
Anti-Patterns
❌ Hardcoding Roles
// WRONG
if (user.getRole().equals("ADMIN")) {
// Do admin stuff
}
// ✅ CORRECT - Use Spring Security
@PreAuthorize("hasRole('ADMIN')")
public void adminOperation() { }
❌ Checking Security After Loading Data
// WRONG - data already loaded
public Order getOrder(OrderId id) {
Order order = repository.findById(id).orElseThrow();
if (!order.getCustomerId().equals(currentUser.getId())) {
throw new AccessDeniedException("Not your order");
}
return order;
}
// ✅ CORRECT - Check before or use query
@PreAuthorize("@orderSecurity.canAccess(#id)")
public Order getOrder(OrderId id) {
return repository.findById(id).orElseThrow();
}