Content created by using AI tool

A comprehensive guide to understanding and implementing security in Spring applications - from basic authentication to advanced security features.

Get Started

Introduction to Spring Security

Spring Security is a powerful and highly customizable authentication and access-control framework. It is the de facto standard for securing Spring-based applications and offers comprehensive security services for Java EE-based enterprise software applications.

Spring Security addresses two primary concerns:

  • Authentication - Verifying that a user is who they claim to be
  • Authorization - Deciding if a user is allowed to perform a requested action

Beyond these core features, Spring Security provides protection against common security vulnerabilities such as CSRF attacks, session fixation, clickjacking, and more.

Why Spring Security?

Spring Security offers several advantages over implementing security mechanisms manually:

  • Comprehensive security framework with proven protection against common attacks
  • Highly customizable to fit various application requirements
  • Seamless integration with Spring ecosystem
  • Active community support and regular updates
  • Support for various authentication protocols and providers

Core Concepts

Authentication

Authentication is the process of verifying that users are who they claim to be. Spring Security supports various authentication mechanisms:

  • Username/password authentication
  • OAuth 2.0 / OpenID Connect
  • LDAP authentication
  • JWT (JSON Web Token) based authentication
  • Custom authentication methods

The central interface for authentication is AuthenticationManager, which has a single method:

public interface AuthenticationManager {
  Authentication authenticate(Authentication authentication) 
      throws AuthenticationException;
}

The most common implementation is ProviderManager, which delegates to a chain of AuthenticationProvider instances.

Authorization

Authorization determines what authenticated users are allowed to do. Spring Security provides comprehensive support for authorization:

  • URL-based authorization
  • Method-level security
  • Domain object security (ACLs)
  • Expression-based access control

Access decisions are made by AccessDecisionManager, which delegates to a list of AccessDecisionVoter implementations.

A simplified authorization check might look like:

if (hasRole("ADMIN") || (hasRole("USER") && object.getOwner().equals(authentication.getName()))) {
  // Grant access
} else {
  // Deny access
}

Filter Chain

Spring Security uses a chain of servlet filters to provide various security services. Each filter has a specific responsibility and can be enabled, disabled, or customized as needed.

Some key filters include:

  • SecurityContextPersistenceFilter - Stores the SecurityContext between requests
  • UsernamePasswordAuthenticationFilter - Processes form login authentication
  • BasicAuthenticationFilter - Processes HTTP Basic authentication
  • RememberMeAuthenticationFilter - Handles remember-me authentication
  • FilterSecurityInterceptor - Makes access control decisions

The filter chain processes each request sequentially:

Spring Security Filter Chain

Security Context

The SecurityContext holds authentication information for the current thread of execution. It can be accessed throughout your application:

SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
String username = authentication.getName();
Object principal = authentication.getPrincipal();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

By default, the SecurityContextHolder uses a ThreadLocal to store the context, making it available to all methods executed in the same thread.

Getting Started with Spring Security

Adding Dependencies

To use Spring Security in your Spring Boot application, add the following dependency to your pom.xml (Maven):

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

Or in your build.gradle (Gradle):

implementation 'org.springframework.boot:spring-boot-starter-security'

Auto-Configuration

Spring Boot's auto-configuration will automatically set up basic security with form-based login when the Spring Security dependency is added.

Basic Configuration

Create a security configuration class:

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((authz) -> authz
                .requestMatchers("/", "/home").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin((form) -> form
                .loginPage("/login")
                .permitAll()
            )
            .logout((logout) -> logout.permitAll());
            
        return http.build();
    }
    
    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails user = User.withDefaultPasswordEncoder()
            .username("user")
            .password("password")
            .roles("USER")
            .build();
            
        return new InMemoryUserDetailsManager(user);
    }
}

Note:

Using withDefaultPasswordEncoder() is not recommended for production! It's only shown here for simplicity. For production, use a proper password encoder like BCryptPasswordEncoder.

Your First Secure Application

Let's build a simple secure application with a protected page and a login form.

Controller

@Controller
public class MainController {
    
    @GetMapping("/")
    public String home() {
        return "home";
    }
    
    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }
    
    @GetMapping("/login")
    public String login() {
        return "login";
    }
}

Login Template (Thymeleaf)

<!-- login.html -->
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
    <head>
        <title>Login</title>
    </head>
    <body>
        <h1>Login</h1>
        <div th:if="${param.error}">
            Invalid username and password.
        </div>
        <div th:if="${param.logout}">
            You have been logged out.
        </div>
        <form th:action="@{/login}" method="post">
            <div>
                <label>Username: <input type="text" name="username"/></label>
            </div>
            <div>
                <label>Password: <input type="password" name="password"/></label>
            </div>
            <div>
                <input type="submit" value="Sign In"/>
            </div>
        </form>
    </body>
</html>

With this setup, the home page is publicly accessible, but the /hello page requires authentication.

Authentication Mechanisms

Form Login

Form login is the most common authentication mechanism for web applications.

http
    .formLogin(form -> form
        .loginPage("/login")
        .loginProcessingUrl("/perform_login")
        .defaultSuccessUrl("/home", true)
        .failureUrl("/login?error=true")
        .usernameParameter("username")
        .passwordParameter("password")
    );

Key configuration options:

  • loginPage - Custom login page URL
  • loginProcessingUrl - URL to submit the username and password
  • defaultSuccessUrl - Where to redirect after successful login
  • failureUrl - Where to redirect after failed login
  • usernameParameter and passwordParameter - Custom parameter names

HTTP Basic Authentication

HTTP Basic is useful for API authentication or simple applications.

http
    .httpBasic(basic -> basic
        .realmName("My Application")
    );

HTTP Basic sends credentials with each request, typically in an Authorization header:

Authorization: Basic dXNlcjpwYXNzd29yZA==

Security Note:

HTTP Basic sends credentials encoded (not encrypted). Always use HTTPS when using HTTP Basic authentication.

OAuth 2.0 and OpenID Connect

Spring Security provides comprehensive support for OAuth 2.0 and OpenID Connect.

OAuth 2.0 Client

Add the dependency:

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

Configure OAuth providers in application.properties:

spring.security.oauth2.client.registration.github.client-id=your-github-client-id
spring.security.oauth2.client.registration.github.client-secret=your-github-client-secret

spring.security.oauth2.client.registration.google.client-id=your-google-client-id
spring.security.oauth2.client.registration.google.client-secret=your-google-client-secret

Configure the security:

http
    .oauth2Login(oauth2 -> oauth2
        .loginPage("/login")
        .defaultSuccessUrl("/home")
        .failureUrl("/login?error=true")
    );

JWT Authentication

JSON Web Tokens (JWT) are commonly used for stateless authentication in RESTful APIs.

Add the dependency:

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>

Create JWT utility class:

@Component
public class JwtTokenUtil {
    
    @Value("${jwt.secret}")
    private String secret;
    
    @Value("${jwt.expiration}")
    private Long expiration;
    
    public String generateToken(UserDetails userDetails) {
        Map claims = new HashMap<>();
        return createToken(claims, userDetails.getUsername());
    }
    
    private String createToken(Map claims, String subject) {
        return Jwts.builder()
            .setClaims(claims)
            .setSubject(subject)
            .setIssuedAt(new Date(System.currentTimeMillis()))
            .setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
            .signWith(SignatureAlgorithm.HS512, secret)
            .compact();
    }
    
    public Boolean validateToken(String token, UserDetails userDetails) {
        final String username = getUsernameFromToken(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }
    
    public String getUsernameFromToken(String token) {
        return getClaimFromToken(token, Claims::getSubject);
    }
    
    private  T getClaimFromToken(String token, Function claimsResolver) {
        final Claims claims = getAllClaimsFromToken(token);
        return claimsResolver.apply(claims);
    }
    
    private Claims getAllClaimsFromToken(String token) {
        return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
    }
    
    private Boolean isTokenExpired(String token) {
        final Date expiration = getExpirationDateFromToken(token);
        return expiration.before(new Date());
    }
    
    public Date getExpirationDateFromToken(String token) {
        return getClaimFromToken(token, Claims::getExpiration);
    }
}

Create JWT filter:

public class JwtRequestFilter extends OncePerRequestFilter {
    
    @Autowired
    private UserDetailsService userDetailsService;
    
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        
        final String requestTokenHeader = request.getHeader("Authorization");
        
        String username = null;
        String jwtToken = null;
        
        // JWT Token is in the form "Bearer token"
        if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
            jwtToken = requestTokenHeader.substring(7);
            try {
                username = jwtTokenUtil.getUsernameFromToken(jwtToken);
            } catch (IllegalArgumentException e) {
                System.out.println("Unable to get JWT Token");
            } catch (ExpiredJwtException e) {
                System.out.println("JWT Token has expired");
            }
        } else {
            logger.warn("JWT Token does not begin with Bearer String");
        }
        
        // Once we get the token validate it
        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
            
            // if token is valid configure Spring Security to manually set authentication
            if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                
                // After setting the Authentication in the context, we specify
                // that the current user is authenticated
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }
        chain.doFilter(request, response);
    }
}

Configure JWT in Spring Security:

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Autowired
    private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    
    @Autowired
    private UserDetailsService jwtUserDetailsService;
    
    @Autowired
    private JwtRequestFilter jwtRequestFilter;
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(authz -> authz
                .requestMatchers("/authenticate").permitAll()
                .anyRequest().authenticated()
            )
            .exceptionHandling(ex -> ex.authenticationEntryPoint(jwtAuthenticationEntryPoint))
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
        
        // Add JWT filter
        http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
        
        return http.build();
    }
    
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
        return authConfig.getAuthenticationManager();
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

Create an authentication controller:

@RestController
public class AuthenticationController {
    
    @Autowired
    private AuthenticationManager authenticationManager;
    
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    
    @Autowired
    private UserDetailsService userDetailsService;
    
    @PostMapping("/authenticate")
    public ResponseEntity createAuthenticationToken(@RequestBody AuthenticationRequest authenticationRequest) throws Exception {
        try {
            authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                    authenticationRequest.getUsername(), 
                    authenticationRequest.getPassword()
                )
            );
        } catch (BadCredentialsException e) {
            throw new Exception("Incorrect username or password", e);
        }
        
        final UserDetails userDetails = userDetailsService
            .loadUserByUsername(authenticationRequest.getUsername());
        
        final String token = jwtTokenUtil.generateToken(userDetails);
        
        return ResponseEntity.ok(new AuthenticationResponse(token));
    }
}

JWT Benefits

JWT offers several advantages for API authentication:

  • Stateless - no need to store session information on the server
  • Portable - the same token can be used across multiple backends
  • Self-contained - contains all necessary user information
  • Efficient - reduces database lookups for user information

Authorization Configuration

URL Security

URL security is the most common way to secure web applications. It restricts access to specific URLs based on user roles or authorities.

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(authz -> authz
            .requestMatchers("/", "/home", "/public/**").permitAll()
            .requestMatchers("/admin/**").hasRole("ADMIN")
            .requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
            .requestMatchers("/api/**").hasAuthority("API_ACCESS")
            .anyRequest().authenticated()
        );
    return http.build();
}

Key methods for configuring URL security:

  • permitAll() - Allows all users (including anonymous) to access
  • hasRole() - Requires a specific role (automatically prefixed with "ROLE_")
  • hasAnyRole() - Requires any of the specified roles
  • hasAuthority() - Requires a specific authority (no prefix added)
  • hasAnyAuthority() - Requires any of the specified authorities
  • authenticated() - Requires the user to be authenticated
  • denyAll() - Denies access to all users

Method Security

Method security allows you to secure individual methods in your Spring components.

Enable method security in your configuration:

@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {
    // Configuration
}

Use annotations to secure methods:

@Service
public class UserService {
    
    @PreAuthorize("hasRole('ADMIN')")
    public List getAllUsers() {
        // Only admins can access this method
        return userRepository.findAll();
    }
    
    @PreAuthorize("hasRole('ADMIN') or #username == authentication.principal.username")
    public User getUserByUsername(String username) {
        // Admins can access any user, users can only access their own data
        return userRepository.findByUsername(username);
    }
    
    @PostAuthorize("returnObject.username == authentication.principal.username")
    public User getUser(Long id) {
        // Only return the result if the user is accessing their own data
        return userRepository.findById(id).orElse(null);
    }
    
    @Secured("ROLE_ADMIN")
    public void deleteUser(Long id) {
        // Only admins can delete users
        userRepository.deleteById(id);
    }
}

Common method security annotations:

  • @PreAuthorize - Checks conditions before method execution
  • @PostAuthorize - Checks conditions after method execution, can use the return value
  • @Secured - Simple role-based check
  • @RolesAllowed - Java EE equivalent of @Secured
  • @PreFilter - Filters collection arguments before method execution
  • @PostFilter - Filters the returned collection

Expression-Based Security

Spring Security supports expressions for fine-grained access control.

Common expressions used in @PreAuthorize and @PostAuthorize:

// Basic authentication checks
@PreAuthorize("isAuthenticated()")
@PreAuthorize("isAnonymous()")

// Role checks
@PreAuthorize("hasRole('ADMIN')")
@PreAuthorize("hasAnyRole('ADMIN', 'MANAGER')")

// Authority checks
@PreAuthorize("hasAuthority('READ_PRIVILEGE')")
@PreAuthorize("hasAnyAuthority('READ_PRIVILEGE', 'WRITE_PRIVILEGE')")

// Method parameter access
@PreAuthorize("#id == authentication.principal.id")

// Logical operators
@PreAuthorize("hasRole('ADMIN') or hasRole('MANAGER')")
@PreAuthorize("hasRole('ADMIN') and hasAuthority('WRITE_PRIVILEGE')")

// More complex expressions
@PreAuthorize("hasRole('ADMIN') or (hasRole('USER') and #user.id == authentication.principal.id)")

// With method return values
@PostAuthorize("returnObject.owner == authentication.principal.username")

Custom security expressions:

@Component
public class CustomSecurityExpressions {
    
    public boolean isOwner(Authentication authentication, Object targetObject) {
        if (targetObject instanceof OwnedResource) {
            return ((OwnedResource) targetObject).getOwner().equals(authentication.getName());
        }
        return false;
    }
}

// Configuration to use custom expressions
@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
    
    @Autowired
    private CustomSecurityExpressions customSecurityExpressions;
    
    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
        DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
        expressionHandler.setPermissionEvaluator(new CustomPermissionEvaluator());
        expressionHandler.setRootObject(customSecurityExpressions);
        return expressionHandler;
    }
}

Best Practice

Always prefer method security over URL security for fine-grained access control. URL security is easier to configure but less flexible.

Advanced Topics

CSRF Protection

Cross-Site Request Forgery (CSRF) is an attack that forces users to execute unwanted actions on a web application they're currently authenticated to.

Spring Security enables CSRF protection by default for HTML form submissions. To configure it:

http
    .csrf(csrf -> csrf
        .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
    );

To disable CSRF protection (for example, for RESTful APIs using token-based authentication):

http
    .csrf(csrf -> csrf.disable());

With Thymeleaf, CSRF tokens are automatically included in forms:

<form th:action="@{/process}" method="post">
    <!-- CSRF token automatically included by Thymeleaf -->
    <input type="text" name="username" />
    <button type="submit">Submit</button>
</form>

For other template engines or JavaScript applications, you need to manually include the CSRF token:

<form action="/process" method="post">
    <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />
    <input type="text" name="username" />
    <button type="submit">Submit</button>
</form>

For AJAX requests, you can include the token in a header:

const token = document.querySelector('meta[name="_csrf"]').content;
const header = document.querySelector('meta[name="_csrf_header"]').content;

fetch('/api/data', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        [header]: token
    },
    body: JSON.stringify({ key: 'value' })
});

CORS Configuration

Cross-Origin Resource Sharing (CORS) is a mechanism that allows restricted resources on a web page to be requested from another domain outside the domain from which the first resource was served.

Configure CORS in Spring Security:

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

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .cors(cors -> cors.configurationSource(corsConfigurationSource()))
        // other configuration
        ;
    return http.build();
}

For a simpler configuration with Spring Boot, you can use @CrossOrigin annotation:

@RestController
@CrossOrigin(origins = "https://example.com")
public class UserController {
    
    @GetMapping("/users")
    public List getUsers() {
        // ...
    }
}

Password Encoding

Storing passwords securely is crucial for application security. Spring Security provides several password encoders:

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

Available password encoders:

  • BCryptPasswordEncoder - Uses the bcrypt strong hashing function
  • Pbkdf2PasswordEncoder - Uses PBKDF2 algorithm
  • SCryptPasswordEncoder - Uses scrypt algorithm
  • Argon2PasswordEncoder - Uses Argon2 algorithm
  • DelegatingPasswordEncoder - Delegates to other encoders based on a prefix

Using the delegating password encoder (recommended):

@Bean
public PasswordEncoder passwordEncoder() {
    return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}

This encoder prefixes passwords with the encoder ID, allowing you to upgrade your encoding strategy while still supporting existing passwords:

// Passwords in the database might look like:
// {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
// {pbkdf2}e4e76f1a012340a3...
// {scrypt}$e0801$8bWJaSu2...
            
// To encode a new password:
String encoded = passwordEncoder.encode("myPassword");
// Result: {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

// To match a password:
boolean matches = passwordEncoder.matches("myPassword", encoded);
// Result: true

Remember Me Authentication

Remember Me functionality allows users to stay logged in between browser sessions.

http
    .rememberMe(remember -> remember
        .key("uniqueAndSecretKey")
        .tokenValiditySeconds(86400) // 1 day
    );

For enhanced security, use persistent token storage:

@Bean
public PersistentTokenRepository persistentTokenRepository() {
    JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
    tokenRepository.setDataSource(dataSource);
    // tokenRepository.setCreateTableOnStartup(true); // Uncomment for first run
    return tokenRepository;
}

http
    .rememberMe(remember -> remember
        .tokenRepository(persistentTokenRepository())
        .tokenValiditySeconds(86400) // 1 day
    );

Include the remember-me option in the login form:

<form th:action="@{/login}" method="post">
    <input type="text" name="username" />
    <input type="password" name="password" />
    <label>
        <input type="checkbox" name="remember-me" /> Remember me
    </label>
    <button type="submit">Login</button>
</form>

Session Management

Configure session management to control how Spring Security uses sessions:

http
    .sessionManagement(session -> session
        .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
        .invalidSessionUrl("/invalid-session")
        .maximumSessions(1)
        .maxSessionsPreventsLogin(true)
        .expiredUrl("/session-expired")
    );

Session creation policies:

  • ALWAYS - Always create a session
  • NEVER - Never create a session, but use existing ones
  • IF_REQUIRED - Only create a session if needed (default)
  • STATELESS - Don't create or use sessions (for REST APIs)

Concurrent session control:

  • maximumSessions - Maximum concurrent sessions per user
  • maxSessionsPreventsLogin - If true, prevents new logins when max sessions reached
  • expiredUrl - Where to redirect when a session expires

Session fixation protection:

http
    .sessionManagement(session -> session
        .sessionFixation().migrateSession()
    );

Session fixation protection strategies:

  • migrateSession - Create a new session and copy attributes (default)
  • newSession - Create a new session without copying attributes
  • none - Do nothing (not recommended)

Best Practices

Security Checklist

  1. Use HTTPS for all authenticated pages
  2. Use strong password encoding (BCrypt, Argon2, PBKDF2, or SCrypt)
  3. Implement proper CSRF protection
  4. Configure CORS correctly for APIs
  5. Set secure and HttpOnly flags on cookies
  6. Implement proper session management
  7. Use Content Security Policy headers
  8. Validate and sanitize all user input
  9. Implement proper error handling (don't expose sensitive information)
  10. Use principle of least privilege for authorization

Security Headers

Configure security headers to protect against common attacks:

http
    .headers(headers -> headers
        .contentSecurityPolicy(csp -> csp.policyDirectives("default-src 'self'"))
        .frameOptions(frame -> frame.deny())
        .xssProtection(xss -> xss.block(true))
        .contentTypeOptions(content -> content.disable())
    );

Authentication Best Practices

  • Implement rate limiting for login attempts
  • Use multi-factor authentication for sensitive applications
  • Create custom authentication success/failure handlers
  • Log authentication events
  • Implement account lockout policies
@Bean
public DaoAuthenticationProvider authenticationProvider() {
    DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
    provider.setUserDetailsService(userDetailsService);
    provider.setPasswordEncoder(passwordEncoder());
    provider.setHideUserNotFoundExceptions(false); // For debugging only
    return provider;
}

http
    .formLogin(form -> form
        .successHandler(new CustomAuthenticationSuccessHandler())
        .failureHandler(new CustomAuthenticationFailureHandler())
    );

Authorization Best Practices

  • Use method security for fine-grained access control
  • Implement custom permission evaluators for complex scenarios
  • Don't hardcode roles and authorities
  • Follow the principle of least privilege

OAuth 2.0 Best Practices

  • Use authorization code flow with PKCE for web applications
  • Validate redirect URIs
  • Use short-lived access tokens and refresh tokens
  • Implement proper scope management
  • Validate JWT claims (issuer, audience, expiration)

Additional Resources

Official Documentation

Books

  • "Spring Security in Action" by Laurentiu Spilca
  • "Spring Boot in Action" by Craig Walls
  • "Hands-On Spring Security 5 for Reactive Applications" by Tomcy John

Tutorials and Courses

  • Spring Security Guides
  • Baeldung Spring Security Series
  • Spring Security on Pluralsight
  • Spring Academy courses

GitHub Projects