Spring Security 6.x: Authentication, Authorization & Session vs JWT Compared

This comprehensive guide explores Spring Security 6.x core mechanisms including authentication flow with SecurityFilterChain and AuthenticationManager, authorization via URL and method-level security with SpEL expressions, and compares Session-based vs JWT stateless authentication with OAuth2 integration and CSRF protection strategies.

CodeSmart Hoops
CodeSmart Hoops
CodeSmart Hoops
Spring Security 6.x: Authentication, Authorization & Session vs JWT Compared

1. Introduction: Developer A's Security Challenge

A developer faces a security compliance review before launch: plaintext passwords, no access control, missing CSRF protection, no brute-force prevention, and no audit logs. Initially attempting custom interceptors leads to scattered logic and overlooked complexities like password encoding, session management, concurrency control, and Remember-Me. A senior colleague recommends Spring Security.

2. Core Concepts

2.1 Authentication vs Authorization

Two fundamental concepts often confused:

Core Question : Authentication asks "Who are you?"; Authorization asks "What can you do?"

Trigger Timing : Authentication occurs at first system access (login); Authorization occurs when accessing specific resources

Verification Content : Authentication verifies username/password, token, certificate; Authorization verifies roles, permissions, data scope

Failure Response : Authentication failure returns 401 Unauthorized; Authorization failure returns 403 Forbidden

Key Components : Authentication uses AuthenticationManager, UserDetailsService; Authorization uses AccessDecisionManager, @PreAuthorize

Analogy:

Authentication = Guard checking ID. Confirm identity before entry.

Authorization = Floor access card. Being in the building doesn't grant access to all floors; finance room requires finance badge.

Diagram:

Authentication vs Authorization diagram
Authentication vs Authorization diagram

2.2 RBAC Permission Model

Role-Based Access Control is the most common model:

User --> Role --> Permission
  A        ADMIN        user:list
  B        USER         article:read
                         order:create

Typical database schema:

sys_user (id, username, password, ...)
sys_role (id, role_code, role_name, ...)
sys_permission (id, perm_code, perm_name, url, ...)
sys_user_role (user_id, role_id)
sys_role_permission (role_id, perm_id)

In Spring Security:

Authority : Fine-grained operation permission, e.g., user:add, order:delete. Checked via hasAuthority().

Role : Collection of permissions, essentially also an Authority. Spring Security adds ROLE_ prefix by default; checked via hasRole() (prefix not needed).

// hasAuthority needs full permission string
@PreAuthorize("hasAuthority('user:add')")

// hasRole auto-adds ROLE_ prefix
@PreAuthorize("hasRole('ADMIN')")
// Equivalent to
@PreAuthorize("hasAuthority('ROLE_ADMIN')")

2.3 Spring Security Core Components

Components organized in four layers:

+------------------------+
|  Configuration Layer   |  SecurityFilterChain, @EnableMethodSecurity
+------------------------+
|  Authentication Layer  |  AuthenticationManager, ProviderManager,
|                        |  AuthenticationProvider, UserDetailsService
+------------------------+
|  Authorization Layer   |  AccessDecisionManager, AccessDecisionVoter,
|                        |  AuthorizationManager
+------------------------+
|  Context Layer         |  SecurityContextHolder, SecurityContext,
|                        |  Authentication
+------------------------+

Key interfaces and typical implementations:

SecurityFilterChain : Defines security filter chain. Typical implementation: Lambda DSL configuration

AuthenticationManager : Authentication manager interface. Typical implementation: ProviderManager AuthenticationProvider : Specific authentication provider. Typical implementation: DaoAuthenticationProvider UserDetailsService : Loads user information. Typical implementation: Custom implementation

PasswordEncoder : Password encoding/decoding. Typical implementation: BCryptPasswordEncoder AccessDecisionManager : Authorization decision. Typical implementation: AffirmativeBased AuthorizationManager : New-generation authorization manager (5.5+). Typical implementation: AuthorizationManagers SecurityContextHolder : Security context holder. Typical implementation: ThreadLocal storage

3. Background Context

3.1 Evolution: From Acegi to Spring Security

Spring Security originated as Acegi Security, an independent open-source framework, incorporated into Spring around 2007.

Acegi Security (2004-2007): Independent framework, XML configuration

Spring Security 3.x (2009-2013): Joined Spring ecosystem, namespace configuration

Spring Security 4.x (2014-2017): Java Config recommended, CSRF enabled by default

Spring Security 5.x (2017-2022): OAuth2/OIDC support, BCrypt default PasswordEncoder, AuthorizationManager introduced

Spring Security 6.x (2023-present): Removed WebSecurityConfigurerAdapter, requires Spring Framework 6 + JDK 17+, Lambda DSL only

3.2 Spring Security vs Apache Shiro

Learning Curve : Spring Security steep (many concepts, complex config); Shiro gentle (concise intuitive API)

Feature Completeness : Spring Security covers AuthN, AuthZ, CSRF, Session, OAuth2, ACL; Shiro covers AuthN, AuthZ, Session, Crypto

Spring Integration : Spring Security native with Spring Boot auto-configuration; Shiro requires bridge dependencies

Community Activity : Spring Security high (official Spring maintenance); Shiro low (Apache incubator, slow updates)

Suitable Scenarios : Spring Security for medium/large projects needing full security stack; Shiro for small projects, quick integration

Configuration Style : Spring Security uses Java DSL / Annotations; Shiro uses Java API

Caching Support : Spring Security needs manual integration; Shiro has built-in CacheManager interface

Documentation Quality : Spring Security comprehensive but verbose; Shiro concise but incomplete

Shiro code style:

// Shiro: concise and intuitive
Subject subject = SecurityUtils.getSubject();
subject.login(new UsernamePasswordToken(username, password));
subject.hasRole("ADMIN");
subject.checkPermission("user:delete");

Spring Security code style:

// Spring Security: configuration-driven, automatic security context management
@Autowired
private AuthenticationManager authenticationManager;

Authentication auth = authenticationManager.authenticate(
    new UsernamePasswordAuthenticationToken(username, password)
);
SecurityContextHolder.getContext().setAuthentication(auth);

3.3 Spring Boot 3.x & Spring Security 6.x Breaking Changes

1. Removal of WebSecurityConfigurerAdapter

// Spring Security 5.x (deprecated)
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/admin/**").hasRole("ADMIN")
            .anyRequest().authenticated();
    }
}

// Spring Security 6.x (recommended)
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(auth -> auth
            .requestMatchers("/admin/**").hasRole("ADMIN")
            .anyRequest().authenticated()
        );
        return http.build();
    }
}

2. authorizeRequests → authorizeHttpRequests

Old: authorizeRequests() uses AntPathRequestMatcher New: authorizeHttpRequests() uses ServletRequestMatcher with requestMatchers() replacing antMatchers() 3. httpBasic() / formLogin() parameterless

// Old
http.httpBasic().and().formLogin();

// New - no parameters, config via Consumer
http.httpBasic(Customizer.withDefaults());
http.formLogin(Customizer.withDefaults());

4. Migration to AuthorizationManager AccessDecisionManager (voting) marked deprecated

New AuthorizationManager<T> interface replaces it

5. Stricter Default Security Policies

CSRF enabled by default

Security headers (X-Content-Type-Options, X-Frame-Options) enabled by default

Cache control headers default to

no-cache

4. Analogy: Building Security System

Guard checks ID → AuthenticationManager: Confirms user identity

ID authenticity verification → AuthenticationProvider: Validates credentials

Resident registry → UserDetailsService: Queries user info

Floor access card → AuthorizationManager: Checks entry permission

Access card permission table → Authority / Role: Defines who accesses which areas

Surveillance cameras → Audit logs / AuditorAware: Records operation traces

Temporary visitor pass → Remember-Me / Token: Short-term exemption from verification

Security duty roster → SecurityFilterChain: Ordered security process execution

5. Core Part 1: Authentication Flow

5.1 SecurityFilterChain Configuration

Spring Security centers on a Servlet Filter chain processing all HTTP requests.

Diagram:

SecurityFilterChain diagram
SecurityFilterChain diagram

Base configuration template (Spring Boot 3.x / Spring Security 6.x):

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // CORS config
            .cors(Customizer.withDefaults())
            // CSRF config
            .csrf(csrf -> csrf.disable())
            // Session management
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            // Authorization config
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/login", "/api/auth/register").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .requestMatchers("/api/user/**").hasAnyRole("ADMIN", "USER")
                .requestMatchers("/actuator/health").permitAll()
                .anyRequest().authenticated()
            )
            // Login config
            .formLogin(form -> form
                .loginProcessingUrl("/api/auth/login")
                .permitAll()
            )
            // Logout config
            .logout(logout -> logout
                .logoutUrl("/api/auth/logout")
                .permitAll()
            )
            // Exception handling
            .exceptionHandling(ex -> ex
                .authenticationEntryPoint(new JwtAuthenticationEntryPoint())
                .accessDeniedHandler(new JwtAccessDeniedHandler())
            );
        return http.build();
    }
}

5.2 Authentication Filter Chain

Default filters registered by Spring Security (in execution order): CorsFilter: Cross-origin request handling CsrfFilter: CSRF protection LogoutFilter: Logout processing UsernamePasswordAuthenticationFilter: Form login authentication BasicAuthenticationFilter: HTTP Basic authentication RequestCacheAwareFilter: Request caching (pre-login page) SecurityContextHolderAwareRequestFilter: Wraps Request AnonymousAuthenticationFilter: Anonymous identity population SessionManagementFilter: Session management ExceptionTranslationFilter: AuthN/AuthZ exception translation FilterSecurityInterceptor: Final authorization check

Custom JWT authentication filter example:

@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    private final JwtTokenProvider jwtTokenProvider;
    private final UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(
            @NonNull HttpServletRequest request,
            @NonNull HttpServletResponse response,
            @NonNull FilterChain filterChain) throws ServletException, IOException {

        String token = resolveToken(request);

        if (token != null && jwtTokenProvider.validateToken(token)) {
            String username = jwtTokenProvider.getUsername(token);
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);

            // Set security context
            UsernamePasswordAuthenticationToken authentication =
                new UsernamePasswordAuthenticationToken(
                    userDetails, null, userDetails.getAuthorities()
                );
            authentication.setDetails(
                new WebAuthenticationDetailsSource().buildDetails(request)
            );
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        filterChain.doFilter(request, response);
    }

    private String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

Add custom filter in SecurityFilterChain:

http.addFilterBefore(jwtAuthenticationFilter,
    UsernamePasswordAuthenticationFilter.class);

5.3 AuthenticationManager Workflow

AuthenticationManager

is the authentication entry point; actual implementation is ProviderManager maintaining a list of AuthenticationProvider.

Diagram:

AuthenticationManager workflow
AuthenticationManager workflow

Core authentication objects:

// 1. Unauthenticated token (user-submitted credentials)
UsernamePasswordAuthenticationToken unauthenticated =
    new UsernamePasswordAuthenticationToken(username, password);

// 2. AuthenticationManager performs authentication
Authentication authenticated = authenticationManager.authenticate(unauthenticated);

// 3. Set to SecurityContext
SecurityContextHolder.getContext().setAuthentication(authenticated);
Authentication

interface core structure:

public interface Authentication extends Principal, Serializable {
    // Permission list
    Collection<? extends GrantedAuthority> getAuthorities();
    // Credentials (password) - usually cleared after success
    Object getCredentials();
    // User details
    Object getDetails();
    // User identity identifier
    Object getPrincipal();
    // Whether authenticated
    boolean isAuthenticated();
    void setAuthenticated(boolean isAuthenticated);
}

5.4 UserDetailsService Custom User Loading

UserDetailsService

loads user info by username, key for custom authentication logic.

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
    private final SysUserMapper userMapper;
    private final SysPermissionMapper permissionMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // Query user
        SysUser user = userMapper.selectByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("User not found: " + username);
        }

        // Query user permissions
        List<String> permissions = permissionMapper.selectPermissionsByUserId(user.getId());
        List<GrantedAuthority> authorities = permissions.stream()
            .map(SimpleGrantedAuthority::new)
            .collect(Collectors.toList());

        // Build UserDetails
        return new User(
            user.getUsername(),
            user.getPassword(),
            user.getEnabled(),
            true,  // accountNonExpired
            true,  // credentialsNonExpired
            user.getAccountLocked(), // accountNonLocked (note: field meaning opposite)
            authorities
        );
    }
}

Extended UserDetails custom implementation (recommended for extra fields like user ID):

public class SecurityUser implements UserDetails {
    private final Long userId;
    private final String username;
    private final String password;
    private final List<GrantedAuthority> authorities;
    private final boolean enabled;

    public SecurityUser(SysUser user, List<GrantedAuthority> authorities) {
        this.userId = user.getId();
        this.username = user.getUsername();
        this.password = user.getPassword();
        this.authorities = authorities;
        this.enabled = user.getEnabled();
    }

    public Long getUserId() {
        return userId;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }
    // accountNonExpired, credentialsNonExpired, accountNonLocked default to true
}

Getting current user info:

// Method 1: via SecurityContextHolder
SecurityUser currentUser = (SecurityUser)
    SecurityContextHolder.getContext().getAuthentication().getPrincipal();
Long userId = currentUser.getUserId();

// Method 2: via @AuthenticationPrincipal annotation
@GetMapping("/me")
public ResponseEntity<UserVO> getCurrentUser(
    @AuthenticationPrincipal SecurityUser currentUser) {
    return ResponseEntity.ok(convertToVO(currentUser));
}

// Method 3: via HandlerMethodArgumentResolver auto-injection
@GetMapping("/info")
public ResponseEntity<UserVO> getUserInfo(@CurrentUser SecurityUser user) {
    // ...
}

5.5 Password Encoder

Since Spring Security 5.x, plaintext passwords are no longer accepted by default . Recommended: BCryptPasswordEncoder or Argon2PasswordEncoder.

@Configuration
public class PasswordConfig {
    @Bean
    public PasswordEncoder passwordEncoder() {
        // Option 1: BCrypt (recommended, balance of performance & security)
        return new BCryptPasswordEncoder(12); // 12 is strength, range 4-31

        // Option 2: Argon2 (higher security, larger performance overhead)
        // return new Argon2PasswordEncoder(32, 16, 1, 65536, 1);
    }
}

Password encoding and verification:

// Encode password at registration
String rawPassword = "userInput123";
String encodedPassword = passwordEncoder.encode(rawPassword);
// Stored: $2a$12$XYZ...

// Verify password at login
boolean matches = passwordEncoder.matches(rawPassword, encodedPassword);

Common password encoder comparison:

NoOpPasswordEncoder : Security: None; Performance: Extremely fast; Recommended: Testing only, disabled in production

BCryptPasswordEncoder : Security: High; Performance: Medium; Recommended: General purpose

Argon2PasswordEncoder : Security: Very High; Performance: Slower; Recommended: High-security scenarios

Pbkdf2PasswordEncoder : Security: High; Performance: Medium; Recommended: When no native dependencies

SCryptPasswordEncoder : Security: High; Performance: Medium; Recommended: When configurable memory consumption needed

DelegatingPasswordEncoder : Security: -; Performance: -; Recommended: Multi-encoder compatibility migration DelegatingPasswordEncoder for multi-format compatibility (suitable for migration):

@Bean
public PasswordEncoder passwordEncoder() {
    Map<String, PasswordEncoder> encoders = new HashMap<>();
    encoders.put("bcrypt", new BCryptPasswordEncoder());
    encoders.put("sha256", new StandardPasswordEncoder());
    encoders.put("noop", NoOpPasswordEncoder.getInstance());

    // Prefix format: {bcrypt}... or {sha256}...
    return new DelegatingPasswordEncoder("bcrypt", encoders);
}

Storage format:

{bcrypt}$2a$10$...   -- BCrypt encoded
{sha256}abc123...    -- SHA-256 encoded
{noop}plaintext      -- Plaintext (legacy compatibility)

5.6 Login Success/Failure Handlers

Custom handlers enable flexible business logic:

@Component
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    private final JwtTokenProvider jwtTokenProvider;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
            HttpServletResponse response,
            Authentication authentication) throws IOException {
        SecurityUser user = (SecurityUser) authentication.getPrincipal();

        // Generate JWT Token
        String token = jwtTokenProvider.generateToken(user.getUsername());

        // Return JSON response
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(
            """{"code":200,"message":"Login successful","data":{"token":"%s"}}""".formatted(token)
        );
    }
}
@Component
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request,
            HttpServletResponse response,
            AuthenticationException exception) throws IOException {
        String message;
        if (exception instanceof BadCredentialsException) {
            message = "Invalid username or password";
        } else if (exception instanceof AccountExpiredException) {
            message = "Account expired";
        } else if (exception instanceof LockedException) {
            message = "Account locked";
        } else if (exception instanceof DisabledException) {
            message = "Account disabled";
        } else {
            message = "Login failed";
        }

        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(
            """{"code":401,"message":"%s"}""".formatted(message)
        );
    }
}

Register in SecurityFilterChain:

http.formLogin(form -> form
    .loginProcessingUrl("/api/auth/login")
    .successHandler(customAuthenticationSuccessHandler)
    .failureHandler(customAuthenticationFailureHandler)
    .permitAll()
);

6. Core Part 2: Authorization Mechanism

6.1 URL-Based Authorization

Most common authorization method, configured in SecurityFilterChain:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests(auth -> auth
        // Public endpoints
        .requestMatchers(
            "/api/auth/login",
            "/api/auth/register",
            "/actuator/health",
            "/swagger-ui/**",
            "/v3/api-docs/**"
        ).permitAll()
        // Admin endpoints
        .requestMatchers("/api/admin/**").hasRole("ADMIN")
        // Specific operations
        .requestMatchers(HttpMethod.POST, "/api/orders/**").hasAuthority("order:create")
        .requestMatchers(HttpMethod.PUT, "/api/orders/**").hasAuthority("order:update")
        .requestMatchers(HttpMethod.DELETE, "/api/orders/**").hasAuthority("order:delete")
        // User endpoints
        .requestMatchers("/api/user/**").hasAnyRole("ADMIN", "USER")
        // All other requests require authentication
        .anyRequest().authenticated()
    );
    return http.build();
}

Note : Rules match in declaration order; first match wins. More specific rules must come first.

// Wrong: broad rule first, specific rules never match
http.authorizeHttpRequests(auth -> auth
    .anyRequest().authenticated()
    .requestMatchers("/api/admin/**").hasRole("ADMIN") // never executed
);

// Correct: specific rules first
http.authorizeHttpRequests(auth -> auth
    .requestMatchers("/api/admin/**").hasRole("ADMIN")
    .anyRequest().authenticated()
);

6.2 Method-Level Authorization

Uses annotations, enabled in config class:

@Configuration
@EnableWebSecurity
@EnableMethodSecurity // Spring Security 6.x enables method-level security
public class SecurityConfig {
}

In Spring Security 6.x, @EnableGlobalMethodSecurity replaced by @EnableMethodSecurity:

Old: @EnableGlobalMethodSecurity(prePostEnabled = true) → New: @EnableMethodSecurity (Enables @PreAuthorize / @PostAuthorize)

Old: @EnableGlobalMethodSecurity(securedEnabled = true) → New: @EnableMethodSecurity(securedEnabled = true) (Enables @Secured)

Old: @EnableGlobalMethodSecurity(jsr250Enabled = true) → New: @EnableMethodSecurity(jsr250Enabled = true) (Enables @RolesAllowed)

Three annotation comparison:

// 1. @PreAuthorize - pre-execution check (supports SpEL, most flexible)
@PreAuthorize("hasAuthority('user:delete')")
public void deleteUser(Long userId) { }

// Data permission control
@PreAuthorize("#userId == authentication.principal.userId or hasRole('ADMIN')")
public UserProfile getUserProfile(@Param("userId") Long userId) { }

// 2. @PostAuthorize - post-execution check (for return value filtering)
@PostAuthorize("returnObject.ownerId == authentication.principal.userId")
public Order getOrder(@Param("orderId") Long orderId) { }

// 3. @Secured - role-only (no SpEL, simpler)
@Secured({"ROLE_ADMIN", "ROLE_MANAGER"})
public void deleteOrder(Long orderId) { }

// 4. JSR-250 @RolesAllowed
@RolesAllowed("ADMIN")
public void updateUser(UserDTO user) { }
@PreAuthorize

vs @Secured comparison:

SpEL Support : @PreAuthorize full support; @Secured none

Method Parameter Reference : @PreAuthorize yes via #userId; @Secured no

Return Value Reference : @PreAuthorize yes via @PostAuthorize; @Secured no

Role Prefix : @PreAuthorize auto-handles ROLE_; @Secured requires full ROLE_ prefix

Complex Expressions : @PreAuthorize supports hasRole('ADMIN') and #user.status == 1; @Secured not supported

Recommended Use : @PreAuthorize for most scenarios; @Secured for simple role checks

6.3 Permission Expressions

Spring Security provided expressions: hasRole(role): Check role. Example:

hasRole('ADMIN')
hasAnyRole(roles...)

: Check any role. Example:

hasAnyRole('ADMIN', 'MANAGER')
hasAuthority(authority)

: Check permission. Example:

hasAuthority('user:add')
hasAnyAuthority(authorities...)

: Check any permission. Example:

hasAnyAuthority('user:add', 'user:update')
isAuthenticated()

: Authenticated. Example:

isAuthenticated()
isFullyAuthenticated()

: Non-Remember-Me login. Example:

isFullyAuthenticated()
isAnonymous()

: Anonymous user. Example:

isAnonymous()
permitAll

: Allow all. Example:

permitAll
denyAll

: Deny all. Example:

denyAll
principal

: Current user principal. Example:

#user.owner == principal.username
authentication

: Current authentication object. Example: authentication.name Complex expression examples:

// Data permission: only modify own data, or admin can modify all
@PreAuthorize("#order.userId == authentication.principal.userId or hasRole('ADMIN')")
public void updateOrder(Order order) { }

// Composite condition
@PreAuthorize("hasAuthority('order:cancel') and #order.status == 'PENDING'")
public void cancelOrder(@Param("order") Order order) { }

// Collection filtering
@PreAuthorize("hasRole('ADMIN')")
@PostFilter("filterObject.userId == authentication.principal.userId")
public List<Order> getAllOrders() { }

// Parameter filtering
@PreAuthorize("hasRole('ADMIN')")
@PreFilter("#userIds.{? this == authentication.principal.userId or hasRole('ADMIN')}")
public List<Order> getOrdersByIds(@Param("userIds") List<Long> userIds) { }

6.4 Custom Authorization Decision

Spring Security 6.x uses AuthorizationManager interface for authorization checks:

// Custom AuthorizationManager implementation
public class CustomAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {
    private final PermissionService permissionService;

    @Override
    public AuthorizationCheckResult verify(
            Supplier<Authentication> authentication,
            RequestAuthorizationContext context) {

        // Reject unauthenticated users
        if (authentication.get() == null || !authentication.get().isAuthenticated()) {
            return AuthorizationCheckResult.authorized(); // handled by earlier filters
        }

        String requestUrl = context.getRequest().getRequestURI();
        String httpMethod = context.getRequest().getMethod();
        SecurityUser user = (SecurityUser) authentication.get().getPrincipal();

        // Dynamic permissions: query DB for URL permissions
        List<String> requiredPermissions =
            permissionService.getPermissionsByUrl(requestUrl, httpMethod);

        boolean hasPermission = requiredPermissions.stream()
            .anyMatch(perm -> user.getAuthorities().stream()
                .anyMatch(a -> a.getAuthority().equals(perm)));

        return hasPermission
            ? AuthorizationCheckResult.authorized()
            : AuthorizationCheckResult.denied(
                new AccessDeniedException("No permission to access: " + requestUrl)
            );
    }
}

// Usage in SecurityFilterChain
http.authorizeHttpRequests(auth -> auth
    .requestMatchers("/api/dynamic/**").access(new CustomAuthorizationManager(permissionService))
    .anyRequest().authenticated()
);

7. Core Part 3: Session vs JWT Security Solutions

7.1 Session Authentication Solution

Traditional Session authentication:

Diagram:

Session authentication flow
Session authentication flow

Session configuration:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .sessionManagement(session -> session
            // Session creation policy
            .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
            // Session fixation protection
            .sessionFixation(SessionFixationProtectionPolicy.MigrateSession)
            // Max concurrent sessions
            .maximumSessions(1)
            .maxSessionsPreventsLogin(false) // false = new login kicks out old session
        )
        .rememberMe(remember -> remember
            .key("uniqueAndSecret")
            .tokenValiditySeconds(86400 * 7) // 7 days
            .userDetailsService(userDetailsService)
        );
    return http.build();
}

Session creation policies:

ALWAYS : Always create Session. Use case: Traditional web apps

IF_REQUIRED : Create when needed (default). Use case: Most scenarios

NEVER : Don't create, but use existing. Use case: Can provide Session but not force

STATELESS : Never create or use Session. Use case: Frontend-backend separation / JWT

Concurrent session control:

// Configure concurrent session control
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.sessionManagement(session -> session
        .maximumSessions(3) // max 3 concurrent sessions
        .expiredUrl("/session-expired") // redirect on expiry
        .maxSessionsPreventsLogin(true) // true = block new login when limit reached
    );
    return http.build();
}

// Need to register a Listener
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
    return new HttpSessionEventPublisher();
}

7.2 JWT Integration Solution

JWT (JSON Web Token) stateless authentication:

Diagram:

JWT authentication flow
JWT authentication flow

JWT utility class:

@Component
public class JwtTokenProvider {
    @Value("${jwt.secret}")
    private String secret;

    @Value("${jwt.expiration}")
    private long expiration; // 3600000 = 1 hour

    @Value("${jwt.refresh-expiration}")
    private long refreshExpiration; // 604800000 = 7 days

    private Key getSigningKey() {
        return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
    }

    /** Generate access token */
    public String generateToken(String username) {
        return Jwts.builder()
            .subject(username)
            .claim("type", "access")
            .issuedAt(new Date())
            .expiration(new Date(System.currentTimeMillis() + expiration))
            .signWith(getSigningKey(), SignatureAlgorithm.HS256)
            .compact();
    }

    /** Generate refresh token */
    public String generateRefreshToken(String username) {
        return Jwts.builder()
            .subject(username)
            .claim("type", "refresh")
            .issuedAt(new Date())
            .expiration(new Date(System.currentTimeMillis() + refreshExpiration))
            .signWith(getSigningKey(), SignatureAlgorithm.HS256)
            .compact();
    }

    /** Validate token */
    public boolean validateToken(String token) {
        try {
            Jwts.parser()
                .verifyWith((SecretKey) getSigningKey())
                .build()
                .parse(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }

    /** Get username from token */
    public String getUsername(String token) {
        return Jwts.parser()
            .verifyWith((SecretKey) getSigningKey())
            .build()
            .parseClaimsJws(token)
            .getBody()
            .getSubject();
    }

    /** Refresh token */
    public String refreshToken(String oldToken) {
        if (!validateToken(oldToken)) {
            throw new IllegalArgumentException("Invalid refresh token");
        }
        String username = getUsername(oldToken);
        return generateToken(username);
    }
}

Configuration file:

jwt:
  secret: "your-256-bit-secret-key-here-must-be-at-least-32-chars"
  expiration: 3600000 # 1 hour
  refresh-expiration: 604800000 # 7 days

Note: jjwt library 0.12.x has API changes; above uses new API. For old version (0.11.x):

// jjwt 0.11.x style (legacy)
return Jwts.builder()
    .setSubject(username)
    .setIssuedAt(new Date())
    .setExpiration(new Date(System.currentTimeMillis() + expiration))
    .signWith(SignatureAlgorithm.HS256, secret.getBytes(StandardCharsets.UTF_8))
    .compact();

7.3 OAuth2 Client Integration

Spring Security provides OAuth2/OIDC support:

@Configuration
@EnableWebSecurity
public class OAuth2LoginConfig {
    @Bean
    public SecurityFilterChain oauth2LoginFilterChain(HttpSecurity http) throws Exception {
        http
            .securityMatcher("/oauth2/**", "/login/**")
            .authorizeHttpRequests(auth -> auth
                .anyRequest().authenticated()
            )
            .oauth2Login(oauth2 -> oauth2
                .loginPage("/login")
                .defaultSuccessUrl("/home", true)
                .userInfoEndpoint(userInfo -> userInfo
                    .userService(customOAuth2UserService())
                )
                .successHandler(oAuth2SuccessHandler())
            );
        return http.build();
    }

    @Bean
    public OAuth2UserService<OAuth2UserRequest, OAuth2User> customOAuth2UserService() {
        return userRequest -> {
            OAuth2User oauth2User =
                new DefaultOAuth2UserService().loadUser(userRequest);

            String registrationId = userRequest.getClientRegistration()
                .getRegistrationId();
            Map<String, Object> attributes = oauth2User.getAttributes();

            // Handle user info per provider
            String username;
            String email;
            if ("github".equals(registrationId)) {
                username = (String) attributes.get("login");
                email = (String) attributes.get("email");
            } else if ("wechat".equals(registrationId)) {
                username = (String) attributes.get("nickname");
                email = null;
            } else {
                username = oauth2User.getName();
                email = oauth2User.getAttribute("email");
            }

            // Auto-register or link local account
            return buildOAuth2User(username, email, attributes);
        };
    }
}

Configuration file:

spring:
  security:
    oauth2:
      client:
        registration:
          github:
            client-id: ${GITHUB_CLIENT_ID}
            client-secret: ${GITHUB_CLIENT_SECRET}
            scope: read:user,user:email
          wechat:
            client-id: ${WECHAT_APP_ID}
            client-secret: ${WECHAT_APP_SECRET}
            scope: snsapi_login
            provider: wechat
        provider:
          wechat:
            authorization-uri: https://open.weixin.qq.com/connect/qrconnect
            token-uri: https://api.weixin.qq.com/sns/oauth2/access_token
            user-info-uri: https://api.weixin.qq.com/sns/userinfo
            user-name-attribute: openid

7.4 CSRF Protection Configuration

CSRF (Cross-Site Request Forgery) attack protection:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .csrf(csrf -> csrf
            // REST APIs usually disable CSRF
            .csrfTokenProvider(CsrfTokenRequestAttributeHandler::new)
            // Or Cookie-based (suitable for frontend-backend separation)
            .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            // Skip CSRF check for specific paths
            .ignoringRequestMatchers("/api/webhook/**")
        )
        .cors(Customizer.withDefaults());
    return http.build();
}

When to enable/disable CSRF:

Traditional Web (form submit) : Enable. Reason: Browser auto-sends cookies, CSRF risk

Frontend-backend separation (JWT) : Disable. Reason: Token in Header/LocalStorage, cookies not sensitive

Frontend-backend separation (Cookie + Session) : Enable + Cookie CSRF Token. Reason: Browser auto-sends cookies, need CSRF token validation

Pure API (mobile calls) : Disable. Reason: Mobile doesn't use cookies

Webhook callbacks : Disable + other signature verification. Reason: Third-party services cannot provide CSRF token

To be continued...

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

authenticationCSRFjwtoauth2authorizationRBACSessionSpring Security
CodeSmart Hoops
Written by

CodeSmart Hoops

A working programmer who loves coding and basketball. By day I debug code; by night I dissect tactics. I write articles to document my journey, focusing on Java, AI, Python and other programming topics, with occasional posts about basketball, English, and books. Hope it's helpful—thanks for following and support.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.