如何基于Spring Security OAuth2资源服务器自定义令牌校验与认证逻辑?
Let’s break down how to replace your interceptor-based resource protection with Spring Security’s OAuth2 Resource Server support, while preserving all your original logic. I’ll walk through each component and how to map your requirements to Spring Security’s built-in tools.
1. Dependency Setup
First, add the necessary Spring Security dependencies to your project (Maven example):
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
2. Core Security Configuration
This is the main setup that ties all components together. We’ll configure the resource server to use opaque tokens (since you need to call your auth server for validation), handle redirects, and integrate custom filters.
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.oauth2.server.resource.introspection.NimbusOpaqueTokenIntrospector; import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.web.client.RestTemplate; @Configuration @EnableWebSecurity public class ResourceServerSecurityConfig { private final String AUTH_SERVER_INTROSPECTION_URL = "https://your-auth-server.com/api/token/validate"; private final String RESOURCE_SERVER_CLIENT_ID = "your-resource-client-id"; private final String RESOURCE_SERVER_CLIENT_SECRET = "your-resource-client-secret"; private final UserInfoService userInfoService; public ResourceServerSecurityConfig(UserInfoService userInfoService) { this.userInfoService = userInfoService; } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth.anyRequest().authenticated()) .oauth2ResourceServer(oauth2 -> oauth2 .opaqueToken(opaqueToken -> opaqueToken .bearerTokenResolver(new CookieBearerTokenResolver()) .introspectionUri(AUTH_SERVER_INTROSPECTION_URL) .introspectionClientCredentials(RESOURCE_SERVER_CLIENT_ID, RESOURCE_SERVER_CLIENT_SECRET) .introspector(customOpaqueTokenIntrospector()) ) ) .exceptionHandling(exceptions -> exceptions .authenticationEntryPoint(new CustomAuthenticationEntryPoint()) ) .sessionManagement(session -> session .sessionFixation().migrateSession() .maximumSessions(1) ) .csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())) .addFilterAfter(sessionGuidValidationFilter(), OAuth2AuthenticationProcessingFilter.class) .addFilterAfter(customAuthenticationSuccessHandler(), SessionGuidValidationFilter.class); return http.build(); } @Bean public OpaqueTokenIntrospector customOpaqueTokenIntrospector() { NimbusOpaqueTokenIntrospector delegate = new NimbusOpaqueTokenIntrospector( AUTH_SERVER_INTROSPECTION_URL, RESOURCE_SERVER_CLIENT_ID, RESOURCE_SERVER_CLIENT_SECRET ); return new CustomOpaqueTokenIntrospector(delegate); } @Bean public SessionGuidValidationFilter sessionGuidValidationFilter() { return new SessionGuidValidationFilter(); } @Bean public AuthenticationSuccessHandler customAuthenticationSuccessHandler() { return new CustomAuthenticationSuccessHandler(userInfoService); } @Bean public RestTemplate restTemplate() { return new RestTemplate(); } }
3. Custom Token Resolver (Extract from Cookie)
Your original logic uses a cookie to store the access token. This resolver tells Spring Security to look for the token in the cookie instead of the default Authorization header.
import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver; public class CookieBearerTokenResolver implements BearerTokenResolver { private String cookieName = "access_token"; @Override public String resolve(HttpServletRequest request) { Cookie[] cookies = request.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { if (cookieName.equals(cookie.getName())) { return cookie.getValue(); } } } return null; } public void setCookieName(String cookieName) { this.cookieName = cookieName; } }
4. Custom Token Introspector
This component validates the token against your auth server’s API and ensures the user’s uniqueId (GUID) is present in the response.
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal; import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; public class CustomOpaqueTokenIntrospector implements OpaqueTokenIntrospector { private final OpaqueTokenIntrospector delegate; public CustomOpaqueTokenIntrospector(OpaqueTokenIntrospector delegate) { this.delegate = delegate; } @Override public OAuth2AuthenticatedPrincipal introspect(String token) { OAuth2AuthenticatedPrincipal principal = delegate.introspect(token); String uniqueId = principal.getAttribute("uniqueId"); if (uniqueId == null) { throw new IllegalArgumentException("Token missing required uniqueId attribute"); } return principal; } }
5. Authentication Entry Point (Redirect to Auth Server Login)
When no valid token is found, this redirects users to your auth server’s login page with the required clientId and returnURL parameters.
import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.web.util.UriComponentsBuilder; import java.io.IOException; public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { private final String AUTH_SERVER_LOGIN_URL = "https://your-auth-server.com/login"; private final String CLIENT_ID = "your-resource-client-id"; @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { String returnUrl = UriComponentsBuilder.fromRequest(request).build().toUriString(); String redirectUrl = UriComponentsBuilder.fromUriString(AUTH_SERVER_LOGIN_URL) .queryParam("clientId", CLIENT_ID) .queryParam("returnURL", returnUrl) .build().toUriString(); response.sendRedirect(redirectUrl); } }
6. Session GUID Validation Filter
This filter checks if the session’s stored uniqueId matches the token’s uniqueId. If there’s a mismatch, it redirects to the login page.
import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.server.resource.authentication.OAuth2AuthenticationToken; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; public class SessionGuidValidationFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication instanceof OAuth2AuthenticationToken) { OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication; String tokenUniqueId = oauthToken.getPrincipal().getAttribute("uniqueId"); HttpSession session = request.getSession(false); if (session != null) { String sessionUniqueId = (String) session.getAttribute("userUniqueId"); if (sessionUniqueId != null && !sessionUniqueId.equals(tokenUniqueId)) { new CustomAuthenticationEntryPoint().commence(request, response, null); return; } } // Update session with current GUID if missing if (session == null) { session = request.getSession(true); } session.setAttribute("userUniqueId", tokenUniqueId); } filterChain.doFilter(request, response); } }
7. User Info Service with Caching
This service fetches user info from your auth server’s API and caches it to avoid repeated calls, matching your original logic.
First, create a model for user info:
public class UserInfo { private String uniqueId; private String username; private String email; // Add other fields, getters, and setters }
Then the service:
import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; @Service public class UserInfoService { private final RestTemplate restTemplate; private final CacheManager cacheManager; private final String AUTH_SERVER_USER_INFO_URL = "https://your-auth-server.com/api/userinfo"; public UserInfoService(RestTemplate restTemplate, CacheManager cacheManager) { this.restTemplate = restTemplate; this.cacheManager = cacheManager; } public UserInfo getUserInfo(String accessToken) { Cache cache = cacheManager.getCache("userInfoCache"); if (cache != null) { UserInfo cachedUserInfo = cache.get(accessToken, UserInfo.class); if (cachedUserInfo != null) { return cachedUserInfo; } } HttpHeaders headers = new HttpHeaders(); headers.setBearerAuth(accessToken); HttpEntity<Void> requestEntity = new HttpEntity<>(headers); ResponseEntity<UserInfo> response = restTemplate.exchange( AUTH_SERVER_USER_INFO_URL, HttpMethod.GET, requestEntity, UserInfo.class ); UserInfo userInfo = response.getBody(); if (cache != null && userInfo != null) { cache.put(accessToken, userInfo); } return userInfo; } }
8. Authentication Success Handler
After successful token validation, this handler fetches user info (if not cached) and stores it in the session.
import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.server.resource.authentication.OAuth2AuthenticationToken; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.stereotype.Component; import java.io.IOException; @Component public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler { private final UserInfoService userInfoService; public CustomAuthenticationSuccessHandler(UserInfoService userInfoService) { this.userInfoService = userInfoService; } @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { if (authentication instanceof OAuth2AuthenticationToken) { OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication; String accessToken = oauthToken.getToken().getTokenValue(); UserInfo userInfo = userInfoService.getUserInfo(accessToken); HttpSession session = request.getSession(); session.setAttribute("userInfo", userInfo); } response.sendRedirect(request.getRequestURI()); } }
Key Notes
- Cache Configuration: Add
@EnableCachingto your main application class and configure a cache provider (like Caffeine or Redis) to enable user info caching. - Auth Server Integration: Ensure your auth server sets the
access_tokencookie when redirecting back to the resource server, and that the introspection/user info APIs return theuniqueIdattribute. - Client Credentials: The resource server needs valid credentials to call your auth server’s introspection endpoint—keep these secure (use environment variables or a secrets manager).
Content of this question originates from stack exchange, asked by Ishank Gupta

