Jwt에 대한 의견과 고찰

1

Spring

목록 보기
11/12
post-custom-banner

요즘 개발을 하면서 Authentication에 관련된 것들은 거의 Spring Security + Jwt로 개발한다.

처음에 Jwt를 접했을 때 Jwt가 무엇이고 헤더, 페이로드, 액세스 토큰, 리프레시 토큰 등 뭐가 뭐고 얘가 이런애고 쟤는 저거 하는 애고 이론적으로는 그닥 어렵진 않았다.

하지만 백문이 불여일타라고 했다. (( 내가하고 싶은 말은 반대되긴 하지만..
원래 백번 말하는것보다 한번 치는게 더 이해된다고 하던데, 한번 쳐보려하니 머릿속으로는 정리가 됬던 이론들이 제각각 놀기 시작했다.

물론 다른사람 블로그도 보고 깃허브도 보며 염탐을 오지게 해보긴 했지만, 사바사(사람 바이 사람)이여서 그런지 느낌은 비슷하긴해도 복붙으로 해결할 수 없는 부분들이 많았다.

스프링 부트에서 Jwt를 어떤식으로 사용하고 리프레시 토큰을 사용한 방식으로 어떻게 액세스 토큰을 재발급하는가 등등 여러가지 고난역경을 겪었다;

뭐 하여튼, 지금부터 내가 Jwt를 처음 배우고 스프링 부트에 적용하면서 겪었던 난관?을 소개해보겠다. 뭐 솔직히 나만 이해못하고 다른사람들은 거의 이해했을 것이다.

이 글은 Jwt를 소개하는 글이 아니라 내가 개발하면서 Jwt를 어떻게 스프링에 적용시켜야 하는지 했던 고찰이다.

스프링 부트에서 Jwt 방식으로 인증을 구현하는 이유

우리가 알고있는 대표적인 인증 처리 방식은 3가지가 있다.(쿠키, 세션, Jwt)
Jwt는 개인적으로 셋중에서는 제일 까다롭게 느껴졌다.

그래서 이런생각을 해봤다.
셋 다 인증되는건 똑같은데 왜 Jwt를 써야하냐?

이 궁금증이 해결되지 않으면 나에게 있어서 Jwt란 다른 2가지 인증 방식보다 개발 생산성만 떨어지는 인증 방식이라고 인식했을 것이다.

그래서 내가 생각하는 Jwt를 사용해야 하는 이유를 몇가지 살펴보자.

토큰 방식은 사용자가 로그인을 하면 서버에서 발행해주는 토큰을 가지고
브라우저의 저장소에 토큰을 유지시키는 방법이다.
여기서 말하는 토큰이 우리가 말하는 Jwt이다.

확장성

서버에 저장하지 않아서 서버에 확장성이 있다.
위에서 말했듯 로그인을 했을 때 해당 서버에만 요청을 보내는 것이 아닌
요청이 들어왔을 때 해당 토큰이 유효한지만 체크하면 되기 때문에
어떤 서버로 요청을 보내도 상관이 없다는 뜻이다.

정보 교환

Jwt는 당사자 간에 정보를 안전하게 전송하는 좋은 방법이다. 헤더페이로드를 사용하여 서명을 계산하므로 내용이 조작되지 않았는지 확인할 수도 있다.

트래픽에 대한 부담

서버에 직접적으로 관여하는 부분이 크지 않기 때문에 세션보다 트래픽에 대한 부담이 적다.


1. 토큰의 정보는 어디에?

Jwt에 대해서 뭣도 모르고 "쉬워 보이네ㅋㅋ 일단 해보자"라는 생각에 흥분하며 인텔리제이를 실행시켰던 때가 엇그제 같다. 시작하자마자 아무것도 모르고 개발을 한다는건 망상이었다.

Jwt라는 방식으로도 인증할 수 있구나 라는 생각으로 개발을 시작했었던 것 같은데 당연하겠지만 최소한의 Jwt에 관련된 이론적인 지식들은 알아야 개발이 가능하다.

다시 본론으로 돌아와서 액세스 토큰과 리프레시 토큰의 차이점은 유효시간에 있으며, 나머지 정보들은 같다.

이를 바탕으로 로그인 요청을 보내면 유효시간만 다른 토큰 2개가 반환되어야 하는데..

결론부터 말하자면 방법은 쉬웠지만 매번 바뀌는 토큰의 시간을 전역 변수로 선언해야 하는가에 대해 오래 고민했다.

시간만 따로 관리해주는 클래스를 만들어서 사용하면 더 쉽게 접근할 수 있었다.

우선 아시겠다시피 Jwt는 액세스 토큰과 리프레시 토큰이 있으며,

액세스 토큰이 만료되면, 리프레시 토큰을 통해서 액세스 토큰을 재발급 받는다.

라는 개념이 잡혀있어야 한다.

이것이 성립하려면 리프레시 토큰이 액세스 토큰보다 유효 시간이 길어야 하는것이 성립되어야 하기 때문에 리프레시 토큰의 유효 시간을 길게 잡아준다.

액세스 토큰의 유효시간을 짧게 잡는 이유는 Jwt를 발급한 후에는 만료되기 전까지는 토큰을 무효화 할 수 없기 때문에 보안이 취약하기 때문이다.

보통 액세스 토큰의 유효 시간은 약 1시간 ~ 2시간 이며, 리프레시 토큰은 짧으면 2주 ~ 한 달까지 잡기도 한다고 한다. (( 뭐 이것도 사바사이긴 함..

이제 약간의 코드를 섞어가면서 살펴보자.


gradle

dependencies {
	implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2'
	runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2'
	runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2'
}

Jwt를 사용하기 위해서는 당연히 Jwt 라이브러리를 땡겨와야 한다.


application.yml

spring:
	security:
    	jwt:
        	secret: c2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXQtc2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXQK

HS512 알고리즘을 사용할 것이기 때문에 512bit, 즉 64byte 이상의 secret key를 사용해야 한다.

쉽게 말하면 저자리에는 그냥 아무거나 길게 적어놓으면 된다는 말임..


액세스 토큰

뭐 간단하게 토큰의 생성과 유효성 검증등을 담당할 클래스를 만들어주자.

@Component
public class JwtTokenProvider {

}

여기저기서 의존성 주입을 받으며 사용당해야 하기 때문에 @Component 어노테이션을 사용한다.

@Value("${spring.security.jwt.secret}")
private String secretKey;

@Value로 아까 yml에 작성해 놓았던 시크릿 키를 가져오자. 따로 설정을 하지 않았다면 기본 경로는 resources 아래에 있는 application.yml로 지정될 것이다.

@PostConstruct
protected void init() {
     secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
}

@PostConstruct는 의존성 주입이 이루어진 후 초기화를 수행하는 메서드이다.

따라서 이 컴포넌트는 의존성 주입이 이루어진 후 시크릿 키가 Base64로 인코딩 될 것이다.

public String createToken(페이로드에 넣고 싶은 정보(매개변수가 몇개이든 상관없음), 토큰 유효 시간) {
        Claims claims = Jwts.claims();
        claims.put("email", email);
        Date now = new Date();

        return Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(now)
                .setExpiration(new Date(now.getTime() + time))
                .signWith(getSigningKey(secretKey), SignatureAlgorithm.HS256)
                .compact();
    }

claimpayload 부분에 들어갈 정보 조각들이다.

claimRegistered claim, Public claim, Private claim으로 나눠진다.

나같은 경우에는 페이로드 부분에 사용자의 이메일 정보를 넣어줬다.

Jwt.builder에서는

헤더의 타입(typ), 발급 시간(iat), 만료 시간(exp), 비공개 클레임을 설정할 수 있다.

발급 시간(iat)만료 시간(exp)은 Date 타입만 추가가 가능하다.

비공개 클레임key-value 쌍으로 이루어져 있으며, 헤싱 알고리즘시크릿 키를 설정할 수 있다.

public String createAccessToken(페이로드에 넣고싶은 정보) {
    return createToken(페이로드에 넣고싶은 정보, 토큰 유효 시간);
}
    
public String createRefreshToken(String email) {
    return createToken(페이로드에 넣고싶은 정보, 토큰 유효 시간);
}

이렇게 액세스 토큰과 리프레시 토큰을 생성하는 코드를 작성해보았다.

위에서 언급했듯이 이 코드에 토큰 유효 시간만 따로 관리하는 클래스를 만들어서 적용시켜도 되고, 전역 변수로 선언해도 안될건 없다.


2. 액세스 토큰의 재발급에 대해서

내가 액세스 토큰을 리프레시 토큰을 이용해서 재발급 받는 구조를 이해 못해서 그냥 액세스 토큰을 리프레시 토큰만큼 길게 잡아서 액세스 토큰을 리프레시 토큰마냥 사용하려 했던적이 있다.

(난 처음에 엑세스 토큰이 만료되면 리프레시 토큰으로 자동 변경되는줄 알았다ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ)

하지만 리프레시 토큰은 리프레시 토큰이고 액세스 토큰은 액세스 토큰이 아닌가. 리프레시 토큰을 이용해서 액세스 토큰을 재발급한다는게 무슨 말인지 알아보자.

우선 액세스 토큰을 1시간으로, 리프레시 토큰은 2주일로 설정해놓았다고 가정하자. 따라서 액세스 토큰은 유효시간이 짧기 때문에 비교적으로 재발급 받는 빈도가 많을 수 밖에 없다.

따라서 리프레시 토큰을 사용하는 이유는 1시간마다 사용자 재로그인이라는 미친 번거로움을 없애고, 토큰 무효화 기능 관련 부하 문제를 막을 수 있다.

(리프레시 토큰마저 만료된다면, 다시 로그인해야함)

액세스 토큰과 달리 리프레시 토큰은 발급시 데이터베이스에 저장하고, 액세스 토큰을 재발급 받을 시 리프레시 토큰을 request header에 담은 후 요청을 보내고, 데이터베이스에 있는 리프레시 토큰과 같으면 새로운 액세스 토큰을 재발급 해주는 형태이다.

백문이 불여일견이라고했다. (나는 이 말을 참 좋아하는 것 같다..) 사진으로 보자.

로그인 후 액세스 토큰과 리프레시 토큰 발급

액세스 토큰 재발급

기존의 액세스 토큰이 만료됐다고 가정하고 재발급 받아야 하는 상황

처음에 로그인을 하고 발급 받았던 리프레시 토큰을 헤더에 넣어주고 요청을 보내면!

만료되지 않은 새 액세스 토큰으로 반환된다.
리프레시 토큰이 null인 이유는 같은 로그인을 했을때와 같은 responseDto로 반환했는데, 액세스 토큰만 재발급 해서 그렇다. 신경쓰지 않아도 됨..

여기에 관련된 내용은 이 포스트가 정리가 잘되어있다.


3. 매 요청마다 토큰이 만료되었는지 어떻게 확인하는가

액세스 토큰을 재발급 받으려면 우선 토큰이 만료되었는지 안되었는지 먼저 알아야한다.

액세스 토큰이 만료되었는지 안되었는지 판단하는 기능이 어려운 것이 아니었다.
이를 어떻게 매 요청마다 확인할지가 고민이었다.

이것도 결론부터 말하면 Filter에 있었는데, SecurityConfig라는 시큐리티 설정 클래스에서 스프링 시큐리티가 지원하는 필터체인 전에 실행하도록 하는 .addFilterBefore()메서드를 사용했다.

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider, customUserDetailService, jwtValidateService),
                        UsernamePasswordAuthenticationFilter.class)
                .addFilterBefore(new JwtExceptionFilter(), JwtAuthenticationFilter.class);
    }

이렇게 토큰이 유효한지 판단하는 클래스와, Jwt의 정보를 필요로 하는 클래스들을 먼저 필터링 하게 만들었다.


끝내며

이번에 작성한 글은 Jwt로 어떻게 로그인을 해야하고 사용하는지 세세하게 다루기 보다는 내가 Jwt를 스프링 부트에 사용하면서 어떤걸 고민했고, 어떻게 사용해서 문제들을 풀어나갔는지에 대한 관점들이 주를 이루었다.

이번에 Jwt로 개발해보면서 Jwt 뿐만 아니라 스프링 전반적인 이해도 또한 많이 길러진 것 같다.

처음에 언급했다시피 다른사람들의 글과 코드를 많이 보았다.
내가 하던 것보다 더 좋은 방법들도 많아 보였다.

비록 이해하고 사용하자는 내 고질병 덕분에 개발부터 글까지 쓰는 시간이 오래 걸리긴 했지만, 여전히 이해하고 사용해야 한다는 생각은 달라지지 않은 것 같다.

계속 열심히 개발합시다.. 여러분 화이팅!


References

post-custom-banner

0개의 댓글