Skip to content
Home / Skills / Spring / Security
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();
}

References