Information Security 7 min read

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.

Java Architecture Diary
Java Architecture Diary
Java Architecture Diary
Eliminate OAuth2 Check‑Token Bottleneck with JWT and Custom Token Services

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.loadByUsername

to 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

RemoteTokenServices

and 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.

securitySpring CloudJWTOAuth2Resource ServerTokenEnhancer
Java Architecture Diary
Written by

Java Architecture Diary

Committed to sharing original, high‑quality technical articles; no fluff or promotional content.

0 followers
Reader feedback

How this landed with the community

login 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.