본문으로 건너뛰기

12.3 무상태(Stateless) 아키텍처와 JWT 발급/검증 흐름

전통적인 웹 애플리케이션의 인증은 서버 메모리에 로그인 유저의 정보를 직접 등기부에 기록해 두는 세션(Session) 방식(Stateful)이었습니다. 반면 현대적인 백엔드는 무상태성(Stateless)과 서버 확장성(Scale-Out)의 제약을 넘기 위해 대부분 JWT (JSON Web Token) 방식을 채택합니다.


🎫 1. JWT의 구조 낱낱이 파헤치기

JWT는 단순 구문(String)들의 덩어리입니다. 점(.)을 기준으로 정확히 3개의 파트 로 나뉩니다: AAAA.BBBB.CCCC

파트 1: 헤더 (Header)

어떤 알고리즘(예: HS256, RS256)으로 암호화 서명을 했는지, 무슨 타입의 토큰인지 메타데이터가 담깁니다.

{
"alg": "HS256",
"typ": "JWT"
}

파트 2: 페이로드 (Payload / Claims)

토큰 안에 담고자 하는 실질적인 정보의 뭉치(클레임) 입니다. 이 정보 덩어리를 Base64Url 형태 인코딩으로 변환해서 쑤셔 넣습니다.

  • 표준 클레임:sub(유저 식별자 PK), exp(토큰 만료 시간), iat(발급된 시간)
  • 커스텀 클레임:"이 유저는 VIP 권한(Role)을 가지고 있다", "이 유저의 이름은 Alice다" 등 백엔드가 나중에 필요로 하는 식별 조각
{
"sub": "user_id_1004",
"role": "ROLE_ADMIN",
"exp": 1711018800
}

파트 3: 서명 (Signature)

가장 핵심이 되는 위변조 방어막입니다. 서버가 절대 외부로 유출해서는 안 되는 비밀키(Secret Key) 를 활용해 1번 헤더와 2번 페이로드 내용을 복잡한 해시 암호 공식(HMAC SHA256 등)에 돌려 나온 "결괏값 도장"을 쾅 찍어놓은 부분입니다.


🕵️ 2. 서버의 완벽한 무상태성 증명 과정

  1. 유저가 스마트폰 앱에서 헤더에 JWT를 박아넣고 GET /api/v1/orders 로 요청을 보냅니다.
  2. 서버는 이 토큰을 가로챕니다(JwtAuthorizationFilter).
  3. 서버는 토큰 앞의 1번, 2번 데이터를 자신이 품고 있는 절대 비밀키(Secret Key)로 다시 한번 도장(서명)을 연산해 봅니다.
  4. 자신이 방금 직접 구워낸 3번 결과 도장과, 유저가 들고 온 토큰 뒷부분의 3번 서명 도장이 한 글자라도 틀리면 "조작된 가짜 토큰"으로 간주하고 폐기(401 Unauthorized) 합니다.
  5. 반대로 일치한다면, 서버는 "아, 이 페이로드에 적힌 유저(user_id_1004) 권한 데이터는 내 비밀키로 예전에 정상 발급했던 팩트(Fact)가 틀림없군!" 이라고 DB 조회 한 번 없이 즉각 신분증을 100% 신뢰(인가)하고 통과시킵니다.

이것이 DB (세션 스토리지)에 유저 접속 기록을 저장하지 않고도 무상태(Stateless) 인증을 끝내는 기적의 매커니즘 입니다.


💡 3. 실제 시큐리티 검증 로직 구현 필터의 원칙

대부분의 실무에서는 요청이 들어올 때 스프링 커스텀 필터 체인을 하나 만들어서 헤더의 JWT를 빼내와 파서(jjwt)로 검증시킵니다.

@Component
public class JwtAuthorizationFilter extends OncePerRequestFilter {

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {

// 1. HTTP 헤더 Authorization 필드에서 "Bearer AAA.BBB.CCC" 추출
String bearerToken = request.getHeader("Authorization");

if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
String token = bearerToken.substring(7); // "Bearer " 문자열 잘라내기

try {
// 2. JJWT 라이브러리를 통해 서버 비밀키로 토큰 위변조 서명(Signature)을 검증함
Claims claims = Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.getBody();

// 3. 무사 통과 시 강제로 시큐리티 인증 객체(Authentication) 생성 후 꽂아넣어 "인가" 완료 마크 찍음
String username = claims.getSubject();
// (생략) UsernamePasswordAuthenticationToken 생성 후 SecurityContextHolder 에 저장

} catch (ExpiredJwtException e) {
// 토큰 시간 초과!
} catch (JwtException e) {
// 누군가 조작한 토큰(위조 지폐) 예외 폭발! 해킹 공격으로 간주
}
}
filterChain.doFilter(request, response);
}
}

🎯 고수 팁 (Pro Tips)

💡 보안의 가장 어리석은 실수: JWT 페이로드에 극비 데이터 넣기

입문자들이 가장 크게 착각하는 것은 "JWT는 암호화된 토큰이니까 안전하겠지?" 입니다. 절대 아닙니다. JWT의 2번 페이로드(내용물) 부분은 단순 Base64 인코딩일 뿐이므로, 세상 누구라도 사이트(jwt.io)에 복붙하면 0.1초 만에 디코딩되어 읽을 수 있는 평문(투명한 엽서)입니다!!

서명(Signature)의 역할은 그 내용을 "조작/변작" 하지 못하게 막는 도장일 뿐, 내용을 남이 "열람/읽지" 못하게 보안 암호화로 감싸는 기능이 단 하나도 없습니다. 따라서 JWT Payload 내부에는 주민등록번호, 비밀번호, 카드 번호 같은 초절정 민감 정보를 절대 넣으면 안 되며, 오직 유니크한 로그인 ID(혹은 UUID)와 만료 시간, 단순 일반 권한 체계 정도만 집어넣는 것이 황금률입니다.