본문 바로가기

SpringBoot

Spring Boot에서 API 보안을 지키는 방법 - JWT 인증

Spring Boot 환경에서 JWT(JSON Web Token)를 활용해 안전한 API 인증 시스템을 구축하는 방법에 대해 기록하려 합니다.


JWT란 무엇인가

JWT는 정보를 안전하게 주고받기 위해 사용하는 암호화된 토큰입니다. 이 토큰은 세 가지 부분으로 나뉩니다.

  • Header: 토큰의 타입(JWT)과 서명에 사용된 암호화 알고리즘(HS256 등) 정보가 들어있습니다.
  • Payload: Claims라고 불리는, 토큰에 담고 싶은 실제 데이터가 들어가는 곳입니다. 사용자 ID, 토큰 만료 시간, 발급 주체 등 다양한 정보를 자유롭게 넣을 수 있습니다.
  • Signature: Header와 Payload를 합친 후, 비밀 키(Secret Key)를 이용해 암호화한 값입니다. 이 서명이 있기에 토큰의 내용이 위조되었는지 검증할 수 있습니다.

이 모든 정보는 하나의 긴 문자열로 합쳐져서 HTTP 헤더에 담겨 전송됩니다. 서버는 이 토큰을 받아서 서명을 검증하는 것만으로 토큰의 유효성을 확인할 수 있습니다.


JWT 인증의 핵심 흐름 이해하기

JWT 인증 시스템은 간단한 원리로 작동합니다.

  1. 토큰 발급: 외부 서버가 우리 서버에 인증을 요청하면, 우리 서버는 JWT를 발급해 외부 서버에 전달합니다.
  2. API 호출: 외부 서버는 발급받은 JWT를 API 호출 시마다 요청 헤더에 담아 보냅니다.
  3. 토큰 검증: 우리 서버는 요청에 포함된 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) 서버 환경에서도 간편하게 적용할 수 있어, 확장성이 중요한 웹 서비스에 매우 적합한 인증 방법입니다.