JWT

유요한·2023년 3월 29일
1
post-thumbnail

Token 인증

토큰 기반 인증 시스템은 클라이언트가 서버에 접속을 하면 서버에서 해당 클라이언트에게 인증되었다는 의미로 토큰을 부여한다. 이 토큰은 유일하며 토큰을 발급받은 클라이언트는 또 다시 서버에 요청을 보낼 때 요청 헤더에 토큰을 심어서 보낸다. 그러면 서버에서는 클라이언트로부터 받은 토큰을 서버에서 제공한 토큰과의 일치 여부를 체크하여 인증 과정을 처리하게 된다.

기존의 세션기반 인증은 서버가 파일이나 데이터베이스에 세션정보를 가지고 있어야 하고 이를 조회하는 과정이 필요하기 때문에 많은 오버헤드가 발생한다. 하지만 토큰은 세션과는 달리 서버가 아닌 클라이언트에 저장되기 때문에 메모리나 스토리지 등을 통해 세션을 관리했던 서버의 부담을 덜 수 있다. 토큰 자체에 데이터가 들어있기 때문에 클라이언트에서 받아 위조되었는지 판별만 하면 되기 떄문이다. 토큰은 앱과 서버가 통신 및 인증할때 가장 많이 사용된다. 왜냐하면 웹에는 쿠키와 세션이 있지만 앱에서는 없기 때문이다.

Token 인증 방식

Token 방식의 단점

토큰 기반 특징

무상태성

무상태성은 사용자의 인증 정보가 담겨있는 토큰이 서버가 아닌 클라이언트에 있으므로 서버에 저장할 필요가 없습니다. 토큰 기반 인증에서는 클라이언트에서 인증 정보가 담긴 토큰을 생성하고 인증합니다. 따라서 클라이언트에서는 사용자의 인증 상태를 유지하면서 이후 요청을 처리해야 하는데 이것을 상태를 관리한다고 합니다. 이렇게 하면 서버 입장에서는 클라이언트의 인증 정보를 저장하거나 유지하지 않아도 되기 때문에 완전한 무상태(stateless)로 효율적인 검증을 할 수 있습니다.

확장성

무상태성은 확장성에 영향을 줍니다. 서버를 확장할 때 상태 관리를 신경쓸 필요가 없으니 서버 확장에도 용이한 것이죠.

무결성

토큰 방식은 HMAC(hash-based message authentication) 기법이라고도 부르는데, 토큰을 발급한 이후에는 토큰 정보를 변경하는 행위는 할 수 없습니다. 즉, 토큰의 무결성이 보장됩니다. 만약 누군가 토큰을 한 글자라도 변경하면 서버에서는 유효하지 않은 토큰이라고 판단하는 것이죠.


JWT

서버에 의해 전자 서명된 토큰을 이용하면 인증으로 인한 스케일 문제를 해결할 수 있다. 이렇게 전자 서명된 토큰중 하나가 바로 JSON 웹 토큰 이하 JWT다. 발급받은 JWT를 이용해 인증을 하려면 HTTP 요청 헤더 중에 Authentication 키값에 Bearer + 토큰 값을 넣어 보내야 합니다.

JWT는 오픈 스탠다드인데 JWT는 말 그대로 JSON 형태로 된 토큰이다. JWT 토큰은 {header}, {payload}, {signature}로 구성돼 있다.

Header

  • typ : Type을 줄인 말, 이 토큰의 타입을 의미한다.
  • alg : Algorithm을 줄인 말, 토큰의 서명을 발행하기 위해 사용된 해시 알고리즘의 종류를 의미한다.

Payload

  • sub : Subject를 줄인 말, 이 토큰의 주인을 의미한다. sub는 ID처럼 유일한 식별자여야 한다.
  • iss : Issuer를 줄인 말, 이 토큰을 발행한 주체를 의미한다. 예를들어 페이스북이 발행한다면 facebook이 된다.
  • iat : issued at을 줄인 말, 토큰이 발행된 날짜와 시간을 의미한다.
  • exp : expiration을 줄인 말, 토큰이 만료되는 시간을 의미한다.

Signature

토큰을 발행한 주체 Issuer가 발행한 서명, 토큰의 유효성 검사에 사용된다.

JWT에서 전자 서명이란 {header}.{payload}와 시크릿 키를 이용해 해시 함수에 돌린, 즉 암호화한 결괏값이다. 시크릿 키는 나만 알고있는 문자열, 비밀번호 같은 것이다.

최초 로그인 시 서버는 사용자의 아이디와 비밀번호를 서버에 저장된 아이디와 비밀번호에 비교해 인증한다. 만약 인증된 사용자인 경우, 사용자의 정보를 이용해 {헤더}.{페이로드} 부분을 작성한다. 그리고 자신의 시크릿 키로 {헤더}.{페이로드} 부분을 전자 서명한다. 전자 서명의 결과로 나온 값을 {헤더}.{페이로드}.{서명}으로 이어붙이고 Base 64로 인코딩 한 후 반환한다.

이후에 누군가 이 토큰으로 리소스 접근을 요청하면, 서버는 일단 이 토큰을 Base64로 디코딩한다. 디코딩해서 얻은 JSON을 {헤더}.{페이로드}.{서명} 부분으로 나눈다. 서버는 {헤더}.{페이로드}와 자신이 갖고 있는 Secret으로 전자 서명을 만든 후, 방금 만든 전자 서명을 HTTP 요청이 가지고 온 {서명} 부분과 비교해, 이 토큰의 유효성을 검사한다. 서버가 방금, 시크릿 키를 이용해 만든 전자 서명과 HTTP 요청의 {서명} 부분이 일치하면 토큰이 위조되지 않았다는 뜻이다. 누군가 헤더나 페이로드 부분을 변경했다면 서명이 일치하지 않기 때문이다. 따라서 인증 서버에 토큰의 유효성에 대해 물어볼 필요가 없다. 이는 인증 서버에 부하를 일으키지 않는 뜻이고 더 이상 인증 서버가 단일 장애점이 아니라는 뜻이기도 하다.

JWT(JSON Web Token)란 인증에 필요한 정보들을 암호화시킨 JSON 토큰을 의미한다. 그리고 JWT 기반 인증은 JWT 토큰(Access Token)을 HTTP 헤더에 실어 서버가 클라이언트를 식별하는 방식이다. JWT는 JSON 데이터를 Base64-safe Encode를 통해 인코딩하여 직렬화한 것이며, 토큰 내부에는 위변조 방지를 위해 개인키를 통해 전자서명도 들어 있다. 따라서 사용자가 JWT를 서버로 전송하면 서버는 서명을 검증하는 과정을 거치게 되며 검증이 완료되면 요청한 응답을 돌려준다.

  • 클라이언트와 서버, 서비스와 서비스 사이 통신 시 권한 인가(Authorization)를 위해 사용하는 토큰

  • Bearer Authentication (JWT 혹은 OAuth에 대한 토큰을 사용하면 Bearer 타입 인증)

  • 회원 인증과 정보 교류할 때 많이 사용합니다.

  • 확장성이 좋아 토큰 기반 인증을 지원하는 다른 서비스에 접근할 수 있습니다.

  • 사용자 인증에 필요한 모든 정보는 토큰 자체에 포함하기 때문에 별도의 인증 저장소(인증서버, DB, 세션 등)가 필요 없습니다.

  • 담을 내용에 따라 크기가 커질 수 있고, 생성 비용이 많이 듭니다. (그러나 요즘 컴퓨팅 성능으로는 신경 쓰지 않아도 될 정도입니다.)

로그인이 되었는지 JWT로 확인할 때, 서버에서 발행한 JWT는 클라이언트가 로컬스토리지 혹은 쿠키 등에 저장하고, 이후 서버에 요청을 보낼떄 헤더에 이 토큰을 심어서 보냅니다. 그러면 서버측에서는 이 토큰이 유효한지 검증 한 후 요청 작업을 처리합니다. 토큰 발행 유효시간은 보통 30분 이내로, 30분이 경과한 토큰은 자동 파기되기에, 클라이언트는 토큰 재발행 프로세스를 염두에 두어야 합니다. 즉 토큰이 유효한 동안은 사용자가 로그인 되었다고 판단할 수 있습니다.

JWT 구조

JWT. 을 구분자로 나누어지는 세 가지 문자열의 조합이다. . 을 기준으로 좌측부터 Header, Payload, Signature를 의미한다.

Header에는 JWT 에서 사용할 타입과 해시 알고리즘의 종류가 담겨있으며, Payload서버에서 첨부한 사용자 권한 정보와 데이터가 담겨있다. 마지막으로 Signature에는 Header, Payload 를 Base64 URL-safe Encode 를 한 이후 Header 에 명시된 해시함수를 적용하고, 개인키(Private Key)로 서명한 전자서명이 담겨있다.

전자서명에는 비대칭 암호화 알고리즘을 사용하므로 암호화를 위한 키와 복호화를 위한 키가 다르다. 암호화(전자서명)에는 개인키를, 복호화(검증)에는 공개키를 사용한다.

실제 디코딩된 JWT는 다음과 같은 구조를 지닌다.

JWT를 이용한 인증 과정

토큰 인증 신뢰성을 가지는 이유

유저 JWT: A(Header) + B(Payload) + C(Signature) 일 때 (만일 임의의 유저가 B를 수정했다고 하면 B'로 표시한다.)

정리하자면, 서버는 토큰 안에 들어있는 정보가 무엇인지 아는게 중요한 것이 아니라 해당 토큰이 유효한 토큰인지 확인하는 것이 중요하기 때문에, 클라이언트로부터 받은 JWT의 헤더, 페이로드를 서버의 key값을 이용해 시그니처를 다시 만들고 이를 비교하며 일치했을 경우 인증을 통과시킨다.

JWT 장단점 정리

JWT 장점

중앙의 인증 서버, 데이터 스토어에 대한 의존성 없음, 시스템 수평 확장 유리하고 Base64 URL Safe Encoding을 이용하기 때문에 URL, Cookie, Header 모두 사용 가능

JWT 단점

Payload의 정보가 많아지면 네트워크 사용량 증가, 데이터 설계 고려 필요하고 토큰이 클라이언트에 저장, 서버에서 클라이언트의 토큰을 조작할 수 없다.

JWT의 Access Token / Refresh Token 방식

다만 이 JWT도 제 3자에게 토큰 탈취의 위험성이 있기 때문에, 그대로 사용하는것이 아닌 Access Token, Refresh Token으로 이중으로 나누어 인증을 하는 방식을 현업에선 사용한다.

Access TokenRefresh Token은 둘다 똑같은 JWT이다. 다만 토큰이 어디에 저장되고 관리되느냐에 따른 사용 차이일 뿐이다.

  • Access Token
    클라이언트가 갖고있는 실제로 유저의 정보가 담긴 토큰으로, 클라이언트에서 요청이 오면 서버에서 해당 토큰에 있는 정보를 활용하여 사용자 정보에 맞게 응답을 진행

  • Refresh Token
    새로운 Access Token을 발급해주기 위해 사용하는 토큰으로 짧은 수명을 가지는 Access Token에게 새로운 토큰을 발급해주기 위해 사용. 해당 토큰은 보통 데이터베이스에 유저 정보와 같이 기록

정리하자면, Access Token은 접근에 관여하는 토큰, Refresh Token은 재발급에 관여하는 토큰의 역할로 사용되는 JWT 이라고 말할 수 있다.

Refresh token이 왜 필요한가?

Access Token만을 통한 인증 방식의 문제는 만일 제 3자에게 탈취당할 경우 보안에 취약하다는 점이다.

Access Token은 발급된 이후, 서버에 저장되지 않고 토큰 자체로 검증을 하며 사용자 권한을 인증하기 떄문에, Access Token이 탈취되면 토큰이 만료되기 전 까지, 토큰을 획득한 사람은 누구나 권한 접근이 가능해 지기 때문이다.

JWT는 발급한 후 삭제가 불가능하기 때문에, 접근에 관여하는 토큰에 유효시간을 부여하는 식으로 탈취 문제에 대해 대응을 하여야 한다. 이처럼 토큰 유효기간을 짧게하면 토큰 남용을 방지하는 것이 해결책이 될 수 있지만, 유효기간이 짧은 Token의 경우 그만큼 사용자는 로그인을 자주 해서 새롭게 Token을 발급받아야 하므로 불편하다는 단점이 있다. 그렇다고 무턱대고 유효기간을 늘리자면, 토큰을 탈취당했을 때 보안에 더 취약해지게 된다. 이때 “그러면 유효기간을 짧게 하면서 좋은 방법이 있지는 않을까?”라는 질문의 답이 바로 Refresh Token이다.

이름이 다르지만 형태 자체는 Refresh Token은 Access Token과 똑같은 JWT다. 단지 Access Token은 접근에 관여하는 토큰이고, Refresh Token은 재발급에 관여하는 토큰 이므로 행하는 역할이 다르다고 보면 된다.

예를 들면, 처음에 로그인을 했을 때, 서버는 로그인을 성공시키면서 클라이언트에게 Access Token과 Refresh Token을 동시에 발급한다. 서버는 데이터베이스에 Refresh Token을 저장하고, 클라이언트는 Access Token과 Refresh Token을 쿠키, 세션 혹은 웹스토리지에 저장하고 요청이 있을때마다 이 둘을 헤더에 담아서 보낸다. 이 Refresh Token은 긴 유효기간을 가지면서, Access Token이 만료됐을 때 새로 재발급해주는 열쇠가 된다. 따라서 만일 만료된 Access Token을 서버에 보내면, 서버는 같이 보내진 Refresh Token을 DB에 있는 것과 비교해서 일치하면 다시 Access Token을 재발급하는 간단한 원리이다. 그리고 사용자가 로그아웃을 하면 저장소에서 Refresh Token을 삭제하여 사용이 불가능하도록 하고 새로 로그인하면 서버에서 다시 재발급해서 DB에 저장한다.

Access / Refresh Token 재발급 원리

Refresh Token 인증 과정

언제 사용하는가?

1. 로그인

  • 사용자 로그인 -> 서버가 해당 유저의 토큰을 유저에게 전달 (JWT)

    유저가 요청을 할때 토큰을 포함해서 전달
    → 서버는 해당 토큰일 권한이 있는지 유효하고 인증이 되었는지 확인하고 작업을 진행

  • 서버는 유저의 세션을 유지할 필요가 없다.

    유저가 보낸 토큰만 확인하면 된다.
    서버의 자원을 아낄수 있다.

2. 정보교류

JWT는 두 개체 사이에서 안정성있게 정보를 교환하기에 좋은 방법이다. 그 이유는, 정보가 sign 이 되어있기 때문에 정보를 보낸이가 바뀌진 않았는지, 또 정보가 도중에 조작되지는 않았는지 검증할 수 있다.

이제 토큰을 발급받기 위한 기능을 정의해보겠습니다.

토큰의 종류로는 Json Web Token을 편리하게 사용하기 위해서 다음과 같은 dependency를 추가해주어야합니다.

implementation 'io.jsonwebtoken:jjwt:0.9.1'

토큰을 다루기 위해 간단한 핸들러를 작성해보겠습니다. 토큰 발급 및 검증을 위한 최소한의 기능만을 만들어낼 것입니다. handler 패키지에 JwtHandler를 작성해줍니다.

package com.example.board2.handler;

import io.jsonwebtoken.*;
import lombok.Data;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
public class JwtHandler {

    private String type = "Bearer";

    public String createToken(String encodedKey, String subject, long maxAgeSeconds){
        Date now = new Date();

        return type + Jwts.builder()
                .setSubject(subject)
                .setIssuedAt(now)
                .setExpiration(new Date(now.getTime() + maxAgeSeconds * 1000L))
                .signWith(SignatureAlgorithm.ES256. encodedKey)
                .compact();
    }

    public String extractSubject(String encodedKey, String token) {
        return parse(encodedKey, token).getBody().getSubject();
    }

    public boolean validate(String encodedKey, String token) {
        try {
            parse(encodedKey, token);
            return true;
        } catch (JwtException e) {
            return false;
        }
    }

    private Jws<Claims> parse(String key, String token) {
        return Jwts.parser()
                .setSigningKey(key)
                .parseClaimsJws(untype(token));
    }

    private String untype(String token) {
        return token.substring(type.length());
    }
}

복잡해보이지만, 실제로는 그렇게 복잡하지 않습니다.

단순히, https://github.com/jwtk/jjwt
위 링크에 서술된 방식대로 토큰을 생성하고, 검증할 뿐입니다.

profile
발전하기 위한 공부

0개의 댓글