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.
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:
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:createTypical 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-cache4. 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:
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
AuthenticationManageris the authentication entry point; actual implementation is ProviderManager maintaining a list of AuthenticationProvider.
Diagram:
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); Authenticationinterface 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
UserDetailsServiceloads 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) { } @PreAuthorizevs @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 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 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 daysNote: 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: openid7.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...
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
