spring-security-core

star 4

Implements Spring Security 6.x filter chain configuration, JWT authentication filters, method-level security with @EnableMethodSecurity, password encoding, and CORS/CSRF handling for production Spring Boot applications.

paulpas By paulpas schedule Updated 6/4/2026

name: spring-security-core description: Implements Spring Security 6.x filter chain configuration, JWT authentication filters, method-level security with @EnableMethodSecurity, password encoding, and CORS/CSRF handling for production Spring Boot applications. license: MIT compatibility: opencode metadata: version: "1.0.0" domain: coding triggers: spring security, jwt authentication, security filter chain, method security, password encoder, cors csrf, authorization rules, spring security core archetypes: - tactical - diagnostic anti_triggers: - brainstorming - vague ideation response_profile: verbosity: low directive_strength: high abstraction_level: operational role: implementation scope: implementation output-format: code content-types: - code - config - do-dont - patterns related-skills: spring-boot-auto-config, spring-data-jpa


Spring Security Core & JWT Authentication

Implements production-grade Spring Security 6.x (Spring Boot 3.x) security configurations including SecurityFilterChain bean definition without the deprecated WebSecurityConfigurerAdapter, JWT token authentication filters, method-level authorization with @EnableMethodSecurity, password encoding strategies, and CORS/CSRF handling. When loaded, this skill makes the model write secure request matching rules, custom authentication providers, and role-based authorization annotations using modern Spring Security APIs.

TL;DR Checklist

  • Confirm WebSecurityConfigurerAdapter is never used — all configuration goes through SecurityFilterChain beans
  • Verify JWT filter extends OncePerRequestFilter or implements AuthenticationFilter interface
  • Ensure PasswordEncoder bean uses BCrypt (BCryptPasswordEncoder) or Argon2 (Argon2PasswordEncoder)
  • Check that @EnableMethodSecurity is present and @PreAuthorize / @Secured annotations guard endpoints
  • Validate CORS configuration is explicit per origin (never .cors().configurationSource(request -> { cors.setAllowedOrigins(List.of("*")); }))
  • Confirm CSRF is disabled for stateless JWT APIs but enabled for form-based or session authentication

When to Use

Use this skill when:

  • Configuring SecurityFilterChain beans to define authorization rules per URL pattern in Spring Boot 3.x
  • Implementing JWT-based stateless authentication with a custom filter in the security filter chain
  • Adding method-level security annotations (@PreAuthorize, @Secured, @RolesAllowed) on service or controller methods
  • Setting up password encoding with BCrypt for user credential storage and verification
  • Configuring CORS policies for cross-origin API access with specific origin whitelisting

When NOT to Use

Avoid this skill for:

  • OAuth2 / OpenID Connect flows — use the spring-security-oauth2-resource-server or spring-security-oauth2-client starters instead (this skill covers JWT-based direct authentication only)
  • LDAP or SAML enterprise identity integration — these require dedicated Spring Security modules beyond core filter chain configuration
  • Simple in-memory user authentication for testing — use spring.security.user.name/password auto-configuration without manual beans
  • Authorization logic that belongs in domain service methods (business rules) — security annotations should guard the entry point, not replace business-level checks

Core Workflow

  1. Define the SecurityFilterChain Bean — Create a @Bean method returning SecurityFilterChain. Apply .authorizeHttpRequests() with .requestMatchers("/public/**").permitAll(), .requestMatchers("/admin/**").hasRole("ADMIN"), and .anyRequest().authenticated() as the final catch-all. Call .csrf(csrf -> csrf.disable()) for stateless JWT APIs or configure CSRF token validation for form-based auth. Enable CORS via .cors(cors -> cors.configurationSource(...)) with explicit allowed origins. Always register a SecurityFilterChain bean — never use the deprecated WebSecurityConfigurerAdapter which was removed in Spring Security 5.7+ and fully dropped in 6.x.

    Checkpoint: Every URL pattern must have an explicit authorization decision — .anyRequest().authenticated() should be the last matcher. Verify that .requestMatchers() uses exact paths or ant patterns (not regex), since requestMatchers replaced regexMatchers for performance reasons. Confirm CSRF is disabled only when the application is truly stateless (no session, no form submissions).

  2. Implement the JWT Authentication Filter — Create a class extending OncePerRequestFilter that intercepts requests with an Authorization: Bearer <token> header. Parse and validate the JWT using a library like jjwt (io.jsonwebtoken). Extract the username and authorities from the token claims, create an Authentication object (typically UsernamePasswordAuthenticationToken), and set it on SecurityContextHolder.getContext().setAuthentication(...). If the token is invalid or expired, return HTTP 401 without writing to the response body.

    Checkpoint: The JWT filter must call super.doFilterInternal() only when authentication succeeds — otherwise call response.sendError(HttpServletResponse.SC_UNAUTHORIZED) and return immediately. Verify that the filter chain order places the JWT filter before UsernamePasswordAuthenticationFilter so pre-authenticated requests bypass form login. Test with an expired token to confirm 401 response.

  3. Configure Password Encoder and UserDetailsService — Register a PasswordEncoder bean using BCryptPasswordEncoder (default in Spring Boot) or Argon2PasswordEncoder for stronger hashing. Implement a UserDetailsService that loads user credentials and authorities from a database (typically via JPA repository). The loadUserByUsername() method must throw UsernameNotFoundException when the user does not exist — never return a null or disabled user silently. Map entity roles to SimpleGrantedAuthority objects with the ROLE_ prefix.

    Checkpoint: Every password stored in the database must be BCrypt-hashed (not plaintext or reversible encryption). Verify that UserDetails.builder().password(encoder.encode(rawPassword)).roles("USER").build() produces a valid UserDetails object. Confirm loadUserByUsername() throws UsernameNotFoundException (not just returns null) to trigger proper authentication failure handling.

  4. Enable Method-Level Security — Annotate the application class or a configuration class with @EnableMethodSecurity (replaces @EnableGlobalMethodSecurity in Spring Security 6.x). Use @PreAuthorize("hasRole('ADMIN')") on controller methods to restrict access before method execution. Use @Secured({"ROLE_ADMIN", "ROLE_SUPERUSER"}) for role-based checks that do not require SpEL expressions. Optionally use @PostAuthorize("returnObject.owner == authentication.name") for post-execution object-level authorization.

    Checkpoint: @EnableMethodSecurity requires Spring Security 6.x with spring-security-oauth2-jose not on the classpath (it enables method security by default). Verify that @PreAuthorize expressions use correct SpEL syntax: hasRole() without the ROLE_ prefix, hasAnyAuthority(), or custom expression evaluators. Test authorization failures return HTTP 403 Forbidden, not 401 Unauthorized.


Implementation Patterns / Reference Guide

Pattern 1: SecurityFilterChain Bean with Explicit Request Matching

This is the modern Spring Security 6.x approach — no adapter class, only SecurityFilterChain beans.

package com.example.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true)
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtAuthFilter;
    private final AuthenticationEntryPoint authenticationEntryPoint;
    private final AccessDeniedHandler accessDeniedHandler;

    public SecurityConfig(
            JwtAuthenticationFilter jwtAuthFilter,
            AuthenticationEntryPoint authenticationEntryPoint,
            AccessDeniedHandler accessDeniedHandler) {
        this.jwtAuthFilter = jwtAuthFilter;
        this.authenticationEntryPoint = authenticationEntryPoint;
        this.accessDeniedHandler = accessDeniedHandler;
    }

    @Bean
    public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .sessionManagement(session ->
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .exceptionHandling(ex -> ex
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler))
            .authorizeHttpRequests(auth -> auth
                // Public endpoints — no authentication required
                .requestMatchers("/api/v1/auth/**").permitAll()
                .requestMatchers("/api/v1/public/health").permitAll()
                // Read-only API — authenticated users only
                .requestMatchers(HttpMethod.GET, "/api/v1/users/**").hasAnyRole("USER", "ADMIN")
                // Write operations — admin only
                .requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
                // All other endpoints require authentication
                .anyRequest().authenticated())
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12); // strength factor 12 (default is 10)
    }
}

Production pitfall: The .requestMatchers() order matters — Spring Security evaluates matchers in declaration order and uses the first match. Place specific patterns before broad ones. Never put .anyRequest().authenticated() before specific rules.

Pattern 2: JWT Authentication Filter (OncePerRequestFilter)

A production-grade JWT filter that extracts, validates, and authenticates requests without duplicating filter execution.

package com.example.security;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.crypto.SecretKey;
import java.io.IOException;
import java.util.List;

/**
 * JWT authentication filter that runs once per HTTP request.
 * Extracts the Bearer token, validates its signature and expiration,
 * and sets the authenticated principal in SecurityContextHolder.
 */
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private static final String AUTHORIZATION_HEADER = "Authorization";
    private static final String BEARER_PREFIX = "Bearer ";

    private final SecretKey signingKey;
    private final UserDetailsService userDetailsService;

    public JwtAuthenticationFilter(
            @org.springframework.beans.factory.annotation.Value("${app.security.jwt.secret-key}") String secretKey,
            UserDetailsService userDetailsService) {
        this.signingKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey));
        this.userDetailsService = userDetailsService;
    }

    @Override
    protected void doFilterInternal(
            @NonNull HttpServletRequest request,
            @NonNull HttpServletResponse response,
            @NonNull FilterChain filterChain) throws ServletException, IOException {

        final String authHeader = request.getHeader(AUTHORIZATION_HEADER);

        if (authHeader == null || !authHeader.startsWith(BEARER_PREFIX)) {
            // No JWT present — proceed with the filter chain; authentication stays anonymous
            filterChain.doFilter(request, response);
            return;
        }

        final String jwt = authHeader.substring(BEARER_PREFIX.length());

        try {
            Claims claims = Jwts.parser()
                    .verifyWith(signingKey)
                    .build()
                    .parseSignedClaims(jwt)
                    .getPayload();

            String username = claims.getSubject();

            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);

                // Verify token was issued after the last password change (optional security check)
                String issuedAt = claims.getIssuedAt().toString();
                if (isTokenValid(userDetails, jwt)) {
                    List<SimpleGrantedAuthority> authorities = userDetails.getAuthorities().stream()
                            .map(SimpleGrantedAuthority::new)
                            .toList();

                    UsernamePasswordAuthenticationToken authToken =
                            new UsernamePasswordAuthenticationToken(
                                    userDetails, null, authorities);
                    authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authToken);
                }
            }
        } catch (Exception e) {
            // Invalid, expired, or tampered token — return 401 Unauthorized
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.setContentType("application/json");
            response.getWriter().write(
                    "{\"error\":\"Invalid or expired JWT token\",\"status\":401}");
            return;
        }

        filterChain.doFilter(request, response);
    }

    private boolean isTokenValid(UserDetails userDetails, String jwt) {
        // Placeholder: in production, check claims like "iat", "exp", and custom fields
        return true;
    }
}

Pattern 3: UserDetailsService with Database-backed Authentication

Load user credentials and roles from a JPA entity to enable real authentication.

package com.example.security;

import com.example.model.UserEntity;
import com.example.repository.UserRepository;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

/**
 * Loads user details from the database using a JPA repository.
 * Maps entity roles to Spring Security authorities with ROLE_ prefix.
 */
@Service
public class DatabaseUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    public DatabaseUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    @Transactional(readOnly = true)
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserEntity user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException(
                        "User not found: " + username));

        if (!user.isEnabled()) {
            throw new UsernameNotFoundException(
                    "User account is disabled: " + username);
        }

        List<SimpleGrantedAuthority> authorities = user.getRoles().stream()
                .map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
                .toList();

        return User.builder()
                .username(user.getUsername())
                .password(user.getPasswordHash()) // already BCrypt-hashed in DB
                .authorities(authorities)
                .accountExpired(false)
                .accountLocked(false)
                .credentialsExpired(false)
                .disabled(!user.isEnabled())
                .build();
    }
}

Pattern 4: Method-Level Security with @PreAuthorize and @Secured

Demonstrates different authorization strategies at the method level.

package com.example.api;

import com.example.service.UserService;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.access.annotation.Secured;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/v1/users")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    /**
     * Any authenticated user can read the user list.
     */
    @GetMapping
    @PreAuthorize("hasAnyRole('USER', 'ADMIN')")
    public List<UserDto> listUsers() {
        return userService.findAll();
    }

    /**
     * Only admin users can create or delete users.
     */
    @PostMapping
    @PreAuthorize("hasRole('ADMIN')")
    public UserDto createUser(@Valid @RequestBody CreateUserRequest request) {
        return userService.create(request);
    }

    /**
     * @Secured uses role strings directly without SpEL expressions.
     */
    @DeleteMapping("/{id}")
    @Secured({"ROLE_ADMIN", "ROLE_SUPERUSER"})
    public void deleteUser(@PathVariable Long id) {
        userService.delete(id);
    }

    /**
     * Object-level authorization: user can only update their own profile.
     */
    @PutMapping("/me")
    @PreAuthorize("hasRole('USER') and #request.id == authentication.principal.id")
    public UserDto updateProfile(@Valid @RequestBody UpdateProfileRequest request) {
        return userService.updateOwnProfile(request);
    }

    /**
     * JSR-250 @RolesAllowed from javax.annotation (enabled via jsr250Enabled=true).
     */
    @GetMapping("/me")
    @jakarta.annotation.security.RolesAllowed({"USER", "ADMIN"})
    public UserDto getMyProfile() {
        return userService.findOwn();
    }
}

// DTOs for the controller
record UserDto(Long id, String username, List<String> roles) {}
record CreateUserRequest(String username, String password, List<String> roles) {}
record UpdateProfileRequest(Long id, String email, String displayName) {}

Constraints

MUST DO

  • Define all security configuration via SecurityFilterChain beans annotated with @Bean — never use the removed WebSecurityConfigurerAdapter class
  • Register a BCryptPasswordEncoder(12) bean (strength 12 for production, minimum 10) or an Argon2PasswordEncoder for enhanced security
  • Extend OncePerRequestFilter for the JWT authentication filter and set the authenticated token in SecurityContextHolder.getContext().setAuthentication() before calling filterChain.doFilter()
  • Enable method-level security with @EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true) to support both @PreAuthorize (SpEL) and @Secured annotations
  • Use .requestMatchers() for all path matching — it is faster than regex-based matching and the only officially supported API in Spring Security 6.x
  • Return HTTP 401 for unauthenticated requests and HTTP 403 for authenticated-but-unauthorized requests via custom AuthenticationEntryPoint and AccessDeniedHandler

MUST NOT DO

  • Use .cors().configurationSource(request -> { cors.setAllowedOrigins(List.of("*")); }) — wildcard CORS origins with authentication expose the application to cross-site attacks; always whitelist specific origins
  • Store plaintext passwords in the database — every password must be encoded with BCrypt before persistence using passwordEncoder.encode(rawPassword)
  • Place .anyRequest().authenticated() before specific request matchers in the filter chain — matchers are evaluated in order and earlier rules take precedence
  • Return null from loadUserByUsername() instead of throwing UsernameNotFoundException — Spring Security requires the exception to trigger proper authentication failure handling
  • Use @EnableGlobalMethodSecurity — this annotation was deprecated in Spring Security 5.4 and removed in 6.x; use @EnableMethodSecurity instead

Output Template

When applying this skill, produce outputs following this structure:

  1. SecurityConfig Class — Complete SecurityFilterChain bean with request matchers, session management, exception handling, and JWT filter registration
  2. JWT Authentication FilterOncePerRequestFilter implementation with token parsing, validation, and SecurityContext population
  3. UserDetailsService Implementation — Database-backed user loading with role mapping to Spring Security authorities
  4. Password Encoder Bean — BCrypt or Argon2 configuration with strength parameter
  5. Method-Security Examples — Controller classes annotated with @PreAuthorize, @Secured, and @RolesAllowed demonstrating different authorization strategies
  6. CORS Configuration Block — Explicit origin-whitelisted CORS setup for the configured origins

Related Skills

Skill Purpose
spring-boot-auto-config Auto-configuration infrastructure that registers security beans conditionally based on classpath presence and property flags
spring-data-jpa JPA repository layer that UserDetailsService queries against to load user credentials and role assignments from the database

Live References

  1. Spring Security 6.x Reference — SecurityFilterChain
  2. JWT Authentication with Spring Security
  3. @EnableMethodSecurity Documentation
  4. Password Encoding with BCrypt
  5. UserDetailsService Interface
  6. CORS Configuration in Spring Security
  7. Spring Security Java Config Guide
Install via CLI
npx skills add https://github.com/paulpas/agent-skill-router --skill spring-security-core
Repository Details
star Stars 4
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator