티스토리 뷰

오랜만에 글을 쓴다.

 

9월에도 다양한 작업을 했지만, 새 서비스 런칭을 준비하면서 인증관련 작업을 할 일이 생겼다.

 

세션을 써야할까 JWT를 써야할까 고민하다가, 앞으로의 확장성과 러닝커브 등의 이유로 JWT를 선택했다.

(서비스가 언젠가 MSA로 확장될 가능성이 있었다.)

 

JWT를 적용하기 위한 방법으로는 크게 두 가지가 있었다.

 

1. filter에 적용
2. interceptor에 적용

 

기존 서비스는 interceptor에 적용되어 있었고, 이번 서비스에서는 filter+스프링시큐리티를 적용해봤다.

 

각각의 방범의 장단점이 있는데, 이 부분들은 차차 정리하도록 하고

 

이번 글에선 JWT에 대해서 알아보려고 한다.

 

1. JWT란?

JSON 웹 토큰 (JSON Web Token, JWT)은 선택적 서명 및 선택적 암호화를 사용하여 데이터를 만들기 위한 인터넷 표준입니다. 페이로드에는 몇 가지 클레임(claim)을 처리하는 JSON을 보관하고 있으며, 토큰은 비공개 시크릿 키 또는 공개/비공개 키를 사용하여 서명됩니다. 토큰은 크기가 작고 URL 안전으로 설계되어 있으며 특히 웹 브라우저 통합 인증(SSO) 컨텍스트에 유용하다. JWT 클레임은 아이덴티티 제공자와 서비스 제공자 간(또는 비즈니스 프로세스에 필요한 클레임)의 인가된 사용자의 아이덴티티를 전달하기 위해 보통 사용할 수 있다.
- 위키백과

뭔가 어려운 말이 많지만 정리하면, 서버에서 비공개 시크릿 키를 이용해 암호화된 서명과 인코딩된 페이로드를 내려준다고 생각하면 편하다.

 

헤더에는 서명이 어떤 알고리즘으로 암호화가 되었나를 담고있다. 

 

이 그림이 JWT가 담고 있는 내용을 간단히 보여준다.

eyJhbGciOiJIUzI1NiJ9. [헤더]
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNjIwMjk3MTg5LCJleHAiOjE2MjAyOTk5ODksImp0aSI6IjZmMjJkY2RhLThhNDItNDJiYi1iNWFlLTcyZjU2OTFkNzA0YyJ9.[페이로드]
Kd9o0O-P1LS0P0uE8J9qQHPHeNfGq-4WSmGvTVrCkuA [서명]

"." 을 기준으로 헤더, 페이로드, 서명이 나뉜다. (원래는 한줄로 표현됨)

 

여기서 중요한 건 페이로드(PAYLOAD)는 암호화되는게 아니라 단순히 BASE64로 인코딩이 된다는 점이다.

 

그래서 JWT Decoder 같은 사이트에서 열어 볼 수 있으니, 절대로 암호화가 필요한 데이터를 실어서 보내면 안된다.

 

단순히 인코딩 된 데이터라 디코딩하면 모든 내용을 확인할 수 있다.

페이로드에는 다양한 정보를 넣을 수 있는데, 이 내용을 간단히 정리해보자.

 

2. 페이로드(Payload)

페이로드(payload)는 토큰에 포함되는 클레임(claim) 정보를 담고 있는 부분이다. 페이로드에 담을 수 있는 데이터는 JWT의 표준 클레임들과 사용자 정의 클레임들을 포함한다. 주로 사용되는 JWT 페이로드 클레임은 다음과 같다.

 

Registered Claims (표준 클레임)

 

  1. iss (Issuer): 토큰을 발행한 엔터티(발행자)의 식별자를 나타냅니다.
  2. sub (Subject): 토큰의 주제를 나타냅니다. 일반적으로 사용자 ID나 다른 엔터티의 ID를 나타냅니다.
  3. aud (Audience): 토큰이 의도된 대상 그룹을 나타냅니다.
  4. exp (Expiration Time): 토큰의 만료 시간을 나타냅니다.
  5. nbf (Not Before): 토큰의 사용을 허용하기 시작하는 시간을 나타냅니다.
  6. iat (Issued At): 토큰이 발행된 시간을 나타냅니다.
  7. jti (JWT ID): 토큰의 고유 식별자를 나타냅니다.

Public Claims (공개 클레임)

사용자 정의 데이터를 표현하기 위해 사용된다. 사용자의 이름, 이메일 주소, 권한, 역할 등을 나타낸다.


Private Claims (비공개 클레임)

사용자나 클라이언트 애플리케이션 간의 사용자 정의 정보 교환을 위해 사용된다. 

 

3. 구현

구현도 간단하다. token을 생성하고 검증하는 provider 정도만 필요하기 때문이다. 

 

그런데 이 부분이 어디에 위치할 것인가는 담당하는 서비스에서 고민해야할 부분이다.

 

build.gradle

implementation 'io.jsonwebtoken:jjwt:0.9.1'

provider 코드

@Slf4j
@Configuration
public class JwtTokenProvider {

    @Value("${custom.jwt.secret}")
    private String jwtSecret; // 암호화 키

    @Value("${custom.jwt.expirationMinutes}")
    private int jwtExpirationMinutes; // 만료일 상수

    public String createJwtToken(String userId) {
        Date now = new Date();
        Date expirationDate = new Date(now.getTime() + jwtExpirationMinutes * 60 * 1000);

        log.info("새로운 토큰을 발급합니다. userId: {}", userId);

        return Jwts.builder()
                   .setHeaderParam(Header.TYPE, Header.JWT_TYPE)
                   .setIssuer("daniel")
                   .setSubject(userId)
                   .setIssuedAt(now)
                   .setExpiration(expirationDate)
                   .signWith(SignatureAlgorithm.HS256, jwtSecret)
                   .compact();
    }

    public Claims parseJwtToken(String token) {
        try {
            return Jwts.parser()
                       .setSigningKey(jwtSecret)
                       .parseClaimsJws(token)
                       .getBody();
        } catch (JwtException e) {
            log.error("JWT 처리 중 오류 발생: {}", e.getMessage());
            return null;
        }
    }

    public String extractJwtTokenFromHeader(HttpServletRequest request) {
        String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);

        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            return authorizationHeader.substring(7);
        }

        return null;
    }
}

여기선 페이로드에 실을 데이터를 몇 가지 지정하지 않았다. 

 

페이로드에 발행자, 만료시간정도만 담고 subject에 FE에서 사용할 userId 정도만 전달했다.

 

userId는 공개되도 상관이 없어서 별도의 암호화처리를 하지 않았는데,

 

정말 중요한 데이터를 사용하고자하면 별도의 암호화 처리를 해주는게 좋다.

 

4. 사용법

우선 JwtTokenProvider에서 생성한 토큰을 로그인 시 response에 실어서 보내줘야한다.

// 로그인 성공 시 jwt 토큰을 생성
String token = jwt.makeJwtToken(user.getUserId());

// 응답코드 
{
    "time": "",
    "code": "200",
    "response": {
        "token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNjIwMjk3MTg5LCJleHAiOjE2MjAyOTk5ODksImp0aSI6IjZmMjJkY2RhLThhNDItNDJiYi1iNWFlLTcyZjU2OTFkNzA0YyJ9.Kd9o0O-P1LS0P0uE8J9qQHPHeNfGq-4WSmGvTVrCkuA"
    }
}

그리고 클라이언트에서는 HTTP 요청 헤더의 "Authorization" 필드에 "Bearer" 접두사와 함께 포함시켜 서버로 전달한다.

Authorization: Bearer <token>

클라이언트가 이러한 Bearer 토큰을 서버에 제공하면, 서버는 토큰의 유효성을 검사를 통해 사용자를 검증해서 요청을 받아들인다.

 

마치며

JWT 자체는 어려운 내용이 아니다.

 

서버에서 클라이언트에게 주고 싶은 데이터를 전달 + 사용자를 인증하기 위한 하나의 방법이기 때문이다.

 

세션처럼 DB 구성이나 MSA에서의 확장을 위해 큰 고민을 할 필요도 없다.

 

jwt를 사용하기 위해서는 그냥 서버에서 시크릿 키를 잘 보관하고, 검증만 잘해주면 되기 때문이다.

 

다만 구현 방법이 다양하고, 여기서부터 스프링 시큐리티 공부를 시작할 수 있었기 때문에 간단히 정리해보고 싶었다.

 

다음엔 스프링부트에서 어떻게 사용할지 알아보자.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/07   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
글 보관함