🔐

Spring Security

Cheat Sheet · Spring Boot 3.x · Security 6.x

Fundamentals
401
Unauthorized
Not authenticated
AuthenticationEntryPoint
403
Forbidden
Authenticated, no permission
AccessDeniedHandler

Authentication Object
PrincipalUserDetails (who is the user)
CredentialsPassword (null after auth)
AuthoritiesGranted roles/permissions

Auth Types
HTTP BasicEncoded credentials every request
SessionCredentials once → session cookie
JWTCredentials once → token per request

Session Policies
STATELESSNever create/use HTTP session
ALWAYSAlways create session
IF_REQUIREDCreate only if required (default)
NEVERNever create, but will use existing

Dependency
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>
Authentication Components & Flow
SecurityContextHolderStatic class holding SecurityContext. Thread-local. Call .clearContext() after request.
SecurityContextContainer for Authentication object
AuthenticationHolds principal, credentials, authorities. Core interface.
AuthenticationManagerInterface. ProviderManager is default impl — delegates to providers.
AuthenticationProviderDoes actual auth. DaoAuthenticationProvider for username/pass.
UserDetailsServiceloadUserByUsername(). Only for loading user data from DB.
UserDetailsSpring's user interface: username, password, authorities, flags.
PasswordEncoderBCryptPasswordEncoder — encodes & verifies. Adds random salt.
GrantedAuthorityRepresents ROLE_ADMIN, ROLE_USER etc. hasRole("ADMIN") → checks ROLE_ADMIN.
Full Auth Flow
1HTTP Request
2Security Filter Chain
3JWT / Auth Filter
4AuthenticationManager
5AuthenticationProvider
6UserDetailsService
7PasswordEncoder
8Authentication Object Created
9SecurityContextHolder.set()
10Controller / @PreAuthorize

UsernamePasswordAuthenticationToken (3-arg constructor = authenticated)
// 2-arg = NOT authenticated (pre-auth, for login attempt)
new UsernamePasswordAuthenticationToken(principal, credentials);

// 3-arg = IS authenticated (post-auth, set in SecurityContext)
new UsernamePasswordAuthenticationToken(
  userDetails,    // principal
  null,           // credentials — null for JWT (token IS the proof)
  userDetails.getAuthorities()  // roles
);

Custom AuthenticationProvider
@Component
public class CustomAuthProvider
    implements AuthenticationProvider {

  @Override
  public Authentication authenticate(
      Authentication auth) throws AuthenticationException {
    String user = auth.getName();
    String pass = auth.getCredentials().toString();
    // Throw BadCredentialsException if invalid
    return new UsernamePasswordAuthenticationToken(
        user, null, List.of(new SimpleGrantedAuthority("ROLE_USER")));
  }

  @Override
  public boolean supports(Class<?> auth) {
    return UsernamePasswordAuthenticationToken.class.isAssignableFrom(auth);
  }
}
CustomUserDetailsService
@Service
public class CustomUserDetailsService
    implements UserDetailsService {

  @Autowired
  private UserRepository userRepository;

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

    return User.builder()
        .username(user.getUsername())
        .password(user.getPassword()) // BCrypt in DB
        .roles(user.getRole())         // "ADMIN" → ROLE_ADMIN
        .build();
  }
}
Security Filter Chain
1
SecurityContextPersistenceFilter

Loads SecurityContext from session at start, saves at end

2
CorsFilter

Handles CORS preflight and response headers

3
CsrfFilter

Validates CSRF token (disable for stateless JWT APIs)

4
LogoutFilter

Handles /logout. Clears SecurityContext.

5
UsernamePasswordAuthenticationFilter

Processes /login form POST. Add JWT filter BEFORE this.

6
BasicAuthenticationFilter

Processes Authorization: Basic header

7
BearerTokenAuthenticationFilter

OAuth2 — extracts Bearer token from header

8
JwtAuthenticationFilter*

Your custom filter. Extends OncePerRequestFilter.

9
ExceptionTranslationFilter

Converts AccessDeniedException / AuthenticationException to HTTP responses

10
AuthorizationFilter

Was FilterSecurityInterceptor. Final authorization check.


OncePerRequestFilter
✓ Guaranteed single execution per request
✓ Use for JwtAuthenticationFilter
Override: doFilterInternal()
GenericFilterBean
⚠ May execute multiple times
Less control over execution order
Override: doFilter()

SecurityConfig Skeleton
@Configuration
@EnableWebSecurity
@EnableMethodSecurity        // enables @PreAuthorize etc.
public class SecurityConfig {

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
      .csrf(csrf -> csrf.disable())
      .cors(cors -> cors.configurationSource(corsConfigurationSource()))
      .sessionManagement(s -> s.sessionCreationPolicy(STATELESS))
      .authorizeHttpRequests(auth -> auth
          .requestMatchers("/api/auth/**").permitAll()
          .requestMatchers("/api/admin/**").hasRole("ADMIN")
          .requestMatchers("/api/users/**").hasAnyRole("USER","ADMIN")
          .anyRequest().authenticated()
      )
      .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
      .exceptionHandling(ex -> ex
          .authenticationEntryPoint(customEntryPoint)    // 401
          .accessDeniedHandler(customAccessDeniedHandler) // 403
      );
    return http.build();
  }

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

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

  @Bean public DaoAuthenticationProvider authenticationProvider() {
    var p = new DaoAuthenticationProvider();
    p.setUserDetailsService(userDetailsService);
    p.setPasswordEncoder(passwordEncoder());
    return p;
  }
}
JWT Theory
Header
{"alg":"HS256","typ":"JWT"}
Payload (Claims)
{"sub":"alice","role":"ADMIN",
 "iat":1700000000,"exp":1700003600,
 "iss":"myapp","email":"a@a.com"}
Signature
HMACSHA256(base64(header) + "." + base64(payload), secret)

Registered Claims
subSubject (username/userId)
iatIssued At (Unix timestamp)
expExpiration Time
issIssuer
audAudience
jtiJWT ID (for revocation)

Storage Strategy
Access TokenlocalStorage + Authorization: Bearer header. Short-lived (15m).
Refresh TokenhttpOnly Cookie. Longer-lived (7d). Invisible to JS → XSS safe.
⚠ localStorage → XSS risk. Cookies → CSRF risk (mitigate: SameSite=Strict + CSRF token)

Token Revocation
ProblemJWTs are stateless — can't invalidate before expiry
Option 1Blacklist in Redis until expiry
Option 2Short expiry + refresh tokens
Option 3Token versioning (version in DB + JWT)

Dependencies (jjwt)
<!-- groupId: io.jsonwebtoken -->
<dependency>jjwt-api</dependency>
<dependency>jjwt-impl</dependency>
<dependency>jjwt-jackson</dependency>
JWT Implementation
JwtUtil.java
@Component
public class JwtUtil {
  @Value("${jwt.secret}")   private String secretKey;
  @Value("${jwt.expiration}") private long expirationMs;

  private Key getSigningKey() {
    byte[] keyBytes = Decoders.BASE64.decode(secretKey);
    return Keys.hmacShaKeyFor(keyBytes);
  }

  public String generateToken(UserDetails userDetails) {
    Map<String, Object> claims = new HashMap<>();
    claims.put("roles", userDetails.getAuthorities());
    return Jwts.builder()
        .setClaims(claims)
        .setSubject(userDetails.getUsername())
        .setIssuedAt(new Date(System.currentTimeMillis()))
        .setExpiration(new Date(System.currentTimeMillis() + expirationMs))
        .signWith(getSigningKey(), SignatureAlgorithm.HS256)
        .compact();
  }

  private Claims extractAllClaims(String token) {
    return Jwts.parserBuilder()
        .setSigningKey(getSigningKey()).build()
        .parseClaimsJws(token).getBody();
  }

  // Generic claim extractor
  public <T> T extractClaim(String token, Function<Claims,T> resolver) {
    return resolver.apply(extractAllClaims(token));
  }

  public String extractUsername(String token) {
    return extractClaim(token, Claims::getSubject);
  }

  public Date extractExpiration(String token) {
    return extractClaim(token, Claims::getExpiration);
  }

  private boolean isTokenExpired(String token) {
    return extractExpiration(token).before(new Date());
  }

  public boolean validateToken(String token, UserDetails ud) {
    return extractUsername(token).equals(ud.getUsername())
        && !isTokenExpired(token);
  }
}
JwtAuthenticationFilter.java
@Component
public class JwtAuthenticationFilter
    extends OncePerRequestFilter {

  @Autowired private JwtUtil jwtUtil;
  @Autowired private UserDetailsService userDetailsService;

  @Override
  protected void doFilterInternal(
      HttpServletRequest req,
      HttpServletResponse res,
      FilterChain chain) throws ServletException, IOException {

    String authHeader = req.getHeader("Authorization");
    String jwt = null, username = null;

    if (authHeader != null && authHeader.startsWith("Bearer ")) {
      jwt = authHeader.substring(7);
      try {
        username = jwtUtil.extractUsername(jwt);
      } catch (ExpiredJwtException e) {
        res.sendError(SC_UNAUTHORIZED, "Token expired");
        return;
      } catch (MalformedJwtException e) {
        res.sendError(SC_UNAUTHORIZED, "Invalid token");
        return;
      }
    }

    if (username != null &&
        SecurityContextHolder.getContext().getAuthentication() == null) {

      UserDetails ud = userDetailsService.loadUserByUsername(username);

      if (jwtUtil.validateToken(jwt, ud)) {
        // 3-arg = authenticated
        var authToken = new UsernamePasswordAuthenticationToken(
            ud, null, ud.getAuthorities());
        authToken.setDetails(
            new WebAuthenticationDetailsSource().buildDetails(req));
        SecurityContextHolder.getContext().setAuthentication(authToken);
      }
    }
    chain.doFilter(req, res);
  }
}
application.properties
jwt.secret=your_base64_encoded_256bit_secret_here
jwt.expiration=900000     # 15 minutes in ms
# Refresh token: 604800000 = 7 days

JWT Creation Flow (Auth Endpoint)
@RestController @RequestMapping("/api/auth")
public class AuthController {
  @Autowired private AuthenticationManager authManager;
  @Autowired private JwtUtil jwtUtil;
  @Autowired private UserDetailsService userDetailsService;

  @PostMapping("/login")
  public ResponseEntity<AuthResponse> login(@RequestBody AuthRequest req) {
    // 1. Verify credentials via AuthenticationManager (throws BadCredentialsException if invalid)
    authManager.authenticate(new UsernamePasswordAuthenticationToken(req.username(), req.password()));
    // 2. Load UserDetails
    UserDetails ud = userDetailsService.loadUserByUsername(req.username());
    // 3. Generate token
    String token = jwtUtil.generateToken(ud);
    return ResponseEntity.ok(new AuthResponse(token, "Bearer", ud.getUsername(), 900));
  }
}
// DTOs
record AuthRequest(String username, String password) {}
record AuthResponse(String token, String type, String username, int expiresIn) {}
JWT Security Concerns
Algorithm Confusion (alg:none)
Explicitly whitelist algorithms in parser. Never accept 'none'.
Weak Secret Key
Use cryptographically random 256+ bit (32 byte) secrets. Not passwords.
Sensitive Data in Payload
JWT is base64, NOT encrypted. Never store PII. Use JWE for confidential data.
XSS (localStorage)
Prefer httpOnly cookies for refresh tokens. Sanitize all inputs.
CSRF (cookies)
SameSite=Strict + CSRF token header.
Token Theft
Short expiry + HTTPS only + bind to user-agent fingerprint if possible.

Nimbus (used internally by Spring)
Open source library handling JWT math/cryptography. JwtEncoder → passes claims to Nimbus → returns signed JWT string. Spring's JwtDecoder uses Nimbus to verify signature and parse claims.
OAuth2 Theory
Roles
Resource OwnerThe user granting access
ClientApp requesting access
Auth ServerIssues tokens (Google, Auth0, Okta)
Resource ServerHosts & protects resources
Tokens
ID TokenJWT with user identity (OpenID Connect login)
Access TokenFor API access. Short-lived. Opaque or JWT.
Refresh TokenGets new access tokens. Long-lived.

Grant Types
Auth Code + PKCE
Server-side apps. Code exchanged for token. Most secure.
Client Credentials
Machine-to-machine. Client accesses its own resources.
Device Code
Devices with limited input (TV, CLI).
Refresh Token
Exchange refresh token for new access token.
OAuth2 Implementation (Resource Server)
Dependencies
<!-- For UI login (Thymeleaf, etc.) -->
spring-boot-starter-oauth2-client

<!-- For validating tokens in REST APIs (Auth0, Google, Okta) -->
spring-boot-starter-oauth2-resource-server

application.properties
spring.security.oauth2.client.registration.google.client-id=CLIENT_ID
spring.security.oauth2.client.registration.google.client-secret=SECRET
spring.security.oauth2.client.registration.google.redirect-uri=http://localhost:8080/login/oauth2/code/{registrationId}
spring.security.oauth2.client.registration.google.scope=profile,email

# Resource Server (validate tokens from external Auth server)
spring.security.oauth2.resourceserver.jwt.issuer-uri=https://accounts.google.com

SecurityConfig for Resource Server
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
  http
    .csrf(csrf -> csrf.disable())
    .sessionManagement(s -> s.sessionCreationPolicy(STATELESS))
    .authorizeHttpRequests(auth -> auth
        .requestMatchers(GET, "/employees/**").authenticated()
        .requestMatchers(POST, "/employees").hasRole("ADMIN")
        .anyRequest().authenticated()
    )
    .oauth2ResourceServer(oauth2 ->
        oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthConverter()))
    );
  return http.build();
}

@Bean
public JwtAuthenticationConverter jwtAuthConverter() {
  var grantedConverter = new JwtGrantedAuthoritiesConverter();
  grantedConverter.setAuthoritiesClaimName("roles");  // Your token's claim name
  grantedConverter.setAuthorityPrefix("ROLE_");        // Spring needs ROLE_ prefix
  var converter = new JwtAuthenticationConverter();
  converter.setJwtGrantedAuthoritiesConverter(grantedConverter);
  return converter;
}

Controller with OAuth JWT
@PostMapping
@PreAuthorize("hasRole('ADMIN')")
public Employee create(
    @Valid @RequestBody Employee employee,
    @AuthenticationPrincipal Jwt jwt) {   // Inject decoded JWT
  String who = jwt.getClaimAsString("email"); // Audit logging
  return service.create(employee);
}
Authorization
Enable Method Security
@EnableMethodSecurity           // Modern (6.x)
// @EnableGlobalMethodSecurity  // Legacy - deprecated

Method-Level Annotations
@PreAuthorizeEvaluates BEFORE method. Preferred.
@PreAuthorize("hasRole('ADMIN')")
@PreAuthorize("hasAnyRole('ADMIN','USER')")
@PreAuthorize("hasAuthority('READ_PRIVILEGE')")
@PreAuthorize("#id == authentication.principal.id") // SpEL — param check
@PreAuthorize("isAuthenticated()")
@PreAuthorize("isAnonymous()")
@PostAuthorizeEvaluates AFTER method. Access returnObject.
// Only return if the fetched resource belongs to current user
@PostAuthorize("returnObject.username == authentication.name")
@SecuredLegacy. No SpEL. Simple role list.
@Secured({"ROLE_ADMIN", "ROLE_USER"})
@RolesAllowedJSR-250. Like @Secured with nicer syntax.
@RolesAllowed({"ADMIN", "USER"})

URL vs Method Security
URL-Based (coarse)
In SecurityFilterChain. Pattern: HTTP method + URL. Applied before request reaches controller.
Method-Based (fine)
@PreAuthorize. Has access to auth object, method params (#param), return values. More expressive.

SpEL Expressions Reference
authentication.nameCurrent username
authentication.authoritiesCollection of GrantedAuthority
authentication.principalUserDetails object
#paramMethod parameter named 'param'
returnObjectReturn value (@PostAuthorize)
principal.usernameUsername from principal
Testing
Unit Tests
// Mock any user
@WithMockUser(username="alice",
              roles="USER")

// Load real UserDetails from DB
@WithUserDetails("alice")

// JWT-specific
@WithMockUser + SecurityMockMvcRequestPostProcessors.jwt()

MockMvc
mockMvc.perform(
  get("/api/admin")
    .with(user("alice").roles("USER")))
  .andExpect(status().isForbidden());

mockMvc.perform(
  get("/api/admin")
    .with(user("bob").roles("ADMIN")))
  .andExpect(status().isOk());

// JWT token mock
mockMvc.perform(
  get("/api/data")
    .with(jwt().jwt(j -> j.claim("roles","ADMIN"))));

Integration Tests
// 1. Login first → get real JWT
var loginReq = new AuthRequest("alice","pass");
var tokenRes = mockMvc.perform(
  post("/api/auth/login")
    .content(toJson(loginReq))
    .contentType(APPLICATION_JSON))
  .andReturn();
String token = extractToken(tokenRes);

// 2. Use real JWT in subsequent requests
mockMvc.perform(
  get("/api/protected")
    .header("Authorization","Bearer " + token))
  .andExpect(status().isOk());
Best Practices & Security Headers
CORS Config
@Bean
public CorsConfigurationSource corsConfigurationSource() {
  CorsConfiguration config = new CorsConfiguration();
  config.setAllowedOrigins(List.of("https://yourfrontend.com"));
  config.setAllowedMethods(List.of("GET","POST","PUT","DELETE","OPTIONS"));
  config.setAllowedHeaders(List.of("Authorization","Content-Type"));
  config.setAllowCredentials(true);
  config.setMaxAge(3600L);  // cache preflight 1hr
  var source = new UrlBasedCorsConfigurationSource();
  source.registerCorsConfiguration("/**", config);
  return source;
}
// Add to SecurityFilterChain:
http.cors(cors -> cors.configurationSource(corsConfigurationSource()))

Security Headers
http.headers(headers -> headers
  .frameOptions(f -> f.deny())         // Prevent clickjacking
  .xssProtection(xss -> xss.enable()) // Legacy - some browsers ignore
  .contentSecurityPolicy(csp ->
      csp.policyDirectives("default-src 'self'"))
  .httpStrictTransportSecurity(hsts -> hsts
      .includeSubDomains(true)
      .maxAgeInSeconds(31536000))      // Force HTTPS 1 year
);

HTTPS Config
server.ssl.enabled=true
server.ssl.key-store=classpath:keystore.p12
server.ssl.key-store-password=${SSL_KEYSTORE_PASSWORD}
server.ssl.key-store-type=PKCS12
security.require-ssl=true

Checklist
HTTPS in production
BCryptPasswordEncoder (never plaintext)
Short-lived access tokens (15m)
httpOnly cookie for refresh tokens
Rate limiting on auth endpoints
Account lockout after N failures
@PreAuthorize on all endpoints
CORS — whitelist origins only
CSP + HSTS headers
Never store PII in JWT payload
256+ bit secret keys
Validate token on every request
Log auth failures for monitoring
Rotate secrets regularly
Quick Reference — Annotations & Interfaces
Config Annotations
@ConfigurationMarks Spring config class
@EnableWebSecurityEnables Spring Security
@EnableMethodSecurityEnables @PreAuthorize etc.
@BeanRegisters Spring bean
Controller Annotations
@PreAuthorizeBefore-method SpEL check
@PostAuthorizeAfter-method SpEL check
@SecuredLegacy role check
@AuthenticationPrincipalInject current user
@WithMockUserMock user in tests
@WithUserDetailsReal UserDetails in test
Key Interfaces
UserDetailsServiceloadUserByUsername()
UserDetailsUser model for Spring Security
AuthenticationManagerauthenticate(Authentication)
AuthenticationProviderCustom auth logic
PasswordEncoderencode() + matches()
GrantedAuthoritygetAuthority() → ROLE_X
Key Classes
SecurityContextHolderHolds auth per thread
UsernamePasswordAuthenticationTokenStandard auth token
DaoAuthenticationProviderUserDetails + PasswordEncoder
BCryptPasswordEncoderBcrypt with random salt
OncePerRequestFilterBase for JWT filter
WebAuthenticationDetailsSourceAttaches request details
Exceptions
AuthenticationExceptionBase auth failure
BadCredentialsExceptionWrong password/user
UsernameNotFoundExceptionUser not found
AccessDeniedException403 — no permission
ExpiredJwtExceptionJWT past expiry
MalformedJwtExceptionBad JWT format
SignatureExceptionJWT signature invalid
hasRole vs hasAuthority
hasRole("ADMIN")
Checks for ROLE_ADMIN. Prepends ROLE_ automatically.
hasAuthority("ROLE_ADMIN")
Checks exact string. No prefix added.
hasAuthority("READ_DATA")
Fine-grained permissions (not roles).
Spring Security 6.x · Spring Boot 3.x · Java 17+ · JWT (jjwt) · OAuth2 Resource Server