리소스 서버 JWT 검증과 클레임
JWT 액세스 토큰을 검증하고 발급자와 대상을 확인하며 클레임에서 권한을 추출합니다.
리소스 서버 JWT 검증과 클레임은(는) CoddyKit의 무료 Spring Boot 4 Complete Guide 강의입니다. 이것은 4개 중 1번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Spring Boot 4 Complete Guide 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Spring Boot 4 Complete Guide 강의에는 총 4개의 강의가 포함되어 있습니다.
이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.
The Resource Server Role
In OAuth2, a resource server is the API that holds protected data. It does not log users in or issue tokens. Its only job at request time is to validate the access token that an authorization server already issued, then authorize the call.
- Tokens are usually JWTs (JSON Web Tokens) signed by the authorization server.
- Validation is stateless: the resource server verifies the signature and claims without calling a database.
- Spring Security ships a dedicated
oauth2ResourceServerDSL for exactly this.
In this lesson you will validate JWTs, verify the iss and aud claims, and turn claims into Spring authorities.
Anatomy of a JWT
A JWT has three Base64URL parts separated by dots: header.payload.signature. The payload carries claims the resource server inspects:
iss— issuer, the URL of the authorization server.sub— subject, the user or client id.aud— audience, who the token is meant for.exp/nbf/iat— expiry, not-before, issued-at timestamps.scopeorscp— granted OAuth2 scopes.
The example below shows how a decoded payload looks as plain JSON. Validating means: signature is genuine AND these claims are acceptable.
// A decoded JWT payload (claims) as JSON
{
"iss": "https://issuer.example.com",
"sub": "user-1234",
"aud": ["orders-api"],
"scope": "orders.read orders.write",
"roles": ["ADMIN", "USER"],
"iat": 1735689600,
"nbf": 1735689600,
"exp": 1735693200
}Minimal Resource Server Config
With spring-boot-starter-oauth2-resource-server on the classpath, you enable JWT validation through the security DSL. The jwt() configurer wires up signature verification and standard timestamp checks automatically.
Point Spring at the authorization server's metadata via the issuer URI and it will discover the JWK Set endpoint (the public keys) on its own.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health").permitAll()
.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(Customizer.withDefaults()));
return http.build();
}
}Configuring the Issuer URI
The cleanest way to set things up is the issuer-uri property. On startup Spring fetches {issuer}/.well-known/openid-configuration (or the OAuth2 equivalent), reads the jwks_uri, and builds a JwtDecoder that caches and rotates signing keys.
It also installs an issuer validator: every token's iss claim must equal this value, or the token is rejected.
# application.yml
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://issuer.example.com
# Optional: pin the audience(s) accepted by this API
audiences:
- orders-apiHow the JwtDecoder Validates
When issuer-uri is set, Spring builds a NimbusJwtDecoder backed by the discovered JWK Set. Each incoming token goes through:
- Signature check — the token's
kidselects a public key from the JWK Set; the signature must verify. - Timestamp check —
expmust be in the future,nbfin the past (with small clock skew). - Issuer check —
issmust match the configured issuer.
You can build the same decoder manually when you need to attach extra validators.
@Bean
JwtDecoder jwtDecoder() {
String issuer = "https://issuer.example.com";
NimbusJwtDecoder decoder =
JwtDecoders.fromIssuerLocation(issuer);
OAuth2TokenValidator<Jwt> withIssuer =
JwtValidators.createDefaultWithIssuer(issuer);
decoder.setJwtValidator(withIssuer);
return decoder;
}Verifying the Audience Claim
The default validators check timestamps and issuer, but not the audience. Skipping the aud check is a real risk: a token minted for another API on the same issuer would otherwise be accepted here. This is the classic token confusion attack.
Write a custom OAuth2TokenValidator<Jwt> that asserts your API's identifier is present in the aud list, and chain it with the defaults.
public class AudienceValidator
implements OAuth2TokenValidator<Jwt> {
private final String audience;
public AudienceValidator(String audience) {
this.audience = audience;
}
@Override
public OAuth2TokenValidatorResult validate(Jwt jwt) {
if (jwt.getAudience().contains(audience)) {
return OAuth2TokenValidatorResult.success();
}
OAuth2Error error = new OAuth2Error(
OAuth2ErrorCodes.INVALID_TOKEN,
"Required audience is missing", null);
return OAuth2TokenValidatorResult.failure(error);
}
}Chaining Validators in the Decoder
Combine the audience validator with the default-with-issuer chain using DelegatingOAuth2TokenValidator. Order does not matter for correctness — all validators must pass — but keep signature and timestamp checks (handled internally) plus issuer and audience together.
This decoder bean overrides the auto-configured one while still reusing the discovered JWK keys.
@Bean
JwtDecoder jwtDecoder(
@Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}") String issuer,
@Value("${api.audience}") String audience) {
NimbusJwtDecoder decoder =
JwtDecoders.fromIssuerLocation(issuer);
OAuth2TokenValidator<Jwt> validator =
new DelegatingOAuth2TokenValidator<>(
JwtValidators.createDefaultWithIssuer(issuer),
new AudienceValidator(audience));
decoder.setJwtValidator(validator);
return decoder;
}From Scopes to Authorities
By default Spring maps the scope (or scp) claim to authorities, prefixing each with SCOPE_. So scope: "orders.read" becomes the authority SCOPE_orders.read, which you can require in the DSL or with method security.
hasAuthority("SCOPE_orders.read")inauthorizeHttpRequests.@PreAuthorize("hasAuthority('SCOPE_orders.write')")on a method.
This default works out of the box, no converter needed.
http.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.GET, "/orders/**")
.hasAuthority("SCOPE_orders.read")
.requestMatchers(HttpMethod.POST, "/orders/**")
.hasAuthority("SCOPE_orders.write")
.anyRequest().authenticated());Extracting Roles from a Custom Claim
Many identity providers (Keycloak, Auth0, Entra ID) put roles in a non-standard claim such as roles or realm_access.roles, not in scope. To map these, supply a JwtAuthenticationConverter with a custom JwtGrantedAuthoritiesConverter-like function.
Here we read a top-level roles array and emit ROLE_-prefixed authorities so hasRole(...) works.
@Bean
JwtAuthenticationConverter jwtAuthConverter() {
JwtAuthenticationConverter converter =
new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(jwt -> {
List<String> roles =
jwt.getClaimAsStringList("roles");
if (roles == null) return List.of();
return roles.stream()
.map(r -> new SimpleGrantedAuthority("ROLE_" + r))
.collect(Collectors.toList());
});
return converter;
}Wiring the Converter and Reading Claims
Register the converter on the resource server DSL so it is used to build the Authentication. Once wired, your controllers can inject the validated Jwt and read any claim directly — it is already verified by the time the handler runs.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain chain(HttpSecurity http,
JwtAuthenticationConverter converter) throws Exception {
http.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.jwtAuthenticationConverter(converter)));
return http.build();
}
}
@RestController
class MeController {
@GetMapping("/me")
Map<String, Object> me(@AuthenticationPrincipal Jwt jwt) {
return Map.of(
"sub", jwt.getSubject(),
"issuer", jwt.getIssuer().toString(),
"roles", jwt.getClaimAsStringList("roles"));
}
}Building Authorities in Plain Java
The role-mapping logic is just data transformation — the same logic the converter runs, minus the framework. The snippet below is a standalone program that demonstrates turning a claim value into ROLE_-prefixed and SCOPE_-prefixed authorities, exactly mirroring Spring's defaults.
import java.util.*;
import java.util.stream.*;
public class AuthorityMapping {
static List<String> fromRoles(List<String> roles) {
return roles.stream()
.map(r -> "ROLE_" + r)
.collect(Collectors.toList());
}
static List<String> fromScope(String scope) {
return Arrays.stream(scope.split(" "))
.filter(s -> !s.isBlank())
.map(s -> "SCOPE_" + s)
.collect(Collectors.toList());
}
public static void main(String[] args) {
List<String> authorities = new ArrayList<>();
authorities.addAll(fromRoles(List.of("ADMIN", "USER")));
authorities.addAll(fromScope("orders.read orders.write"));
System.out.println(authorities);
}
}Quick Check
Your resource server uses issuer-uri, so signature, expiry, and issuer are validated automatically. A pentester sends a valid, unexpired token that was issued by the same authorization server but minted for a different API. Your endpoint accepts it. What is the fix?
Recap
You configured a Spring Boot 4 resource server to validate JWT access tokens end to end:
- Issuer URI auto-discovers the JWK Set and installs signature, timestamp, and issuer validation.
- Audience must be checked explicitly with a custom
OAuth2TokenValidator<Jwt>to block token confusion — defaults do not do this. - Validators chain via
DelegatingOAuth2TokenValidatorso issuer + audience + defaults all apply. - Authorities come from the
scopeclaim asSCOPE_*by default; aJwtAuthenticationConvertermaps custom claims likerolestoROLE_*. - Controllers read verified claims via
@AuthenticationPrincipal Jwt.
Validate the signature, then never trust a claim you have not explicitly verified.
자주 묻는 질문
“리소스 서버 JWT 검증과 클레임” 강의는 무료인가요?
네 — “리소스 서버 JWT 검증과 클레임” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Spring Boot 4 Complete Guide 강의 전체를 잠금 해제할 수 있습니다. Spring Boot 4 Complete Guide 강의에는 총 4개의 강의가 포함되어 있습니다.
“리소스 서버 JWT 검증과 클레임”에서 뭘 배우나요?
JWT 액세스 토큰을 검증하고 발급자와 대상을 확인하며 클레임에서 권한을 추출합니다. 브라우저에서 직접 실행하는 실습 코드로 Spring Boot 4 Complete Guide을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
Spring Boot 4 Complete Guide을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 Spring Boot 4 Complete Guide은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 1번째 강의입니다.
“리소스 서버 JWT 검증과 클레임” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 Spring Boot 4 Complete Guide 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 Spring Boot 4 Complete Guide 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- 리소스 서버 JWT 검증과 클레임
- OAuth2 클라이언트와 인증 코드 흐름
- SpEL과 사용자 지정 투표기를 활용한 메서드 보안
- 불투명 토큰 검사 및 토큰 교환