Java Security Patterns
Spring Security 6 Configuration
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable) // stateless API
.sessionManagement(s -> s.sessionCreationPolicy(STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**", "/actuator/health").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
.exceptionHandling(ex -> ex
.authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
.accessDeniedHandler(new BearerTokenAccessDeniedHandler())
)
.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12);
}
}
JWT with Nimbus (Spring Security)
@Configuration
public class JwtConfig {
@Value("${app.jwt.secret}")
private String jwtSecret;
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withSecretKey(secretKey()).build();
}
@Bean
public JwtEncoder jwtEncoder() {
return new NimbusJwtEncoder(new ImmutableSecret<>(secretKey()));
}
private SecretKey secretKey() {
return new SecretKeySpec(jwtSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
}
}
@Service
@RequiredArgsConstructor
public class TokenService {
private final JwtEncoder encoder;
public String generateToken(UserDetails user) {
Instant now = Instant.now();
JwtClaimsSet claims = JwtClaimsSet.builder()
.issuer("my-service")
.issuedAt(now)
.expiresAt(now.plus(1, ChronoUnit.HOURS))
.subject(user.getUsername())
.claim("roles", user.getAuthorities().stream()
.map(GrantedAuthority::getAuthority).toList())
.build();
return encoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
}
}
Method-Level Security
@Service
public class DocumentService {
@PreAuthorize("hasRole('ADMIN') or #userId == authentication.name")
public Document getDocument(Long id, String userId) { ... }
@PostAuthorize("returnObject.ownerId == authentication.name")
public Document findById(Long id) { ... }
@PreAuthorize("hasAuthority('DOCUMENT_WRITE')")
public Document create(CreateDocumentRequest request) { ... }
@Secured("ROLE_ADMIN")
public void deleteDocument(Long id) { ... }
}
CORS Configuration
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("https://app.example.com"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
config.setAllowCredentials(true);
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return source;
}
Input Validation & SQL Injection Prevention
// Always use parameterized queries
@Query("SELECT u FROM User u WHERE u.email = :email")
Optional<User> findByEmail(@Param("email") String email);
// Never concatenate user input
// BAD: "SELECT * FROM users WHERE name = '" + name + "'"
// GOOD: jdbcTemplate.queryForObject("SELECT * FROM users WHERE name = ?", User.class, name)
// Validate at boundary
@PostMapping("/users")
public UserResponse create(@Valid @RequestBody CreateUserRequest request) { ... }
public record CreateUserRequest(
@NotBlank @Size(max = 100) String name,
@NotBlank @Email String email,
@Pattern(regexp = "^(?=.*[A-Z])(?=.*\\d).{8,}$") String password
) {}
Secrets Management
// Never hardcode secrets — read from environment
@Value("${app.jwt.secret:#{null}}")
private String jwtSecret;
@PostConstruct
public void validateSecrets() {
if (jwtSecret == null || jwtSecret.length() < 32) {
throw new IllegalStateException("JWT secret must be at least 32 characters");
}
}
// Use Spring Cloud Vault or AWS Secrets Manager in production
@Configuration
@EnableVaultConfiguration
public class VaultConfig extends AbstractVaultConfiguration {
@Override
public VaultEndpoint vaultEndpoint() {
return VaultEndpoint.from(URI.create(System.getenv("VAULT_ADDR")));
}
}
Key Rules
BCryptPasswordEncoder(12) — strength 12 is the minimum for production; never store plain or MD5 passwords
- Disable CSRF only for stateless REST APIs; always enable for session-based apps
@PreAuthorize with SpEL is preferred over role checks in business logic
- Never log passwords, tokens, or PII — use structured logging with explicit field exclusions
- Validate all user input at the controller boundary with
@Valid — never trust raw request data