Spring Boot Cheatsheet

Security

Use this Spring Boot reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.

Dependency

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

Adding this starter immediately secures all endpoints with HTTP Basic and a generated password (printed to console on startup). Override by defining a SecurityFilterChain bean.

SecurityFilterChain — the Main Config Bean

@Configuration
@EnableWebSecurity
public class SecurityConfig {

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

        return http.build();
    }
}

Authorization Rules — Quick Reference

MethodMeaning
.permitAll()Everyone, including anonymous
.authenticated()Any logged-in user
.hasRole("ADMIN")User has ROLE_ADMIN (prefix added automatically)
.hasAnyRole("ADMIN","MANAGER")Has any of the listed roles
.hasAuthority("products:write")Has exact authority string
.hasAnyAuthority("a","b")Has any of the exact strings
.denyAll()Block all
.anonymous()Unauthenticated users only

Method-Level Security

@Configuration
@EnableMethodSecurity   // replaces deprecated @EnableGlobalMethodSecurity
public class MethodSecurityConfig {}
@Service
public class ArticleService {

    @PreAuthorize("hasRole('ADMIN') or #authorId == authentication.principal.id")
    public Article getPrivate(Long articleId, Long authorId) { ... }

    @PostAuthorize("returnObject.authorId == authentication.principal.id")
    public Article getOwned(Long id) { ... }

    @PreAuthorize("hasAuthority('articles:write')")
    public Article create(CreateArticleRequest req) { ... }

    @Secured({"ROLE_ADMIN", "ROLE_EDITOR"})   // simpler, no SpEL
    public void delete(Long id) { ... }
}
AnnotationEvaluatesNotes
@PreAuthorizeBefore method invocationSpEL; most flexible
@PostAuthorizeAfter (can inspect return value)SpEL; use sparingly
@SecuredBefore invocationRole names only, no SpEL
@RolesAllowedBefore invocationJSR-250; like @Secured

JWT Authentication Filter

@Component
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {

    private final JwtService jwtService;
    private final UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain)
            throws ServletException, IOException {
        String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            chain.doFilter(request, response);
            return;
        }
        String token = authHeader.substring(7);
        String username = jwtService.extractUsername(token);

        if (username != null && SecurityContextHolder.getContext()
                                                     .getAuthentication() == null) {
            UserDetails user = userDetailsService.loadUserByUsername(username);
            if (jwtService.isTokenValid(token, user)) {
                UsernamePasswordAuthenticationToken authToken =
                    new UsernamePasswordAuthenticationToken(user, null,
                                                           user.getAuthorities());
                authToken.setDetails(new WebAuthenticationDetailsSource()
                                        .buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }
        chain.doFilter(request, response);
    }
}

UserDetailsService Implementation

@Service
@RequiredArgsConstructor
public class AppUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String email)
            throws UsernameNotFoundException {
        return userRepository.findByEmail(email)
            .map(user -> org.springframework.security.core.userdetails.User
                .withUsername(user.getEmail())
                .password(user.getPasswordHash())
                .roles(user.getRole().name())   // adds ROLE_ prefix
                .build())
            .orElseThrow(() -> new UsernameNotFoundException("User not found: " + email));
    }
}

Password Encoding

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

// Encoding
String hash = passwordEncoder.encode(rawPassword);

// Verification
boolean matches = passwordEncoder.matches(rawPassword, storedHash);

Authentication Entry Points and Access Denied Handlers

// 401 — not authenticated
@Bean
public AuthenticationEntryPoint unauthorizedEntryPoint() {
    return (request, response, ex) -> {
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.getWriter().write("{\"error\":\"Unauthorized\"}");
    };
}

// 403 — authenticated but no permission
@Bean
public AccessDeniedHandler forbiddenHandler() {
    return (request, response, ex) -> {
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        response.getWriter().write("{\"error\":\"Forbidden\"}");
    };
}

Session-Based Auth (Stateful)

http
    .formLogin(form -> form
        .loginPage("/login")
        .loginProcessingUrl("/login")
        .defaultSuccessUrl("/dashboard", true)
        .failureUrl("/login?error")
        .permitAll()
    )
    .logout(logout -> logout
        .logoutUrl("/logout")
        .logoutSuccessUrl("/login?logout")
        .invalidateHttpSession(true)
        .deleteCookies("JSESSIONID")
        .permitAll()
    )
    .rememberMe(remember -> remember
        .key("my-unique-key")
        .tokenValiditySeconds(86400 * 14)   // 14 days
    );

OAuth2 Resource Server (JWT)

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
spring.security.oauth2.resourceserver.jwt.issuer-uri=https://auth.example.com
# or public key location
spring.security.oauth2.resourceserver.jwt.public-key-location=classpath:public.pem
http.oauth2ResourceServer(oauth2 ->
    oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()))
);

@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
    JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter =
        new JwtGrantedAuthoritiesConverter();
    grantedAuthoritiesConverter.setAuthoritiesClaimName("roles");
    grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
    JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
    converter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
    return converter;
}

OAuth2 Login (Social Login)

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
spring.security.oauth2.client.registration.google.client-id=${GOOGLE_CLIENT_ID}
spring.security.oauth2.client.registration.google.client-secret=${GOOGLE_CLIENT_SECRET}
spring.security.oauth2.client.registration.google.scope=email,profile
http.oauth2Login(oauth2 -> oauth2
    .loginPage("/login")
    .defaultSuccessUrl("/dashboard")
    .userInfoEndpoint(info -> info
        .userService(customOAuth2UserService))
);

CORS with Security

// Security must delegate to the CorsConfigurationSource bean
http.cors(Customizer.withDefaults());   // picks up CorsConfigurationSource bean

@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowedOrigins(List.of("https://myapp.com"));
    config.setAllowedMethods(List.of("GET","POST","PUT","DELETE","OPTIONS"));
    config.setAllowedHeaders(List.of("Authorization","Content-Type"));
    config.setAllowCredentials(true);
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", config);
    return source;
}

CSRF

// Disable for stateless REST APIs (use JWT/tokens instead)
http.csrf(csrf -> csrf.disable());

// Stateful apps — CSRF is enabled by default
// With Thymeleaf: _csrf token is auto-included in forms
// With SPA: use CookieCsrfTokenRepository
http.csrf(csrf -> csrf
    .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
);

Security Context Access

// Get current user anywhere
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName();
Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();

// In a controller
@GetMapping("/me")
public UserDto me(@AuthenticationPrincipal UserDetails userDetails) {
    return userService.findByEmail(userDetails.getUsername());
}

// With a custom UserDetails implementation
@GetMapping("/me")
public UserDto me(@AuthenticationPrincipal AppUser user) {
    return mapper.toDto(user);
}

Security Testing

@SpringBootTest
@AutoConfigureMockMvc
class ArticleControllerTest {

    @Autowired MockMvc mvc;

    @Test
    @WithMockUser(roles = "ADMIN")   // simulate logged-in admin
    void adminCanDelete() throws Exception {
        mvc.perform(delete("/api/articles/1"))
           .andExpect(status().isNoContent());
    }

    @Test
    void anonymousUserGets401() throws Exception {
        mvc.perform(get("/api/articles/1"))
           .andExpect(status().isUnauthorized());
    }
}

Common Pitfalls

  • Blank page after logindefaultSuccessUrl needs alwaysUse=true as the second argument if users might hit /login directly.
  • CORS preflight blocked by Security — OPTIONS requests need .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() or rely on http.cors(withDefaults()) which handles preflight automatically.
  • @PreAuthorize not firing@EnableMethodSecurity is required; it does not activate automatically.
  • Role prefix confusionhasRole("ADMIN") checks for ROLE_ADMIN; hasAuthority("ROLE_ADMIN") is the explicit equivalent; never double-prefix.
  • SecurityContextHolder in async threads — the security context is thread-local; for @Async methods, set MODE_INHERITABLETHREADLOCAL or pass credentials explicitly.
  • JWT filter running for non-API paths — use shouldNotFilter() override to skip static resources and public paths.