JWT 에 대해서

youngjae-Kim·2023년 6월 1일
0

JWT 탄생 배경

브라우저 ↔ 서버 사이에서 정보(id, pw …)들을 주고 받을 때 쿠키를 사용하고 이는 요청을 하고 다시 요청을 할 때 같은 요청임을 의미하는 입장권이 개념이었다.

그런데 앱 같은 경우,

아이폰, 안드로이드 앱 ↔ 웹서버 간은 쿠키 발행을 하지 않는다. 앱은 웹이 아니기 때문


요청 : ID/PW

요청 : 제목/내용 → 한 번 인증을 하고 나면 그 다음에는 id/pw 를 다시 보내지 않아도 된다 (쿠키가 대신 해줌)

요청 : 제목/내용

웹은 쿠키가 요청마다 계속 ID/PW를 계속 가지고 다닐 수 있는 기능이 있다.


요청 : ID/PW, 제목/내용

요청 : ID/PW, 제목/내용

요청 : ID/PW, 제목/내용

초창기 앱은 계속 ID/PW를 모든 요청 시 마다 계속 보냈었다.

앱은 브라우저가 아니기 때문에 쿠키가 없어서 ID/PW를 계속 해서 가지고 다닐 수 있는 수단이 없다.

그래서 요청마다 계속 정보를 넘겨주어야 했었다.

앱 시작

  • 아이디 비번을 입력
  • 앱이 서버에게 아이디, 비번이 유효한지 물어본다.
  • 유효한 아이디와 비번이면 그 정보를 영구 저장한다.
  • 하지만 그래도 요청(글쓰기, 좋아요, 인증이 필요한 활동 등등)이 있을 때 마다 계속해서 id, pw를 요청에 담아서 넘겨주어야했다.

그래서 쿠키와 같은 역할을 위해서

토큰 ( == 아이디, 비번)

  • 아이디 비번 입력
  • 앱이 서버에게 해당 아이디 비번 유효한지 확인
  • 유효하면 서버는 토큰을 발급해주고 아이디와 비번을 영구 저장한다.

🤔 토큰을 사용하면 괜찮을까?


기존에 아이디와 비번을 요청 정보에 담아서 넘겨주는 방식을 토큰이 대신한다..

근데 토큰 역시 아이디와 비번을 대체하는 수단이라서 이 역시 해킹의 노출에 취약하다.

개선된 것은 하나가 있다..!

바로 개인 정보라서 인증이 가능하지만 아이디와 비번이 쌩으로 노출되지 않아서 해킹을 당해도 토큰이 노출된 것이지 아이디와 비번은 그대로 남아 있기에 비밀번호를 변경하지 않아도 된다는 것이다…

하지만 여전히 좋은 방식은 아니다. → 왜?.. → 요청마다 계속해서 db select가 발생하기 때문이다.

토큰은 또 단점이 존재했다.


아무 의미 없는 난수 따위를 사용했어서 길어지기 시작하면 그냥 쓸데없는 값일 뿐이도 그 값 자체가 의미하는 것이 없었다.

JWT (Json Web Token)


개인정보와 개인키 등을 base64 기반으로 인코딩한 결과 조합을 해시 암호화된 값들로 구성된 토큰인 jwt가 나왔다.

이는 값 자체가 복호화를 하면 의미가 있는 값들로 이루어져 있기 때문에 기존의 토큰보다 조금 개선된 방식이다.

또한 jwt는 최초 인증 후 db select가 일어나지 않아 효율면에서 토큰 보다 더 좋다!

여기서 db를 안거치면 믿을 수 있는 정보가 아니냐 라고 생각할 수 있는데, 시크릿 키를 포함하고 있기 때문에 복호화 후 서버의 시크릿 키와 대조를 하기 때문에 믿을 수 있다.

그래서 정보가 도중에 바뀌어도 서버의 시크릿키를 가지고 정보가 조작되었는지 아닌지의 유무를 알 수 있다..!

서버 사이드에서 이렇게 시크릿키를 가지고 있다.

custom:
  jwt:
    secretKey: secretKeysecretKeysecretKeysecretKeysecretKeysecretKeysecretKeysecretKeysecretKeysecretKey

원문의 시크릿 키를 가지고 base64 로 인코딩 한 후 인코딩한 결과를 해시 암호화를 적용해서 암호화 한다.

String keyBase64Encoded = Base64.getEncoder().encodeToString(secretKeyPlain.getBytes()); // 원문을 Base64로 인코딩
// Base64 인코딩된 키를 이용하여 SecretKey 객체를 만든다.
SecretKey secretKey = Keys.hmacShaKeyFor(keyBase64Encoded.getBytes()); // 인코딩 한 결과를 암호화

그리고 인코딩과 암호화된 시크릿 키는 하나만 존재해야 하고 시크릿 키를 요청할 때마다 키가 유효하다면 일관된 키를 반환해야 한다.

private SecretKey _getSecretKey() { // 시크릿 키를 가져온다
    String keyBase64Encoded = Base64.getEncoder().encodeToString(secretKeyPlain.getBytes());
    return Keys.hmacShaKeyFor(keyBase64Encoded.getBytes());
}

public SecretKey getSecretKey() { // 시크릿키를 가져오는 메서드
    if (cachedSecretKey == null) cachedSecretKey = _getSecretKey(); // 시크릿 키가 존재하지 않으면 가지고 있는 시크릿 키를 base64인코딩 후 암호화

    return cachedSecretKey;
}

getSecretKey() 메서드를 실행할 때에 이미 유효한 키가 존재하는지 여부를 묻고 없으면 생성 후 반환

있다면 원래 있던 (캐시된) 시크릿 키를 반환하는 메서드이다.

읽다가 깔끔하게 정리가 잘 되어 참고하였다.

🌐 JWT 토큰 인증 이란? (쿠키 vs 세션 vs 토큰)

실습할 땐 헤더를 만들지 않았지만 payload에 해당하는 claim 이 토큰 유효 기간과 함께 담긴다.

claim : 토큰에서 사용할 정보의 조각 → 서버와 클라이언트가 주고 받는 시스템에서 사용될 정보들에 대한 내용을 담고 있다.

Map<String, Object> claims = new HashMap<>();
claims.put("id", 1L);
claims.put("username", "admin");

// 지금으로부터 5시간의 유효기간을 가지는 토큰을 생성
String accessToken = jwtProvider.genToken(claims, 60 * 60 * 5);

실제 엑토큰을 생성하는 부분인 genToken() 메서드를 들여다 보자

public String genToken(Map<String, Object> claims, int seconds) {
    long now = new Date().getTime(); // 현재 시간
    Date accessTokenExpiresIn = new Date(now + 1000L * seconds); // 매개 변수로 들어온 시간을 밀리로 바꿔서 현재 시간에 더해줌

    return Jwts.builder() // jwts 객체를 생성해서 반환 (실제 토큰을 생성하는 부분)
            .claim("body", Ut.json.toStr(claims)) // jwt 의 body 부분에 저장할 claim 을 json
            .setExpiration(accessTokenExpiresIn)
            .signWith(getSecretKey(), SignatureAlgorithm.HS512)
            .compact();
}

payload 부분과 (헤더가 있었다면 (헤더 + 페이로드) 부분) 이 body로 들어가는 것을 볼 수 있다.

그리고 토큰 만료 기간을 정해주고 (setExpiration), 시크릿 키를 해시 암호화 해준 뒤 합쳐준다.

질문: claim() 메서드 매개변수 “body”의 의미가 뭘까?


payload가 되어야 하지 않을까?

ChatGPT로 얻은 해답


claim() 메서드의 첫 번째 매개변수는 클레임의 이름을 나타냅니다. 클레임의 이름은 문자열로 지정되며, 보통 문자열 형태의 식별자를 사용합니다. 위 코드에서는 "body"라는 이름의 클레임을 사용하고 있습니다.

"body" 클레임은 주로 JWT의 본문(body)에 해당하는 데이터를 담는 용도로 사용됩니다. 일반적으로 사용자 정의 데이터를 "body" 클레임에 저장하여 JWT의 내용을 구성합니다. 클레임의 값은 객체 또는 문자열 형태일 수 있습니다.

위 코드에서는 Ut.json.toStr(claims)를 통해 claims 객체를 JSON 문자열로 변환한 후, "body" 클레임의 값으로 지정하고 있습니다. Ut.json.toStr() 메서드는 이전에 설명한대로 Map<String, Object> 객체를 JSON 문자열로 변환하는 역할을 합니다.

따라서, 위 코드에서 "body" 클레임은 claims 객체를 JSON 문자열로 변환한 값으로 설정되며, JWT의 payload에 해당 데이터가 포함되어 전송될 수 있습니다. 이렇게 함으로써 JWT는 토큰 내에 사용자 정의 데이터를 담을 수 있게 되며, 클라이언트나 서버에서 해당 정보를 활용할 수 있습니다.

이렇게 얻은 엑세스 토큰은 다음과 같이 생겼다!!!

accessToken : eyJhbGciOiJIUzUxMiJ9.eyJib2R5Ijoie1wiaWRcIjoxLFwidXNlcm5hbWVcIjpcImFkbWluXCJ9IiwiZXhwIjoxNjg0MjU3NTIxfQ.kh7U8BzumJh12s4opNjwybtgelvwAPiathC3OSdmuEDQ7GumeeU_bjXutaEplQlD8B1q0vsklJfJ2ZjeHbTt-A

jwt 토큰 생성 방식을 통해 직접 엑세스 토큰을 만들어 보는 시간을 가지면서 jwt의 원리와 구성에 대해 알 수 있었던 시간이었다.

profile
영원히 남는 기록, 재밌게 쓰자 :)

0개의 댓글