Spring OAuth2 SSO API中拦截器与业务层共享用户上下文问题
Hey Miguel, let's break down your Spring OAuth2 issues step by step—since you're working with a pure REST API (no login/logout pages) and JWT for SSO, most web-focused examples can lead you astray. Let's tackle each problem and fix your setup properly.
1. 适配纯API场景,摆脱页面流示例的误导
Most Spring OAuth2 examples are built for traditional web apps with login pages, but your pure API setup needs a stateless, token-only approach. Here's how to align your config:
- Disable session management entirely (we don't need sessions for JWT-based SSO)
- Use Spring Security's built-in
oauth2ResourceServersupport for JWT, instead of rolling your own full auth flow - Turn off CSRF protection (it's irrelevant for API-only services that don't use browser cookies)
Add this security filter chain config to your project:
@Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth.anyRequest().authenticated()) .oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> jwt .jwtAuthenticationConverter(customJwtConverter()) )); return http.build(); }
This tells Spring Security to handle JWT validation, parse the token, and populate the SecurityContext with a valid Authentication object automatically—no need to reinvent the wheel with raw token reading in your filter (yet).
2. Fixing the null Principal in SecurityContextHolder
The main reason your Authentication is null is filter order and missing resource server config:
- Your
AuthenticationFilteris set to@Order(Ordered.LOWEST_PRECEDENCE), which means it runs after most filters—including potentially before Spring Security's own JWT auth filter has had a chance to populate theSecurityContext. - Without the proper resource server config above, Spring Security isn't processing the JWT token at all, so no
Authenticationgets added to the context.
Fix steps:
- Add the security filter chain config I shared above first—this ensures Spring Security handles JWT auth and populates the
SecurityContext. - Adjust your filter's order to run after Spring Security's auth filters. Use
@Order(SecurityProperties.BASIC_AUTH_ORDER + 10)instead ofLOWEST_PRECEDENCE:@Component @Order(SecurityProperties.BASIC_AUTH_ORDER + 10) // Runs after core security filters public class AuthenticationFilter implements Filter { // ... your existing code ... }
Now when your filter runs, SecurityContextHolder.getContext().getAuthentication() will have a valid OAuth2Authentication (or JwtAuthenticationToken if you use the new JWT support) instead of null.
3. Passing User Instances to the Business Layer: Best Practices
You can use @Autowired in your filter, but it's not the cleanest pattern for passing user data to the business layer. Here's a better approach aligned with Spring Security conventions:
Option 1: Use @AuthenticationPrincipal (Recommended)
Instead of manually creating a User instance in your filter and passing it around, let Spring inject the authenticated user directly into your service methods. First, create a custom User class (or use UserDetails) and configure the JWT converter to use it as the principal:
// Custom User class (implement UserDetails if needed) public class AppUser { private String userId; private String username; // getters, setters, constructor } // Update the JWT converter in your security config private JwtAuthenticationConverter customJwtConverter() { JwtAuthenticationConverter converter = new JwtAuthenticationConverter(); converter.setPrincipalConverter(jwt -> { String userId = jwt.getClaimAsString("sub"); String username = jwt.getClaimAsString("preferred_username"); return new AppUser(userId, username); }); return converter; }
Then in your business layer, simply use @AuthenticationPrincipal to inject the user:
@Service public class UserService { public void processUserRequest(@AuthenticationPrincipal AppUser currentUser) { // Use currentUser directly—no need to pass from filters! System.out.println("Processing request for user: " + currentUser.getUsername()); } }
Option 2: If you still need to use the filter
If you must handle user data in the filter (like your MDC setup), you can modify the Authentication object's principal directly in the filter:
@Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth instanceof JwtAuthenticationToken jwtAuth) { // Extract claims and create your User instance AppUser user = new AppUser( jwtAuth.getClaimAsString("sub"), jwtAuth.getClaimAsString("preferred_username") ); // Create a new Authentication with your User as principal Authentication updatedAuth = new UsernamePasswordAuthenticationToken( user, jwtAuth.getCredentials(), jwtAuth.getAuthorities() ); // Update the SecurityContext with the new Authentication SecurityContextHolder.getContext().setAuthentication(updatedAuth); // Your MDC setup MDC.put("sub", user.getUserId()); MDC.put("preferred_username", user.getUsername()); } try { chain.doFilter(request, response); } finally { MDC.remove("sub"); MDC.remove("preferred_username"); } }
This way, the updated Authentication with your User principal is available throughout the request lifecycle, including in the business layer via SecurityContextHolder.
Quick Notes on Your Existing Filter
Your current code reads tokens from TokenStore, but if you're using JWT (a self-contained token), you don't need a TokenStore at all—JWTs can be validated directly using the public key, no need to store them. You can remove the TokenStore dependency if you're using stateless JWT authentication.
内容的提问来源于stack exchange,提问作者Miguel Costa

