Security — Cheat Sheet
Spring Boot · 12 topics. Download the PDF or the Instagram carousel and share it.
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.
@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();
}
}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.
@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);
}
}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
@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));
}
}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)
# 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();
}
}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
@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();
}
}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.
@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();
}
}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.
// 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));
}
}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.
<!-- 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();
}
}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.
// 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()
)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.
@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();
}
}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.
@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() { ... }
}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.
@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());
}
}