Cheat SheetsSpring BootSecurity

Security — Cheat Sheet

Spring Boot · 12 topics. Download the PDF or the Instagram carousel and share it.

Cheat Sheet · AiCanCode.org
Security
Spring Boot12 topicsQuick revision reference
1

Spring Security Basics

Spring Security adds authentication and authorisation to your application; it plugs in as a chain of servlet filters that intercept every HTTP request.

  • Spring Security works as a chain of servlet filters (SecurityFilterChain) that process every request before it reaches controllers.
  • Disable CSRF and sessions for stateless REST APIs; enable them for server-rendered apps with form login.
  • Always use BCryptPasswordEncoder — never store plain-text or MD5-hashed passwords.
  • UserDetailsService.loadUserByUsername() is the extension point for loading users from your database.
  • @PreAuthorize("hasRole('ADMIN')") enforces access control at the method/service level via Spring EL expressions.
  • Return 401/403 as JSON (not HTML redirects) for REST APIs by customising the AuthenticationEntryPoint and AccessDeniedHandler.
Java — Spring Security Config
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // Stateless REST API — disable session & CSRF
            .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .csrf(csrf -> csrf.disable())

            // Authorisation rules
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()        // public endpoints
                .requestMatchers("/api/admin/**").hasRole("ADMIN")  // admin only
                .anyRequest().authenticated()                        // all others need auth
            )

            // Use JWT filter instead of form login
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)

            // Return 401/403 as JSON, not redirect
            .exceptionHandling(ex -> ex
                .authenticationEntryPoint((req, res, e) -> {
                    res.setStatus(401);
                    res.getWriter().write("{"error":"Unauthorized"}");
                })
            );

        return http.build();
    }

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

Authentication & Authorisation

Authentication verifies who the caller is (UserDetailsService, in-memory, LDAP); authorisation decides what resources they can access (@PreAuthorize, security DSL).

  • Spring Security 6 uses SecurityFilterChain beans — WebSecurityConfigurerAdapter is removed.
  • Always use BCryptPasswordEncoder — never store plaintext or MD5/SHA-1 hashes.
  • @EnableMethodSecurity (replaces @EnableGlobalMethodSecurity) must be on a @Configuration class to activate @PreAuthorize.
  • STATELESS session management is correct for JWT/REST APIs — no server-side session is created.
  • SecurityContextHolder stores the current Authentication; use it to access the logged-in user anywhere without parameter threading.
  • @PostAuthorize is useful to prevent leaking entity data even if a user guesses an ID; runs after the query.
Java — Spring Security 6 SecurityFilterChain
@Configuration
@EnableWebSecurity
@EnableMethodSecurity   // enables @PreAuthorize on service methods
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())            // disable for REST APIs
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/auth/**", "/public/**").permitAll()
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .addFilterBefore(jwtAuthFilter,          // JWT filter before UsernamePassword
                UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }

    @Bean
    public AuthenticationManager authenticationManager(
            UserDetailsService userDetailsService,
            PasswordEncoder encoder) {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService);
        provider.setPasswordEncoder(encoder);
        return new ProviderManager(provider);
    }

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

JWT Authentication

Stateless authentication using signed JSON Web Tokens; a custom OncePerRequestFilter validates the token and populates the SecurityContext on each request.

  • JWT is stateless: server validates the signature without a DB lookup — ideal for scaled microservices
  • Claims: sub (userId), roles, exp (expiry) — all readable by any service with the key
  • OncePerRequestFilter intercepts every request, validates token, sets SecurityContext
  • SessionCreationPolicy.STATELESS prevents Spring from creating HTTP sessions
  • Short access token (15 min) + long-lived refresh token (Redis) balances security and UX
  • For true revocation: maintain a Redis blocklist and check it on every access-token validation
Spring Boot — JWT issue at login (jjwt)
@Service
class JwtService {
    private final SecretKey key = Keys.hmacShaKeyFor(
        Decoders.BASE64.decode(secretBase64));   // from config, min 256 bits

    public String generate(User user) {
        return Jwts.builder()
            .subject(user.getId().toString())
            .claim("roles", user.getRoles())
            .issuedAt(new Date())
            .expiration(new Date(System.currentTimeMillis() + 3_600_000)) // 1h
            .signWith(key)
            .compact();
    }

    public Claims validate(String token) {
        return Jwts.parser()
            .verifyWith(key)
            .build()
            .parseSignedClaims(token)
            .getPayload();  // throws ExpiredJwtException, SignatureException on failure
    }
}

@RestController
class AuthController {
    @PostMapping("/auth/login")
    Map<String, String> login(@RequestBody LoginRequest req) {
        User user = authService.authenticate(req.email(), req.password());
        return Map.of("token", jwtService.generate(user));
    }
}
4

OAuth2 & SSO with Spring Security

Spring Security OAuth2 Client enables "Login with Google/GitHub" and SSO; the resource-server mode protects APIs accepting Bearer tokens from an authorisation server.

  • OAuth2 Client handles "Login with Google/GitHub" SSO via Authorization Code flow; Resource Server validates incoming JWTs
  • spring-security-oauth2-client auto-configures CommonOAuth2Provider for Google, GitHub, Facebook, Okta
  • Resource Server uses JWKS endpoint to fetch public keys and verify JWT signatures without storing them locally
  • JwtAuthenticationConverter maps custom claims (roles, scopes) to Spring Security GrantedAuthority objects
  • Client Credentials grant is for machine-to-machine; OAuth2AuthorizedClientManager caches and auto-refreshes tokens
  • Stateless JWT resource servers must disable CSRF and session creation (SessionCreationPolicy.STATELESS)
YAML + Java — OAuth2 Login with Google and GitHub SSO
# application.yml — Google OAuth2 login
spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: ${GOOGLE_CLIENT_ID}
            client-secret: ${GOOGLE_CLIENT_SECRET}
            scope: openid,email,profile
          github:
            client-id: ${GITHUB_CLIENT_ID}
            client-secret: ${GITHUB_CLIENT_SECRET}
            scope: user:email

# Security config — allow OAuth2 login
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/", "/login", "/error").permitAll()
                .anyRequest().authenticated())
            .oauth2Login(oauth2 -> oauth2
                .defaultSuccessUrl("/dashboard", true)
                .failureUrl("/login?error"))
            .build();
    }
}
5

CSRF Protection & Session Management

Spring Security enables CSRF protection by default for stateful apps; stateless REST APIs typically disable it and rely on JWT or API-key schemes instead.

  • CSRF is only relevant for cookie-based authentication — disable it for JWT Bearer token REST APIs
  • SessionCreationPolicy.STATELESS prevents Spring Security from creating or using HTTP sessions — mandatory for JWTs
  • CookieCsrfTokenRepository.withHttpOnlyFalse() allows JavaScript SPAs to read the CSRF token from a cookie
  • Session fixation: migrateSession() (default) creates a new session after login, copying attributes — prevents fixation attacks
  • maximumSessions(1) + maxSessionsPreventsLogin(true) enforces single-device login, rejecting new logins when limit is reached
  • HttpSessionEventPublisher bean is required for concurrent session control to receive session destruction notifications
Java — CSRF disabled + STATELESS session for JWT REST APIs
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            // Stateless REST API — no session, no CSRF needed
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .csrf(AbstractHttpConfigurer::disable)
            // JWT resource server — validates Bearer token
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(Customizer.withDefaults()))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/actuator/health").permitAll()
                .anyRequest().authenticated())
            .build();
    }
}
6

Spring Security Basics

Spring Security protects your application through a chain of servlet filters. Understanding the SecurityFilterChain, SecurityContext, and HttpSecurity DSL is the foundation for any authentication or authorization implementation.

  • Spring Security is a chain of servlet filters — every request passes through SecurityFilterChain.
  • Disable CSRF for stateless REST APIs that use tokens instead of cookies.
  • SessionCreationPolicy.STATELESS prevents Spring Security from creating HTTP sessions.
  • URL access rules are evaluated in order — specific paths must come before general ones.
  • SecurityContextHolder is thread-local — it holds the authenticated principal for the current request.
  • @AuthenticationPrincipal injects the principal directly into a controller method parameter.
Java — SecurityFilterChain for a stateless REST API
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            // Disable CSRF for stateless REST APIs (tokens, not cookies)
            .csrf(AbstractHttpConfigurer::disable)

            // No session — stateless JWT authentication
            .sessionManagement(s -> s
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS))

            // URL access rules — order matters: specific before general
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/v1/auth/**").permitAll()        // public
                .requestMatchers("/api/v1/admin/**").hasRole("ADMIN")  // admin only
                .requestMatchers(HttpMethod.GET, "/api/v1/courses/**").permitAll()
                .anyRequest().authenticated()                          // everything else requires auth
            )

            // Add JWT filter before the default UsernamePasswordAuthenticationFilter
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)

            .build();
    }
}
7

Custom Authentication

UserDetailsService loads user data during authentication. Implement it to load users from your database, hash passwords with BCrypt, and wire everything into Spring Security's DaoAuthenticationProvider.

  • UserDetailsService.loadUserByUsername() is called by Spring Security to load user details during authentication.
  • UserDetails provides password hash, authorities, and account status — Spring Security handles the comparison.
  • BCryptPasswordEncoder embeds the salt in the hash — never store raw or MD5/SHA-hashed passwords.
  • Cost factor 10-12 is recommended — it makes BCrypt slow enough to resist brute force.
  • Throw UsernameNotFoundException from loadUserByUsername — Spring converts it to BadCredentialsException.
  • Always use passwordEncoder.matches(raw, encoded) to check passwords — never compare hashes directly.
Java — UserDetails wrapper and UserDetailsService
// Option 1: Wrap your User entity in UserDetails
public class UserPrincipal implements UserDetails {
    private final User user;

    public UserPrincipal(User user) { this.user = user; }

    @Override public String getUsername() { return user.getEmail(); }
    @Override public String getPassword() { return user.getPasswordHash(); }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of(new SimpleGrantedAuthority("ROLE_" + user.getRole().name()));
    }

    @Override public boolean isAccountNonExpired()  { return true; }
    @Override public boolean isAccountNonLocked()   { return user.isActive(); }
    @Override public boolean isCredentialsNonExpired() { return true; }
    @Override public boolean isEnabled()             { return user.isEmailVerified(); }

    public String getId() { return user.getId(); } // expose for JWT generation
}

// UserDetailsService — Spring calls this during authentication
@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 {
        return userRepository.findByEmail(email)
            .map(UserPrincipal::new)
            .orElseThrow(() -> new UsernameNotFoundException("User not found: " + email));
    }
}
8

JWT Authentication

JWT is a stateless authentication token — the server signs it, clients store and send it. A OncePerRequestFilter validates the token on every request and populates the SecurityContext without any database lookup.

  • JWT = Header.Payload.Signature — the server signs with a secret key; clients cannot forge tokens.
  • JWTs are stateless — no database lookup required for validation, just signature verification.
  • OncePerRequestFilter ensures the JWT filter runs exactly once per request, not once per servlet dispatch.
  • Set Authentication in SecurityContextHolder after validation — this is how Spring Security knows the user is logged in.
  • Use a secret key of at least 256 bits for HMAC-SHA256 signing — generate with Keys.secretKeyFor(SignatureAlgorithm.HS256).
  • Short expiry (15–60 min) + refresh tokens is more secure than long-lived JWTs — invalidation is impossible without a blocklist.
Java — JwtService with JJWT 0.12
<!-- pom.xml -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.12.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.12.5</version>
    <scope>runtime</scope>
</dependency>

@Service
public class JwtService {

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

    @Value("${app.jwt.expiry-minutes:60}")
    private int expiryMinutes;

    private SecretKey signingKey() {
        return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
    }

    public String generateToken(String userId) {
        return Jwts.builder()
            .subject(userId)
            .issuedAt(new Date())
            .expiration(Date.from(Instant.now().plusSeconds(expiryMinutes * 60L)))
            .signWith(signingKey())
            .compact();
    }

    public String extractUserId(String token) {
        return parseClaims(token).getSubject();
    }

    public boolean isValid(String token) {
        try {
            parseClaims(token); // throws on invalid/expired
            return true;
        } catch (JwtException e) {
            return false;
        }
    }

    private Claims parseClaims(String token) {
        return Jwts.parser()
            .verifyWith(signingKey())
            .build()
            .parseSignedClaims(token)
            .getPayload();
    }
}
9

Role-Based Access

Spring Security's authorization model uses roles (ROLE_ADMIN) and authorities (fine-grained permissions). URL-level access rules in HttpSecurity handle coarse-grained control; method security handles fine-grained access within a use case.

  • Roles are prefixed authorities — ROLE_ADMIN; hasRole("ADMIN") is shorthand for hasAuthority("ROLE_ADMIN").
  • URL-level access rules in HttpSecurity are coarse-grained — evaluated in the order you declare them.
  • More specific matchers must come before general ones — .anyRequest().authenticated() must be last.
  • For ownership checks, verify the principal in the service layer — URL rules can't express "own resource".
  • Load authorities from the database in UserDetailsService — they travel with the JWT or session.
  • Use hasAuthority() for fine-grained permissions (WRITE_BLOG, DELETE_COURSE) separate from broad roles.
Java — loading roles, URL-level access rules
// Loading roles from DB in UserDetailsService
@Override
public UserDetails loadUserByUsername(String email) {
    User user = userRepository.findByEmail(email).orElseThrow(...);
    return org.springframework.security.core.userdetails.User
        .withUsername(user.getEmail())
        .password(user.getPasswordHash())
        .roles(user.getRole().name()) // adds "ROLE_" prefix automatically
        // Or for fine-grained authorities:
        // .authorities(user.getPermissions().stream()
        //     .map(p -> new SimpleGrantedAuthority(p.name()))
        //     .toList())
        .build();
}

// URL-level authorization in SecurityConfig
.authorizeHttpRequests(auth -> auth
    // Public
    .requestMatchers("/api/v1/auth/**").permitAll()
    .requestMatchers(HttpMethod.GET, "/api/v1/courses/**").permitAll()

    // Role-based
    .requestMatchers("/api/v1/admin/**").hasRole("ADMIN")

    // Multiple roles
    .requestMatchers("/api/v1/cohorts/**").hasAnyRole("ADMIN", "INSTRUCTOR")

    // Fine-grained authority
    .requestMatchers(HttpMethod.POST, "/api/v1/blog/**").hasAuthority("WRITE_BLOG")

    // Catch-all
    .anyRequest().authenticated()
)
10

CORS & CSRF

CORS allows browsers to make cross-origin API calls. CSRF protects stateful (cookie-based) apps from forged requests. Stateless REST APIs with JWT disable CSRF and configure CORS explicitly.

  • CORS is enforced by browsers, not the server — configure it to tell browsers which origins are allowed.
  • Never use allowedOrigins("*") with allowCredentials(true) — browsers reject this combination.
  • CSRF is only a risk for cookie-based authentication — stateless JWT APIs can safely disable it.
  • Configure CORS globally via CorsConfigurationSource — avoid @CrossOrigin scattered across controllers.
  • Spring Security's .cors() must reference the CorsConfigurationSource bean, or CORS headers won't be added to responses.
  • OPTIONS preflight requests must be permitted without authentication — Spring Security handles this automatically when CORS is configured.
Java — global CorsConfigurationSource wired into Security
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();

        // Allowed origins — never use "*" in production
        config.setAllowedOrigins(List.of(
            "https://aicancode.org",
            "https://www.aicancode.org",
            "http://localhost:3000"  // dev
        ));

        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")); // headers JS can read

        config.setAllowCredentials(true);   // required for cookies/auth headers
        config.setMaxAge(3600L);            // preflight cache for 1 hour

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

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            .csrf(AbstractHttpConfigurer::disable)  // safe for stateless JWT APIs
            // ...
            .build();
    }
}
11

Method-Level Security

@PreAuthorize with SpEL expressions enforces access control at the method level — inside the service, after the URL check. Use it for ownership checks, fine-grained permissions, and multi-tenant isolation.

  • @EnableMethodSecurity must be on a @Configuration class to activate @PreAuthorize and others.
  • @PreAuthorize evaluates before the method — AccessDeniedException is thrown if the expression is false.
  • authentication.principal in SpEL is whatever your JWT filter set as the principal (String userId or UserPrincipal).
  • #paramName in SpEL refers to a method argument by name — requires -parameters compiler flag or parameter names preserved.
  • @PostAuthorize is expensive for large datasets — use @PostFilter only on small collections.
  • Implement PermissionEvaluator for complex permission logic that cannot be expressed in inline SpEL.
Java — @EnableMethodSecurity and @PreAuthorize expressions
@Configuration
@EnableMethodSecurity  // enables @PreAuthorize, @PostAuthorize, @PreFilter, @PostFilter
public class SecurityConfig { ... }

@Service
public class CourseService {

    // Only ADMIN can publish courses
    @PreAuthorize("hasRole('ADMIN')")
    public void publishCourse(String courseId) { ... }

    // Only ADMIN or INSTRUCTOR roles
    @PreAuthorize("hasAnyRole('ADMIN', 'INSTRUCTOR')")
    public CourseDto createCourse(CreateCourseRequest req) { ... }

    // User can only access their own enrollment
    // #userId method arg must match the principal (or be admin)
    @PreAuthorize("#userId == authentication.principal or hasRole('ADMIN')")
    public List<EnrollmentDto> getEnrollments(String userId) { ... }

    // Check a property on the principal object
    // (when principal is a UserPrincipal with .isPro() method)
    @PreAuthorize("authentication.principal.pro or hasRole('ADMIN')")
    public List<CertificationDto> getPremiumCertifications() { ... }

    // Free method — no restriction
    public List<CourseDto> getPublishedCourses() { ... }
}
12

Testing Secured Endpoints

@WithMockUser fakes authentication for unit/integration tests. For realistic tests with JWTs, add the Authorization header manually. MockMvc with Spring Security support validates both authentication and authorization behavior.

  • @WebMvcTest includes Spring Security by default — tests will fail with 401/403 without authentication setup.
  • @WithMockUser is fast and simple — use it for authorization rule testing without touching your filter chain.
  • @WithUserDetails uses your real UserDetailsService — more realistic but requires a real or mocked user in the DB.
  • For JWT API testing, generate tokens in @BeforeEach and pass as Authorization: Bearer header.
  • Test both the happy path (valid token, right role) AND the failure cases (no token, wrong role, expired token).
  • Use @TestPropertySource or application-test.yml to override JWT secrets and expiry in test context.
Java — @WithMockUser with MockMvc security assertions
@WebMvcTest(CourseController.class)
class CourseControllerTest {

    @Autowired MockMvc mockMvc;

    // ── Authentication tests ────────────────────────────────────────────────

    @Test
    void unauthenticated_request_returns_401() throws Exception {
        mockMvc.perform(get("/api/v1/courses/my-courses"))
            .andExpect(status().isUnauthorized());
    }

    // @WithMockUser — creates SecurityContext with username="user", roles=["USER"]
    @Test
    @WithMockUser
    void authenticated_user_can_access_their_courses() throws Exception {
        mockMvc.perform(get("/api/v1/courses/my-courses"))
            .andExpect(status().isOk());
    }

    // Custom role
    @Test
    @WithMockUser(roles = "ADMIN")
    void admin_can_publish_course() throws Exception {
        mockMvc.perform(post("/api/v1/admin/courses/{id}/publish", "course-123"))
            .andExpect(status().isOk());
    }

    // Verify non-admin gets 403
    @Test
    @WithMockUser(roles = "USER")
    void non_admin_gets_403_on_publish() throws Exception {
        mockMvc.perform(post("/api/v1/admin/courses/{id}/publish", "course-123"))
            .andExpect(status().isForbidden());
    }
}
Learn this free with Aria, your AI tutor → AiCanCode.org/learn/spring-boot