API 서버가 JWT를 받는다는 말은, 그 서버가 반드시 로그인 화면과 회원가입까지 만든다는 뜻은 아니에요.지난 글에서는 Spring Security를 filter chain부터 봐야 한다고 했어요. 요청은 컨트롤러로 바로 들어오지 않고, 먼저 보안 filter를 지나면서 인증(authentication), 인가(authorization), CSRF, CORS 같은 경계를 만났죠. 오늘은 그중에서도 API에서 자주 만나는 장면을 볼 거예요.
“JWT를 쓰면 로그인 기능을 만든 거 아닌가요?”사실은 아니에요. JWT(JSON Web Token)는 토큰의 모양과 서명 검증 방식에 가까워요. OAuth2는 권한을 위임하고 access token을 발급하는 흐름이에요. 그리고 OAuth2 resource server는 이미 발급된 access token을 받아서 이 요청을 허용할지 판단하는 API 서버 역할이에요. 이 셋을 한 덩어리로 외우면 금방 헷갈려요.
- 로그인 화면은 누가 보여주나요?
- 비밀번호는 누가 검증하나요?
- access token은 누가 발급하나요?
- API 서버는 token 안에서 무엇을 믿어도 되나요?
- refresh token은 API 서버가 받아도 되나요?
- scope와 role은 Spring Security에서 어떤 권한으로 바뀌나요?
이 글은 Spring Boot 4.x의 OAuth2 설정 문서와 Spring Security 7.x의 Servlet OAuth2 Resource Server JWT 문서를 기준으로 작성했어요. Spring Boot 3.x 프로젝트에서도 issuer, JWK Set, scope,
SecurityFilterChain이라는 핵심 모델은 비슷하게 읽을 수 있지만, starter 이름과 세부 dependency는 프로젝트의 Boot 버전 문서를 확인하세요.먼저 역할을 나눠야 덜 헷갈려요
쇼핑몰 API를 생각해볼게요. 사용자는 브라우저나 모바일 앱에서 로그인하고, 주문 목록을 보려고 해요. 겉으로는 한 흐름처럼 보여요.- 사용자가 로그인해요.
- access token을 받아요.
- API를 호출해요.
- 서버가 token을 보고 주문 정보를 내려줘요.
이 그림에서 API 서버는 사용자의 비밀번호를 직접 확인하지 않아요. API 서버는 Authorization header에 담긴 bearer token을 보고, “이 token이 믿을 수 있는 발급자에게서 왔고, 아직 유효하고, 이 API를 호출할 권한이 있나?”를 확인해요.
그래서 resource server 글에서 가장 먼저 버려야 할 생각은 이거예요.
“JWT를 받으니까 로그인 서버까지 만든다.”Resource server의 기본 책임은 로그인 화면이 아니라 검증과 인가예요.
JWT는 서버가 읽을 수 있는 서명된 주장 묶음이에요
JWT는 보통 세 부분으로 생겼어요.
여기서 중요한 건 **서명(signature)**이에요. Resource server는 payload를 그냥 믿지 않아요. 인증 서버가 공개한 public key로 signature를 검증해서, token이 중간에 바뀌지 않았고 믿는 발급자가 서명했다는 점을 확인해요.
이 그림에서 payload를 읽는 일과 payload를 믿는 일은 달라요. 누구나 payload를 읽을 수 있지만, resource server는 signature, issuer, 만료 시각, audience 같은 조건을 통과한 token만 인증 정보로 바꿔요.
Spring Boot Resource Server는 issuer나 JWK Set으로 검증 준비를 해요
Spring Boot에서 resource server를 만들 때 핵심은 두 가지예요.- Resource server 관련 Spring Security module이 classpath에 있어야 해요.
- JWT를 검증할 기준을 알려줘야 해요.
issuer-uri 또는 jwk-set-uri를 사용할 수 있다고 설명해요.
issuer-uri를 쓰면 Spring Security는 issuer의 discovery metadata를 통해 JWK Set 위치를 찾고, 그 public key들로 token signature를 검증할 수 있어요.
인증 서버가 discovery endpoint를 제공하지 않거나 JWK Set 위치를 직접 고정해야 한다면 이렇게 쓸 수 있어요.
aud 값을 둘 수 있어요.
issuer-uri와 audiences를 같이 보는 습관이 좋아요. iss는 “누가 발급했는가”이고, aud는 “누구에게 쓰라고 발급했는가”예요. 둘 중 하나만 보면 다른 서비스용 token이 실수로 받아들여지는 설계를 놓칠 수 있어요.
Servlet 기반 Spring MVC 앱에서는 보통 SecurityFilterChain에서 resource server JWT 지원을 켜요.
Spring Security는 bearer token이 오면 JWT를 검증하고, 성공하면
Authentication 객체를 SecurityContext에 넣어요. 그 다음 인가 규칙이 이 인증 정보와 권한을 보고 요청을 통과시킬지 결정해요.
Scope는 Spring Security 권한으로 바뀌어요
OAuth2에서 scope는 “이 token이 어디까지 할 수 있는가”를 표현하는 값이에요. 예를 들어 token에 이런 claim이 있다고 해볼게요.scope나 scp claim을 읽어서 권한(authority)으로 바꿔요. 이때 SCOPE_ prefix가 붙어요.
그래서 아래 두 표현은 같은 방향으로 읽을 수 있어요.
hasScope(...)가 더 읽기 쉬워요. 하지만 디버깅할 때는 실제 Authentication 안에 SCOPE_orders:read처럼 들어간다는 점을 기억해야 해요.
인증 서버가 scope나 scp 대신 roles, permissions, authorities 같은 custom claim을 준다면 기본 매핑만으로는 부족할 수 있어요. 그때는 JwtAuthenticationConverter로 어떤 claim을 Spring Security authority로 바꿀지 직접 정해요.
OAuth2 login과 Resource Server는 다른 기능이에요
Spring Security에서 OAuth2를 검색하면oauth2Login과 oauth2ResourceServer를 같이 보게 돼요. 이름이 비슷해서 같은 기능처럼 느껴지지만 역할이 달라요.
예를 들어 “Google로 로그인해서 우리 웹 화면을 보여준다”면
oauth2Login이 중심일 수 있어요. 반대로 “이미 발급된 access token으로 /api/orders를 보호한다”면 resource server가 중심이에요.
둘을 한 애플리케이션에서 같이 쓸 수도 있어요. 하지만 그때도 질문을 분리해야 해요.
- 이 요청은 브라우저 session으로 인증하나요?
- 이 요청은 bearer token으로 인증하나요?
- API endpoint와 web page endpoint가 같은
SecurityFilterChain에 있나요? - CSRF는 cookie 기반 흐름에 맞게 설계됐나요?
Refresh token은 API 서버에 아무 때나 보내는 토큰이 아니에요
Access token은 보통 수명이 짧아요. 유출됐을 때 피해를 줄이기 위해서예요. 그러면 사용자는 매번 다시 로그인해야 할까요? 그래서 refresh token이 등장해요. 하지만 refresh token을 이해할 때 가장 중요한 경계가 있어요.Resource server는 보통 refresh token을 받아서 API 권한을 판단하지 않아요.Refresh token은 access token을 새로 받기 위한 자격에 가까워요. 그래서 보통 authorization server나 token을 관리하는 OAuth2 client 쪽에서 다뤄요. 주문 API 같은 resource server가 refresh token을 받아서 주문 목록을 내려주면 역할이 섞여요.
Refresh token 설계에서 자주 생기는 실수는 이런 것들이에요.
- 브라우저 localStorage에 refresh token을 오래 보관해요.
- API 서버 여러 곳이 refresh token을 직접 받도록 만들어요.
- 로그나 error report에 bearer token 전체가 남아요.
- access token 만료와 refresh 실패를 구분하지 못해 무한 재시도를 만들어요.
- refresh token rotation, 재사용 감지, 폐기 전략 없이 긴 수명만 줘요.
401과 403은 token API에서 특히 다르게 읽어야 해요
JWT resource server를 붙이면 가장 자주 보는 증상은 401과 403이에요. 둘 다 “안 된다”처럼 보이지만 질문이 달라요.
예를 들어 token이 아예 없으면 보통 401이에요.
orders:read scope가 없다면 403이 더 자연스러워요.
- Authorization header가 정확히
Bearer <token>모양인가요? - Token의
iss가 설정한issuer-uri와 맞나요? aud검증을 켰다면 우리 API audience가 들어 있나요?exp,nbf시간이 현재 서버 시간과 맞나요?- JWK Set에서 token header의
kid에 맞는 key를 찾을 수 있나요? - Spring Security authority에 기대한
SCOPE_...또는ROLE_...값이 들어갔나요?
Opaque token을 쓰는 시스템도 있어요
JWT가 흔하지만 모든 access token이 JWT인 건 아니에요. 어떤 시스템은 token 문자열 자체만 봐서는 내용을 알 수 없는 opaque token을 써요. Opaque token은 resource server가 token을 직접 검증하기보다 authorization server의 introspection endpoint에 물어보는 방식으로 확인할 수 있어요.
이 글은 JWT resource server를 중심으로 봤지만, 실무에서는 “JWT가 더 최신이라서 무조건 낫다”가 아니에요. Token을 어디서 폐기해야 하는지, API 서버가 인증 서버와 얼마나 강하게 연결되어도 되는지, 지연 시간과 장애 전파를 어떻게 감당할지로 선택해야 해요.
실무 설정은 “검증 기준”과 “권한 모델”을 분리해서 리뷰해요
JWT resource server 설정을 code review할 때는 코드 줄 수보다 경계를 보는 편이 좋아요.
처음에는 이 정도만 잡아도 Spring Security 설정을 훨씬 덜 무섭게 읽을 수 있어요. JWT가 들어와도 결국 요청 흐름은 지난 글과 이어져요.
- Security filter chain이 요청을 받아요.
- Bearer token을 찾고 검증해요.
- 검증에 성공하면
Authentication을 만들어요. - Scope나 claim을 authority로 바꿔요.
authorizeHttpRequests나 method security가 접근을 결정해요.
참고한 링크
- Spring Boot Reference - OAuth2
- Spring Boot Reference - Spring Security
- Spring Security Reference - OAuth2 Resource Server JWT
- Spring Security Reference - OAuth2 Resource Server Opaque Token
자, 정리해볼까요?
- JWT는 로그인 기능 전체가 아니라, claim과 signature를 가진 token 형식으로 읽어야 해요.
- OAuth2 resource server는 token을 발급하는 서버가 아니라, 들어온 access token을 검증하고 API 접근을 판단하는 서버예요.
- Spring Boot resource server는
issuer-uri,jwk-set-uri,audiences같은 기준으로 JWT를 검증해요. scope나scpclaim은 기본적으로SCOPE_...authority로 바뀌고, custom claim은 converter로 매핑해야 해요.- Refresh token은 access token 갱신을 위한 더 민감한 자격이라서 resource server 책임과 분리해서 다뤄야 해요.
- 401은 token 신뢰 문제, 403은 권한 문제로 나눠 보면 디버깅이 쉬워져요.