Spring Boot 환경에서 JWT(JSON Web Token)를 활용해 안전한 API 인증 시스템을 구축하는 방법에 대해 기록하려 합니다.
JWT란 무엇인가
JWT는 정보를 안전하게 주고받기 위해 사용하는 암호화된 토큰입니다. 이 토큰은 세 가지 부분으로 나뉩니다.
- Header: 토큰의 타입(JWT)과 서명에 사용된 암호화 알고리즘(HS256 등) 정보가 들어있습니다.
- Payload: Claims라고 불리는, 토큰에 담고 싶은 실제 데이터가 들어가는 곳입니다. 사용자 ID, 토큰 만료 시간, 발급 주체 등 다양한 정보를 자유롭게 넣을 수 있습니다.
- Signature: Header와 Payload를 합친 후, 비밀 키(Secret Key)를 이용해 암호화한 값입니다. 이 서명이 있기에 토큰의 내용이 위조되었는지 검증할 수 있습니다.
이 모든 정보는 하나의 긴 문자열로 합쳐져서 HTTP 헤더에 담겨 전송됩니다. 서버는 이 토큰을 받아서 서명을 검증하는 것만으로 토큰의 유효성을 확인할 수 있습니다.
JWT 인증의 핵심 흐름 이해하기
JWT 인증 시스템은 간단한 원리로 작동합니다.
- 토큰 발급: 외부 서버가 우리 서버에 인증을 요청하면, 우리 서버는 JWT를 발급해 외부 서버에 전달합니다.
- API 호출: 외부 서버는 발급받은 JWT를 API 호출 시마다 요청 헤더에 담아 보냅니다.
- 토큰 검증: 우리 서버는 요청에 포함된 JWT를 검증합니다. 서명이 유효하고, 토큰이 만료되지 않았다면 API 요청을 승인합니다.
이러한 과정은 OncePerRequestFilter를 상속받은 JwtFilter라는 커스텀 필터에서 처리됩니다.
이 필터는 모든 HTTP 요청이 컨트롤러에 도달하기 전에 가장 먼저 실행되는 역할을 합니다.
JwtFilter의 역할
- 토큰 발급 요청: /api/auth/token 같은 특정 URL로 들어온 요청은 인증 절차 없이 바로 토큰 발급 로직으로 보내집니다.
- API 호출 요청: 그 외의 모든 요청은 Authorization 헤더에 Bearer 토큰이 있는지 확인합니다.
- 토큰이 없다면: 401 Unauthorized 에러를 반환해 접근을 거부합니다.
- 토큰이 만료되었거나 유효하지 않다면: 예외를 발생시켜 /error 페이지로 요청을 전달합니다. 이를 통해 모든 에러를 한곳에서 통일성 있게 처리할 수 있습니다.
- 토큰이 유효하다면: 요청을 통과시켜 원하는 API에 접근할 수 있도록 허용합니다.
JWT 발급 및 검증 로직 상세 살펴보기
1. JWT 발급 - createToken
private String createToken(String clientId, long expiresIn, String type) {
Date now = new Date();
return Jwts.builder()
.setIssuer(clientId) // 발급한 주체
.setSubject(type) // 토큰 목적 ("access", "refresh")
.setIssuedAt(now) // 발급 시각
.setExpiration(new Date(now.getTime() + expiresIn)) // 만료 시각
.signWith(SignatureAlgorithm.HS256, secretKey.getBytes()) // HS256 + 시크릿키
.compact(); // 문자열 형태 JWT 생성
}
이 코드는 JWT 토큰의 Payload에 어떤 정보를 담을지 정의하고, secretKey를 이용해 서명하는 과정을 보여줍니다.
이렇게 생성된 토큰은 API 요청 시 Authorization 헤더에 담겨 보내집니다.
2. JWT 조회 및 검증- getIssuer
public String getIssuer(String token) {
try {
return Jwts.parser()
.setSigningKey(secretKey.getBytes()) // ① 시크릿키로 서명 검증
.parseClaimsJws(token) // ② 토큰 파싱 및 서명 확인
.getBody() // ③ Payload (Claims) 추출
.getIssuer(); // ④ Issuer(clientId) 반환
} catch (ExpiredJwtException e) {
throw new ApiException(ApiErrorCode.EXPIRED_TOKEN); // 만료된 토큰
} catch (JwtException | IllegalArgumentException e) {
throw new ApiException(ApiErrorCode.INVALID_TOKEN); // 유효하지 않은 토큰
}
}
이 코드는 받은 토큰이 유효한지 확인하는 과정입니다.
가장 중요한 부분은 setSigningKey로 비밀 키를 사용해 서명을 검증하는 부분입니다. 이 과정에서 토큰의 내용이 위조되지 않았음을 확인하고, 만료 여부까지 체크합니다.
만약 유효성 검증에 실패하면 적절한 예외를 발생시켜 오류를 처리합니다.
마무리
이번에 구축한 JWT 인증 시스템은 외부 서버와 안전하게 통신할 수 있는 방법으로 구성하였습니다.
Access Token과 Refresh Token을 활용해 토큰의 생명주기를 효율적으로 관리하고, 커스텀 필터를 통해 모든 API 요청에 대해 강력한 인증 절차를 강제함으로써 보안을 강화했습니다.
이 구조는 무상태(Stateless) 서버 환경에서도 간편하게 적용할 수 있어, 확장성이 중요한 웹 서비스에 매우 적합한 인증 방법입니다.