Skip to main content
Technology & EngineeringJava Spring325 lines

Spring Security

Authentication, authorization, and security configuration with Spring Security including JWT, OAuth2, and method-level security

Quick Summary17 lines
You are an expert in Spring Security for building secure Java applications with Spring Boot. You design security as an integral architectural layer, applying defense in depth and the principle of least privilege across authentication, authorization, and transport security.

## Key Points

- **Use BCrypt** for password hashing — it is intentionally slow and resistant to brute-force attacks. Never store plaintext passwords.
- **Keep JWT expiry short** (15-60 minutes) and implement refresh tokens for long-lived sessions.
- **Store secrets externally** — JWT signing keys and OAuth2 client secrets belong in environment variables or a secrets manager, not in `application.yml`.
- **Apply the principle of least privilege** — deny by default and explicitly permit only required endpoints.
- **Enable CSRF protection** for browser-based sessions. Only disable it for stateless JWT APIs.
- **Use `@PreAuthorize`** for fine-grained access control at the method level rather than only relying on URL-based rules.
- **Validate all input** — security is defense in depth. Use `@Valid` on request bodies alongside authentication and authorization.
- **Forgetting to hash passwords** — calling `passwordEncoder.encode()` on registration but comparing raw passwords on login. Let Spring Security handle comparison through the `PasswordEncoder` bean.
- **Leaking security details in error responses** — returning "user not found" vs "wrong password" lets attackers enumerate valid accounts. Return a generic "invalid credentials" message.
- **Not invalidating JWTs on logout** — JWTs are stateless, so they remain valid until expiry. Implement a token blacklist or use short-lived tokens with refresh token rotation.
- **Disabling CSRF globally** when only the API portion is stateless — keep CSRF enabled for any endpoints serving browser sessions.
skilldb get java-spring-skills/Spring SecurityFull skill: 325 lines
Paste into your CLAUDE.md or agent config

Spring Security — Java/Spring Boot

You are an expert in Spring Security for building secure Java applications with Spring Boot. You design security as an integral architectural layer, applying defense in depth and the principle of least privilege across authentication, authorization, and transport security.

Core Philosophy

Security is not a feature to be added after the application works. It is a cross-cutting architectural concern that shapes how requests are processed, how data is accessed, and how errors are communicated. Spring Security's filter chain model enforces this perspective: every request passes through security checks before it reaches application code. When security is treated as an afterthought, the result is a patchwork of ad-hoc checks scattered across controllers, easily bypassed and impossible to audit. When security is designed into the architecture from the start, it becomes consistent, testable, and maintainable.

The principle of least privilege should be the default posture. Deny everything, then explicitly permit what is needed. Spring Security's authorizeHttpRequests DSL reads naturally from most specific to most general, and the final rule should be anyRequest().authenticated() or anyRequest().denyAll(). A permissive default that grants access unless explicitly denied is an invitation for privilege escalation bugs every time a new endpoint is added. The security configuration should be reviewed with the same rigor as the data model, because a misconfigured matcher can expose sensitive operations to unauthenticated users.

Authentication and authorization are separate concerns that must not be conflated. Authentication answers "who are you?" -- verifying credentials, validating tokens, establishing identity. Authorization answers "are you allowed to do this?" -- checking roles, permissions, and ownership. Mixing these concerns produces code where a JWT filter also checks admin status, or a policy check assumes a specific authentication mechanism. Keep them cleanly separated: the filter chain handles authentication, @PreAuthorize and method security handle authorization, and policies express fine-grained access rules.

Overview

Spring Security is the standard framework for securing Spring-based applications. It provides authentication (verifying identity), authorization (controlling access), and protection against common exploits like CSRF, session fixation, and clickjacking. Spring Boot auto-configures sensible defaults, and the security filter chain architecture allows precise customization.

Core Concepts

Security Filter Chain

Every HTTP request passes through a chain of security filters. Spring Security 6+ uses a component-based configuration model:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable()) // Disable for stateless APIs
            .sessionManagement(session ->
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .requestMatchers(HttpMethod.GET, "/api/products/**").permitAll()
                .requestMatchers("/api/**").authenticated()
                .anyRequest().permitAll()
            )
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }
}

UserDetailsService

The bridge between your user store and Spring Security:

@Service
public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

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

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        User user = userRepository.findByEmail(email)
                .orElseThrow(() -> new UsernameNotFoundException("User not found: " + email));

        return org.springframework.security.core.userdetails.User.builder()
                .username(user.getEmail())
                .password(user.getPasswordHash())
                .roles(user.getRoles().stream().map(Role::getName).toArray(String[]::new))
                .accountLocked(!user.isActive())
                .build();
    }
}

JWT Authentication

@Component
public class JwtTokenProvider {

    @Value("${jwt.secret}")
    private String secret;

    @Value("${jwt.expiration-ms:3600000}")
    private long expirationMs;

    public String generateToken(Authentication authentication) {
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        Date now = new Date();
        Date expiry = new Date(now.getTime() + expirationMs);

        return Jwts.builder()
                .setSubject(userDetails.getUsername())
                .setIssuedAt(now)
                .setExpiration(expiry)
                .signWith(Keys.hmacShaKeyFor(secret.getBytes()), SignatureAlgorithm.HS256)
                .compact();
    }

    public String extractUsername(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(Keys.hmacShaKeyFor(secret.getBytes()))
                .build()
                .parseClaimsJws(token)
                .getBody()
                .getSubject();
    }

    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder()
                    .setSigningKey(Keys.hmacShaKeyFor(secret.getBytes()))
                    .build()
                    .parseClaimsJws(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }
}

JWT Filter

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtTokenProvider tokenProvider;
    private final UserDetailsService userDetailsService;

    public JwtAuthenticationFilter(JwtTokenProvider tokenProvider, UserDetailsService userDetailsService) {
        this.tokenProvider = tokenProvider;
        this.userDetailsService = userDetailsService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                     FilterChain filterChain) throws ServletException, IOException {
        String header = request.getHeader("Authorization");

        if (header != null && header.startsWith("Bearer ")) {
            String token = header.substring(7);
            if (tokenProvider.validateToken(token)) {
                String username = tokenProvider.extractUsername(token);
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                UsernamePasswordAuthenticationToken authToken =
                        new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }

        filterChain.doFilter(request, response);
    }
}

Implementation Patterns

Authentication Controller

@RestController
@RequestMapping("/api/auth")
public class AuthController {

    private final AuthenticationManager authenticationManager;
    private final JwtTokenProvider tokenProvider;
    private final UserService userService;

    public AuthController(AuthenticationManager authenticationManager,
                          JwtTokenProvider tokenProvider, UserService userService) {
        this.authenticationManager = authenticationManager;
        this.tokenProvider = tokenProvider;
        this.userService = userService;
    }

    @PostMapping("/login")
    public ResponseEntity<AuthResponse> login(@Valid @RequestBody LoginRequest request) {
        Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(request.getEmail(), request.getPassword()));
        String token = tokenProvider.generateToken(authentication);
        return ResponseEntity.ok(new AuthResponse(token));
    }

    @PostMapping("/register")
    public ResponseEntity<Void> register(@Valid @RequestBody RegisterRequest request) {
        userService.register(request);
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }
}

Method-Level Security

@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {
}

@Service
public class DocumentService {

    @PreAuthorize("hasRole('ADMIN') or #userId == authentication.principal.id")
    public List<Document> getUserDocuments(Long userId) {
        // Only admins or the user themselves can access
        return documentRepository.findByUserId(userId);
    }

    @PreAuthorize("hasAuthority('DOCUMENT_DELETE')")
    public void deleteDocument(Long documentId) {
        documentRepository.deleteById(documentId);
    }

    @PostAuthorize("returnObject.owner == authentication.name")
    public Document getDocument(Long id) {
        return documentRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("Document", id));
    }
}

OAuth2 Resource Server

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://auth.example.com/realms/myapp
@Bean
public SecurityFilterChain resourceServerFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/api/public/**").permitAll()
            .anyRequest().authenticated()
        )
        .oauth2ResourceServer(oauth2 -> oauth2
            .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthConverter()))
        );
    return http.build();
}

private JwtAuthenticationConverter jwtAuthConverter() {
    JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter();
    converter.setAuthoritiesClaimName("roles");
    converter.setAuthorityPrefix("ROLE_");

    JwtAuthenticationConverter authConverter = new JwtAuthenticationConverter();
    authConverter.setJwtGrantedAuthoritiesConverter(converter);
    return authConverter;
}

CORS Configuration

@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowedOrigins(List.of("https://myapp.com"));
    config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
    config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
    config.setAllowCredentials(true);
    config.setMaxAge(3600L);

    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/api/**", config);
    return source;
}

Best Practices

  • Use BCrypt for password hashing — it is intentionally slow and resistant to brute-force attacks. Never store plaintext passwords.
  • Keep JWT expiry short (15-60 minutes) and implement refresh tokens for long-lived sessions.
  • Store secrets externally — JWT signing keys and OAuth2 client secrets belong in environment variables or a secrets manager, not in application.yml.
  • Apply the principle of least privilege — deny by default and explicitly permit only required endpoints.
  • Enable CSRF protection for browser-based sessions. Only disable it for stateless JWT APIs.
  • Use @PreAuthorize for fine-grained access control at the method level rather than only relying on URL-based rules.
  • Validate all input — security is defense in depth. Use @Valid on request bodies alongside authentication and authorization.

Common Pitfalls

  • Ordering filter chains incorrectly — a permitAll() rule before a more specific authenticated() rule causes the permissive rule to match first. Order matchers from most specific to least specific.
  • Forgetting to hash passwords — calling passwordEncoder.encode() on registration but comparing raw passwords on login. Let Spring Security handle comparison through the PasswordEncoder bean.
  • Leaking security details in error responses — returning "user not found" vs "wrong password" lets attackers enumerate valid accounts. Return a generic "invalid credentials" message.
  • Not invalidating JWTs on logout — JWTs are stateless, so they remain valid until expiry. Implement a token blacklist or use short-lived tokens with refresh token rotation.
  • Disabling CSRF globally when only the API portion is stateless — keep CSRF enabled for any endpoints serving browser sessions.

Anti-Patterns

  • Security by obscurity — relying on undocumented URL paths or non-standard header names instead of proper authentication and authorization. Every endpoint must be explicitly secured regardless of how "hidden" it appears. Attackers use automated scanners, not guesswork.

  • The permissive catch-all — ending the authorization chain with .anyRequest().permitAll() because "we will lock it down later." Every new endpoint added after this configuration is publicly accessible by default. Use .anyRequest().denyAll() or .anyRequest().authenticated() as the final rule.

  • Logging sensitive credentials — including raw passwords, tokens, or API keys in log statements during authentication debugging. Even in development, this creates a habit that leaks into production logs and violates compliance requirements. Log only non-sensitive metadata like usernames and authentication outcomes.

  • Monolithic security configuration — a single 200-line SecurityFilterChain bean that handles API authentication, admin panel sessions, actuator access, and OAuth2 flows. Split into multiple filter chains with @Order and securityMatcher so each concern is isolated, testable, and independently modifiable.

  • JWT without rotation or revocation — issuing long-lived JWTs with no mechanism for revocation and no key rotation strategy. When a token is compromised or a user's access should be revoked, there is no way to invalidate outstanding tokens. Use short-lived access tokens with refresh token rotation, and implement a token blacklist for immediate revocation needs.

Install this skill directly: skilldb add java-spring-skills

Get CLI access →