본문 바로가기

Back-End/Spring-MVC

JWT(Json Web Token)

1. JWT란?

  • JWT는 사용자 인증과 정보 교환을 위해 사용되는 JSON 기반의 웹 토큰이다.
  • 구성
    1. Header : 토큰 타입과 서명 알고리즘 정보
    2. Payload : 사용자 정보와 클레임(데이터)
      • JWT 클레임 : JWT의 Payload 부분에 포함된 데이터로, 토큰에 담고 싶은 정보를 저장
      • claim("key", value)
        • "key"는 클레임 이름
        • value는 클레임의 값
    3. Signature : 토큰의 무결성을 보장하는 서명
      • Header와 Payload를 비밀 키로 서명한 값
  • JWT는 주로 클라이언트가 인증 서버에서 로그인 후 발급받아 이후 요청에 포함해 서버가 인증을 수행하도록 사용된다.

JWT 홈페이지


2. JwtUtil 클래스

JWT 관련 모든 핵심 로직을 처리하는 유틸리티 클래스

  • BEARER_PREFIX
    • JWT를 HTTP 헤더에 포함할 때 사용하는 접두사(Authorization : Bearer <token>)
  • TOKEN_TIME
    • JWT의 유효 기간(60분)
  • signatureAlgorithm
    • JWT 서명에 사용되는 알고리즘
  • secretKey
    • JWT 서명에 사용되는 비밀 키로, 애플케이션 설정 파일에서 주입 받음. 

리소스 파일에서 시크릿키를 만들어준다. 시크릿 키가 짧거나 단순하면 에러가 발생한다.

  • Base64 디코딩
    • 문자열 형태의 비밀 키를 바이너리 데이터로 변환
  • Keys.hmacShaKeyFor
    • 디코딩된 비밀 키를 Key 객체로 변환해 JWT 서명에 사용

사용자 정보를 포함하는 JWT를 생성

  1. setSubject
    • JWT의 subject 필드에 사용자 ID(username) 저장
      subject는 JWT의 표준 클레임(필드) 중 하나로, 토큰의 주체를 나타낸다.
  2. claim("auth", userRole)
    • JWT 토큰의 Payload에 사용자 정의 데이터를 추가하는 과정이다.
    • 이 메서드를 통해 JWT의 클레임(claim)에 "auth"라는 이름으로 사용자 역할(role) 데이터를 저장하는 것.
    • 따라서 "auth" 라는 이름의 클레임에 userRole 값을 저장한다.
  3. setExpiration
    • 토큰 만료 시간 설정
  4. signWith
    • 비밀 키와 알고리즘으로 서명
  5. compact
    • JWT 문자열 생성

 

JWT를 파싱해 사용자 정보를 추출

  • setSigningKey
    • 서명 검증을 위해 비밀 키 설정
  • 서명 검증이란?
    • JWT의 Signature가 변조되지 않았는지 확인하는 작업
    • JWT의 Header와 Payload는 누구나 Base64로 디코딩할 수 있지만, Signature는 비밀 키로 생성되므로 위조를 방지함.
  • parseClaimsJws(token)
    • JWT의 Signature와 서버에서 생성된 서명이 같은지 확인.
    • JWT 문자열을 파싱하여 Header, Payload, Signature를 분리.
  • 토큰 파싱이란?
    • JWT 문자열(압축된 JSON 형태)을 풀어서 Header, Payload(claims), Signature의 3부분으로 나누고, 데이터를 읽을 수 있는 구조로 변환하는 작업
    • 왜 필요한가? JWT는 Base64로 인코딩된 문자열이므로, 데이터를 사용하려면 이를 디코딩하고, 각 부분을 구분해야함.
    • JWT는 .(점)으로 구분된 3개의 부분으로 이루어짐.(Header.Payload.Signature 요롷게)

토큰의 서명 및 만료 여부를 확인


3. JWTFilter 클래스

JwtFilter 클래스는 요청에 포함된 JWT 토큰을 검증하고, 권한(Role)에 따라 요청 흐름을 제어하는 역할

1) 클래스 정의

  • @Slf4j
    • 로깅을 위한 Logger를 추가한다. topic = "JwtFilter"는 로그의 카테고리 이름.
  • @RequiredArgsConstructor
    • final 필드를 초기화하는 생성자를 자동으로 생성
  • implements Filter
    • Java Servlet의 필터 기능을 구현

2) doFilter 메서드

doFilter는 요청을 처리하는 메인 메서드

  • ServletRequest : HTTP 요청 객체
  • ServletResponse : HTTP 응답 객체
  • FilterChain : 다음 필터 또는 컨트롤러로 요청을 전달하는 메서드를 제공

HTTP 요청/응답 객체로 캐스팅

  • ServletRequestServletResponse는 일반적으로 요청 / 응답 객체로 제공됨
    그래서 HTTP 요청 / 응답(HttpServletRequest, HttpServletResponse)으로 변환해야 HTTP 전용 메서드(getHeader, getRequestURI)를 사용할 수 있다.

로그인 요청 처리

  • 로그인 요청(/api/login)
    • JWT 인증이 필요 없는 요청이므로 필터를 통과(chain.doFilter)해서 다음 필터 또는 컨트롤러로 이동

JWT 토큰 존재 여부 확인

  • Authorization 헤더 확인
    • 클라이언트가 요청 헤더에 JWT 토큰을 포함하지 않으면 Unauthorized 응답 반환
  • Bearer 접두사 확인
    • JWT 토큰은 Bearer로 시작해야 한다. 아니면 Unauthorized 응답 반환

JWT 토큰 유효성 검증

  • JWT 추출
    • Bearer 접두사를 제외한 JWT를 추출
  • 유효성 검증
    • JwtUtil.validateToken(jwt)를 호출해 토큰의 서명 및 만료 여부 확인
    • 유효하지 않은 토큰은 403 Forbidden 응답 반환

사용자 정보 추출

  • 사용자 이름 추출
    • JwtUtil.extractUsername 메서드를 통해 토큰의 subject 필드에서 사용자 ID를 가져온다.

관리자 전용 API 처리

  • 관리자 전용 API(/api/admin)
    • JWT에 ADMIN 권한이 포함되어 있는지 확인, 맞다면 요청을 그대로 다음 필터 또는 컨트롤러로 전달
    • ADMIN 권한이 없으면 403 Forbidden 응답 반환

사용자 전용 API 처리

  • 사용자 전용 API(/api/user)
    • JWT에 USER 권한이 포함되어 있는지 확인
    • USER 권한이 없으면 403 Forbidden 응답 반환

일반 API 처리

  • 일반 유저 API 처리
    • 관리자/ 사용자 전용 API가 아닌 경우 요청을 그대로 다음 필터 또는 컨트롤러로 저장

4. FIlterConfig 클래스

SpringBoot 애플리케이션에서 서블릿 필터를 등록하고 설정하는 역할

1) @Configuration 어노테이션

  • 이 클래스가 Spring의 설정 클래스임을 나타낸다.
  • Spring Boot는 이 클래스에 정의된 @Bean 메서드를 호출하여 필터를 등록한다.

2) FilterRegistrationBean 등록

  • @Bean
    • Spring 컨텍스트에 FilterRegistrationBean을 등록하여 JwtFilter가 애플리케이션 요청 흐름에 포함되도록 만든다.
  • FilterRegistrationBean
    • 서블릿 필터를 등록하고, 요청 URL 패턴, 필터 순서 등을 설정한다.
  • JwtUtil
    • JwtUtil 객체가 JwtFilter 생성자에 주입된다.

3) 필터 설정

  • SetFilter(필터 등록)
    • JwtFilter 인스턴스를 등록한다.
    • JwtFilter는 JWT 검증 및 권한 확인을 수행하는 필터
  • SetOrder(필터 실행 순서)
    • 숫자가 작을수록 먼저 실행된다.
    • 이 설정은 여러 필터가 있을 때 어떤 필터가 먼저 실행될지 결정한다.
    • 1로 설정했으므로, 이 필터가 가장 먼저 실행한다.
  • addUrlPatterns(URL 패턴 설정)
    • 필터가 적용될 요청 URL을 지정한다.
    • "/*"로 설정했으므로, 애플리케이션의 모든 요청에 대해 필터가 실행된다. 

4) 반환

  • 설정된 FilterRegistrationBean 객체를 반환하여 Spring 컨텍스트에 등록한다.

동작 흐름

그럼 이제 어드민 권한을 가진 계정으로 로그인을 하고 어드민 컨트롤러에 접근할 때까지의 동작 순서를 알아보자

1. 회원가입 요청

  • URL :http://localhost:8080/api/register
  • Method : POST
  • Headers 
    • Content-Type: application/json
  • Body:
{
  "username": "박병천",
  "password": "1234",
  "email": "admin@example.com",
  "role": "ADMIN"
}
  • 응답 예시
    • HTTP status : 200 OK
    • Body : "회원가입 성공 : 박병천"

2. 로그인 요청

  • 요청
  • URL :http://localhost:8080/api/login
  • Method : POST
  • Headers 
    • Content-Type: application/json
  • Body
{
  "userName": "박병천",
  "password": "1234"
}

동작

  1. LoginController의 /api/login 엔드포인트 호출:
    • UserService의 login 메서드 호출
  2. UserService의 동작
    • UserRespository.findByUsername("박병천") 호출 :
      • DB에서 Username="박병천"에 해당하는 User 엔티티 조회
    • 조회된 User 객체와 요청받은 password 비교 :
      • PasswordEncoder의 matches() 메서드를 통해 암호화된 비밀번호와 일치 여부 확인
    • 비밀번호가 일치하면 JwtUtil.generateToken() 호출:
      • 박병천의 사용자 정보(username, role)를 기반으로 JWT 토큰 생성.
    • 생성된 토큰 반환
  3. 응답
    1. HTTP status : 200 OK
    2. Body : { "token": "Bearer <JWT 토큰>"}

3. 어드민 엔드포인트 접근

요청

    • URL: http://localhost:8080/api/admin/get
    • Method : GET
    • Headers : 
      • Authorization: Bearer <JWT 토큰> (로그인 응답에서 받은 토큰)

동작

1. FilterConfig에 등록된 JwtFilter 실행

  • doFilter 메서드 호출
  • 요청 URI(/api/admin/get) 확인 
    • /api/admin으로 시작하므로 관리자 전용 엔드포인트로 판단.
  • Authorization 헤더에서 JWT 토큰 추출 및 검증 
    • JwtUtil.validateToken() 호출
      • 토큰의 서명과 만료 시간 검증
    • JwtUtil.hasRole() 호출
      • 토큰에 포함된 권한(ADMIN) 확인
  • ADMIN 권한이 있으므로 요청을 체인에 전달(chain.doFilter)

2. AdminController의 /api/admin/get 호출

  • getAdminInfo() 메서드 실행
  • 로그 출력 : 어드민 페이지 호출
  • "어드민 페이지 리소스가 허가 되었습니다." 문자열 반환

3. 응답

  • HTTP Status : 200 OK
  • Body : 어드민 페이지 리소스가 허가 되었습니다.