Deep Dive into JWT with Spring Security OAuth2: Token Enhancement Techniques

This tutorial explains the JWT structure, shows how to add custom claims such as user ID, department and roles, implements token blacklisting for logout, handles refresh token logic, and provides step‑by‑step code and testing instructions for a Spring Security OAuth2 authentication system.

Coder Trainee
Coder Trainee
Coder Trainee
Deep Dive into JWT with Spring Security OAuth2: Token Enhancement Techniques

A JSON Web Token (JWT) consists of three Base64URL‑encoded parts: Header (algorithm and type), Payload (standard claims such as sub, iss, exp, iat and any custom claims), and Signature (generated with the server’s private key to ensure integrity).

Custom Token Enhancer

package com.teaching.auth.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;

import java.time.Instant;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class TokenCustomizer {
    @Bean
    public OAuth2TokenCustomizer<JwtEncodingContext> tokenCustomizer() {
        return context -> {
            var principal = context.getPrincipal();
            var authorities = principal.getAuthorities();
            var username = principal.getName();

            Map<String, Object> claims = new HashMap<>();
            claims.put("user_id", getUserIdByUsername(username));
            claims.put("department", getDepartmentByUsername(username));
            claims.put("roles", authorities.stream()
                    .map(auth -> auth.getAuthority().replace("ROLE_", ""))
                    .toList());
            claims.put("login_time", Instant.now().toEpochMilli());
            claims.put("version", 1);
            context.getClaims().claims(existing -> existing.putAll(claims));
        };
    }

    private Long getUserIdByUsername(String username) {
        return switch (username) {
            case "user" -> 1001L;
            case "admin" -> 1002L;
            default -> 0L;
        };
    }

    private String getDepartmentByUsername(String username) {
        return switch (username) {
            case "user" -> "技术部";
            case "admin" -> "运维部";
            default -> "未知";
        };
    }
}

The customizer injects user_id, department, roles, login_time and a version field into the JWT payload.

Enhanced Authorization Server Configuration

package com.teaching.auth.config;

import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
import org.springframework.security.web.SecurityFilterChain;

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.time.Duration;
import java.util.UUID;

@Configuration
public class AuthorizationServerConfig {

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
                .oidc(Customizer.withDefaults());
        http.oauth2ResourceServer(resource -> resource.jwt(Customizer.withDefaults()));
        return http.build();
    }

    @Bean
    public RegisteredClientRepository registeredClientRepository() {
        RegisteredClient publicClient = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId("teaching-client")
                .clientSecret("{noop}teaching-secret")
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                .authorizationGrantType(AuthorizationGrantType.PASSWORD)
                .redirectUri("http://localhost:8081/login/oauth2/code/teaching")
                .redirectUri("http://localhost:8080/authorized")
                .scope(OidcScopes.OPENID)
                .scope(OidcScopes.PROFILE)
                .scope("message.read")
                .scope("message.write")
                .tokenSettings(TokenSettings.builder()
                        .accessTokenTimeToLive(Duration.ofMinutes(30))
                        .refreshTokenTimeToLive(Duration.ofDays(7))
                        .reuseRefreshTokens(true)
                        .build())
                .clientSettings(ClientSettings.builder()
                        .requireAuthorizationConsent(true)
                        .build())
                .build();
        return new InMemoryRegisteredClientRepository(publicClient);
    }

    @Bean
    public KeyPair keyPair() {
        try {
            KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
            generator.initialize(2048);
            return generator.generateKeyPair();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Bean
    public RSAKey rsaKey(KeyPair keyPair) {
        return new RSAKey.Builder((RSAPublicKey) keyPair.getPublic())
                .privateKey((RSAPrivateKey) keyPair.getPrivate())
                .keyID(UUID.randomUUID().toString())
                .build();
    }

    @Bean
    public JWKSource<SecurityContext> jwkSource(RSAKey rsaKey) {
        return new ImmutableJWKSet<>(new JWKSet(rsaKey));
    }

    @Bean
    public JwtDecoder jwtDecoder(KeyPair keyPair) {
        return NimbusJwtDecoder.withPublicKey((RSAPublicKey) keyPair.getPublic()).build();
    }

    @Bean
    public AuthorizationServerSettings authorizationServerSettings() {
        return AuthorizationServerSettings.builder()
                .issuer("http://localhost:8080")
                .build();
    }
}

Key changes: access‑token TTL set to 30 minutes, refresh‑token TTL set to 7 days, and refresh tokens are reusable.

Token Blacklist Service (Logout)

package com.teaching.auth.service;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.time.Duration;

@Service
public class TokenBlacklistService {
    private final RedisTemplate<String, String> redisTemplate;
    private static final String BLACKLIST_PREFIX = "blacklist:";

    public TokenBlacklistService(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public void blacklist(String token, long ttlSeconds) {
        String key = BLACKLIST_PREFIX + token;
        redisTemplate.opsForValue().set(key, "blacklisted", Duration.ofSeconds(ttlSeconds));
    }

    public boolean isBlacklisted(String token) {
        String key = BLACKLIST_PREFIX + token;
        return Boolean.TRUE.equals(redisTemplate.hasKey(key));
    }
}

Logout Controller

package com.teaching.auth.controller;

import com.teaching.auth.service.TokenBlacklistService;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.Instant;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/api/auth")
public class LogoutController {
    private final TokenBlacklistService blacklistService;
    private final JwtDecoder jwtDecoder;

    public LogoutController(TokenBlacklistService blacklistService, JwtDecoder jwtDecoder) {
        this.blacklistService = blacklistService;
        this.jwtDecoder = jwtDecoder;
    }

    @PostMapping("/logout")
    public Map<String, Object> logout(@RequestHeader("Authorization") String authorization) {
        String token = authorization.substring(7);
        Jwt jwt = jwtDecoder.decode(token);
        long ttl = jwt.getExpiresAt().getEpochSecond() - Instant.now().getEpochSecond();
        blacklistService.blacklist(token, ttl);
        Map<String, Object> result = new HashMap<>();
        result.put("code", 200);
        result.put("message", "登出成功");
        return result;
    }
}

Refresh Token Controller

package com.teaching.auth.controller;

import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/api/auth")
public class RefreshController {
    private final OAuth2AuthorizationService authorizationService;
    private final RegisteredClientRepository clientRepository;

    public RefreshController(OAuth2AuthorizationService authorizationService,
                             RegisteredClientRepository clientRepository) {
        this.authorizationService = authorizationService;
        this.clientRepository = clientRepository;
    }

    @PostMapping("/refresh")
    public Map<String, Object> refreshToken(@RequestParam("refresh_token") String refreshToken) {
        OAuth2Authorization authorization = authorizationService.findByToken(
                refreshToken, OAuth2TokenType.REFRESH_TOKEN);
        Map<String, Object> result = new HashMap<>();
        if (authorization == null) {
            result.put("code", 401);
            result.put("message", "Refresh Token 无效");
            return result;
        }
        if (authorization.getRefreshToken() == null || authorization.getRefreshToken().isExpired()) {
            result.put("code", 401);
            result.put("message", "Refresh Token 已过期");
            return result;
        }
        result.put("code", 200);
        result.put("message", "请使用 OAuth2 标准端点刷新 Token");
        result.put("endpoint", "POST /oauth2/token?grant_type=refresh_token");
        return result;
    }
}

Resource Server – Extracting Enhanced Claims

package com.teaching.resource.controller;

import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/api/user")
public class UserInfoController {
    @GetMapping("/info")
    public Map<String, Object> getUserInfo(Authentication authentication) {
        Jwt jwt = (Jwt) authentication.getPrincipal();
        Map<String, Object> result = new HashMap<>();
        result.put("username", jwt.getSubject());
        result.put("user_id", jwt.getClaim("user_id"));
        result.put("department", jwt.getClaim("department"));
        result.put("roles", jwt.getClaim("roles"));
        result.put("login_time", jwt.getClaim("login_time"));
        result.put("scope", jwt.getClaim("scope"));
        result.put("exp", jwt.getExpiresAt());
        return result;
    }
}

Testing the Implementation

Start the authorization server and the resource server with mvn spring-boot:run.

Obtain an enhanced token via a password‑grant request:

curl -X POST http://localhost:8080/oauth2/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=password" \
  -d "client_id=teaching-client" \
  -d "client_secret=teaching-secret" \
  -d "username=user" \
  -d "password=123456" \
  -d "scope=message.read"

Decode the JWT (e.g., with jwt.io or PowerShell) and verify that the custom fields user_id, department, roles, login_time are present.

# PowerShell example
$token = "YOUR_TOKEN"
$token.Split('.') [1] | % { [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($_)) }

Call the protected resource to retrieve user information:

curl http://localhost:8081/api/user/info -H "Authorization: Bearer $TOKEN"

Common Issues and Solutions

JWT size too large – keep only essential data in the token; fetch additional details via APIs.

Token cannot be revoked – use a Redis blacklist to record tokens on logout.

Refresh token theft – adopt refresh‑token rotation (issue a new token on each refresh) and store refresh tokens in a database to enable revocation.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

RedisSpring BootjwtOAuth2Spring SecurityRefresh TokenToken Enhancement
Coder Trainee
Written by

Coder Trainee

Experienced in Java and Python, we share and learn together. For submissions or collaborations, DM us.

0 followers
Reader feedback

How this landed with the community

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.