Spring Security Configuration
Spring Security 6 patterns: SecurityFilterChain DSL, JWT filter, BCrypt password encoding, method-level security, and CORS configuration.
1. SecurityFilterChain
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
private final JwtAuthFilter jwtFilter;
public SecurityConfig(JwtAuthFilter jwtFilter) {
this.jwtFilter = jwtFilter;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable()) // stateless API โ no CSRF
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.cors(cors -> cors.configurationSource(corsConfigSource()))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**", "/public/**").permitAll()
.requestMatchers(HttpMethod.GET, "/articles/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.exceptionHandling(ex -> ex
.authenticationEntryPoint((req, res, e) ->
res.sendError(HttpServletResponse.SC_UNAUTHORIZED, e.getMessage()))
.accessDeniedHandler((req, res, e) ->
res.sendError(HttpServletResponse.SC_FORBIDDEN, e.getMessage()))
)
.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12);
}
}
2. JWT Authentication Filter
@Component
public class JwtAuthFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain) throws ServletException, IOException {
String header = req.getHeader("Authorization");
if (header == null || !header.startsWith("Bearer ")) {
chain.doFilter(req, res);
return;
}
String token = header.substring(7);
try {
String username = jwtService.extractUsername(token);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails user = userDetailsService.loadUserByUsername(username);
if (jwtService.isValid(token, user)) {
var auth = new UsernamePasswordAuthenticationToken(
user, null, user.getAuthorities());
auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(req));
SecurityContextHolder.getContext().setAuthentication(auth);
}
}
} catch (JwtException e) {
res.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid token");
return;
}
chain.doFilter(req, res);
}
}
3. JwtService
@Service
public class JwtService {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration:3600}")
private long expirationSeconds;
private SecretKey getKey() {
return Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret));
}
public String generate(UserDetails user) {
Map<String, Object> claims = new HashMap<>();
claims.put("roles", user.getAuthorities().stream()
.map(GrantedAuthority::getAuthority).toList());
return Jwts.builder()
.claims(claims)
.subject(user.getUsername())
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + expirationSeconds * 1000))
.signWith(getKey())
.compact();
}
public String extractUsername(String token) {
return Jwts.parser().verifyWith(getKey()).build()
.parseSignedClaims(token).getPayload().getSubject();
}
public boolean isValid(String token, UserDetails user) {
return extractUsername(token).equals(user.getUsername()) && !isExpired(token);
}
}
4. Method Security
// Enable with @EnableMethodSecurity on config class
@RestController
@RequestMapping("/articles")
public class ArticleController {
@GetMapping("/{id}")
@PreAuthorize("hasRole('USER') or hasRole('ADMIN')")
public ArticleDto getById(@PathVariable Long id) { ... }
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN') or @articleSecurity.isOwner(#id, authentication)")
public void delete(@PathVariable Long id) { ... }
@PostMapping
@Secured("ROLE_USER") // simpler alternative to @PreAuthorize
public ArticleDto create(@Valid @RequestBody CreateArticleRequest req) { ... }
}
// Custom security expression component
@Component("articleSecurity")
public class ArticleSecurity {
private final ArticleRepository repo;
public boolean isOwner(Long articleId, Authentication auth) {
return repo.findById(articleId)
.map(a -> a.getAuthor().getUsername().equals(auth.getName()))
.orElse(false);
}
}
5. CORS Configuration
@Bean
public CorsConfigurationSource corsConfigSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOriginPatterns(List.of("https://*.example.com", "http://localhost:*"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
config.setAllowedHeaders(List.of("Authorization", "Content-Type", "X-Requested-With"));
config.setExposedHeaders(List.of("X-Total-Count", "X-Page-Count"));
config.setAllowCredentials(true);
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
6. URL Authorization Expressions
| Expression | Meaning |
|---|---|
| permitAll() | Allow everyone |
| denyAll() | Deny everyone |
| authenticated() | Must be logged in |
| hasRole("ADMIN") | Has ROLE_ADMIN authority |
| hasAnyRole("A","B") | Has any of the roles |
| hasAuthority("READ") | Has exact authority string |
| anonymous() | Must be anonymous |