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

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

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


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

- setSubject
- JWT의 subject 필드에 사용자 ID(username) 저장
subject는 JWT의 표준 클레임(필드) 중 하나로, 토큰의 주체를 나타낸다.
- JWT의 subject 필드에 사용자 ID(username) 저장
- claim("auth", userRole)
- JWT 토큰의 Payload에 사용자 정의 데이터를 추가하는 과정이다.
- 이 메서드를 통해 JWT의 클레임(claim)에 "auth"라는 이름으로 사용자 역할(role) 데이터를 저장하는 것.
- 따라서 "auth" 라는 이름의 클레임에 userRole 값을 저장한다.
- setExpiration
- 토큰 만료 시간 설정
- signWith
- 비밀 키와 알고리즘으로 서명
- compact
- 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 요청/응답 객체로 캐스팅

- ServletRequest와 ServletResponse는 일반적으로 요청 / 응답 객체로 제공됨
그래서 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"
}
동작
- LoginController의 /api/login 엔드포인트 호출:
- UserService의 login 메서드 호출
- UserService의 동작
- UserRespository.findByUsername("박병천") 호출 :
- DB에서 Username="박병천"에 해당하는 User 엔티티 조회
- 조회된 User 객체와 요청받은 password 비교 :
- PasswordEncoder의 matches() 메서드를 통해 암호화된 비밀번호와 일치 여부 확인
- 비밀번호가 일치하면 JwtUtil.generateToken() 호출:
- 박병천의 사용자 정보(username, role)를 기반으로 JWT 토큰 생성.
- 생성된 토큰 반환
- UserRespository.findByUsername("박병천") 호출 :
- 응답
- HTTP status : 200 OK
- 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) 확인
- JwtUtil.validateToken() 호출
- ADMIN 권한이 있으므로 요청을 체인에 전달(chain.doFilter)
2. AdminController의 /api/admin/get 호출
- getAdminInfo() 메서드 실행
- 로그 출력 : 어드민 페이지 호출
- "어드민 페이지 리소스가 허가 되었습니다." 문자열 반환
3. 응답
- HTTP Status : 200 OK
- Body : 어드민 페이지 리소스가 허가 되었습니다.
'Back-End > Spring-MVC' 카테고리의 다른 글
| JWT 기반 인증/인가 흐름에서 AOP와 필터(URI기반) (0) | 2025.01.08 |
|---|---|
| JWT 기반 인증 및 역할별 API 설계와 구현 : 관리자와 사용자 권한 처리 (0) | 2025.01.06 |
| 스프링 글로벌 API 예외 처리와 @Valid 검증 예외 처리 (1) | 2024.12.31 |
| Servlet Filter (0) | 2024.12.12 |
| Cookie와 Session (0) | 2024.12.12 |