Eliminate OAuth2 Check‑Token Bottleneck with JWT and Custom Token Services
This article explains how the default OAuth2 check‑token flow creates a performance bottleneck, then shows how to extend JWT tokens with user details via a custom TokenEnhancer and replace RemoteTokenServices with a custom ResourceServerTokenServices, including configuration, code examples, and the security trade‑offs of using JWT.
OAuth2 Performance Bottleneck
When a user sends a request with a token, the resource server interceptor forwards the token to the authentication server for validation. The token initially contains only the username, so the resource server must call
userDetailsService.loadByUsernameto retrieve the full user information. This extra call puts heavy load on the authentication center and becomes a key system bottleneck.
Check‑Token Source Code
For a detailed source analysis, refer to the previous article "Spring Cloud OAuth2 Resource Server CheckToken Source Code Analysis". The core classes involved in the check‑token process are illustrated below.
Extend JWT to Carry Detailed User Information
Why replace the default UUID token with JWT? Using JWT eliminates the check‑token step; after parsing the JWT, authentication and login information are obtained directly, reducing network overhead and improving overall micro‑service cluster performance.
The default JWT token generated by Spring Security OAuth only contains the username. By extending
TokenEnhancer, additional fields can be injected into the JWT for easier use by the resource server.
<code>@Bean
public TokenEnhancer tokenEnhancer() {
return (accessToken, authentication) -> {
if (SecurityConstants.CLIENT_CREDENTIALS.equals(authentication.getOAuth2Request().getGrantType())) {
return accessToken;
}
Map<String, Object> additionalInfo = new HashMap<>(8);
PigxUser pigxUser = (PigxUser) authentication.getUserAuthentication().getPrincipal();
additionalInfo.put("user_id", pigxUser.getId());
additionalInfo.put("username", pigxUser.getUsername());
additionalInfo.put("dept_id", pigxUser.getDeptId());
additionalInfo.put("tenant_id", pigxUser.getTenantId());
additionalInfo.put("license", SecurityConstants.PIGX_LICENSE);
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
return accessToken;
};
}
</code>The generated token now includes the key fields.
Rewrite Default Resource Server Handling
Stop using
RemoteTokenServicesand remove the authentication center's check‑token by implementing a custom client
TokenService.
<code>@Slf4j
public class PigxCustomTokenServices implements ResourceServerTokenServices {
@Setter
private TokenStore tokenStore;
@Setter
private DefaultAccessTokenConverter defaultAccessTokenConverter;
@Setter
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Override
public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException {
OAuth2Authentication oAuth2Authentication = tokenStore.readAuthentication(accessToken);
UserAuthenticationConverter userTokenConverter = new PigxUserAuthenticationConverter();
defaultAccessTokenConverter.setUserTokenConverter(userTokenConverter);
Map<String, ?> map = jwtAccessTokenConverter.convertAccessToken(readAccessToken(accessToken), oAuth2Authentication);
return defaultAccessTokenConverter.extractAuthentication(map);
}
@Override
public OAuth2AccessToken readAccessToken(String accessToken) {
return tokenStore.readAccessToken(accessToken);
}
}
</code>JWT Authentication Converter
<code>public class PigxUserAuthenticationConverter implements UserAuthenticationConverter {
private static final String USER_ID = "user_id";
private static final String DEPT_ID = "dept_id";
private static final String TENANT_ID = "tenant_id";
private static final String N_A = "N/A";
@Override
public Authentication extractAuthentication(Map<String, ?> map) {
if (map.containsKey(USERNAME)) {
Collection<? extends GrantedAuthority> authorities = getAuthorities(map);
String username = (String) map.get(USERNAME);
Integer id = (Integer) map.get(USER_ID);
Integer deptId = (Integer) map.get(DEPT_ID);
Integer tenantId = (Integer) map.get(TENANT_ID);
PigxUser user = new PigxUser(id, deptId, tenantId, username, N_A, true);
return new UsernamePasswordAuthenticationToken(user, N_A, authorities);
}
return null;
}
private Collection<? extends GrantedAuthority> getAuthorities(Map<String, ?> map) {
Object authorities = map.get(AUTHORITIES);
if (authorities instanceof String) {
return AuthorityUtils.commaSeparatedStringToAuthorityList((String) authorities);
}
if (authorities instanceof Collection) {
return AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils.collectionToCommaDelimitedString((Collection<?>) authorities));
}
throw new IllegalArgumentException("Authorities must be either a String or a Collection");
}
}
</code>Resource Server Configuration
<code>@Slf4j
public class PigxResourceServerConfigurerAdapter extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
DefaultAccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter();
UserAuthenticationConverter userTokenConverter = new PigxUserAuthenticationConverter();
accessTokenConverter.setUserTokenConverter(userTokenConverter);
PigxCustomTokenServices tokenServices = new PigxCustomTokenServices();
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("123");
converter.setVerifier(new MacSigner("123"));
JwtTokenStore jwtTokenStore = new JwtTokenStore(converter);
tokenServices.setTokenStore(jwtTokenStore);
tokenServices.setJwtAccessTokenConverter(converter);
tokenServices.setDefaultAccessTokenConverter(accessTokenConverter);
resources.authenticationEntryPoint(resourceAuthExceptionEntryPoint)
.tokenServices(tokenServices);
}
}
</code>Issues Introduced by Extending JWT
Because the server does not store session state, a JWT cannot be revoked or have its permissions changed before expiration; it remains valid for its entire lifetime unless additional logic is added.
Removing the check‑token step means JWT security relies solely on token integrity; without server‑side verification, token tampering cannot be detected.
If a JWT is leaked, anyone can use it with all embedded permissions. Therefore, JWTs should have short expiration times and critical operations should re‑authenticate the user.
JWTs must be transmitted over HTTPS to prevent interception.
Java Architecture Diary
Committed to sharing original, high‑quality technical articles; no fluff or promotional content.
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.