Search
🔐

JWT 토큰과 시큐리티의 인증/인가 구현기

Person
태그
인증/인가
목차

JWT Token 전쟁의 서막

[특종] 김시은, 본인의 무지함에 충격먹고 실신…
프로젝트 초반, 저는 웹토큰에 대한 이해도가 상당히 낮은 상태였습니다. 그당시 저는 다른 사람들의 프로젝트를 보며 대략적인 흐름만 파악 후 작업을 진행했고, 그결과는 꽤나 처참했습니다.
오류가 터져도 왜 오류가 터졌는지 몰랐고, 심지어 헤더에 민감정보를 넣고 난리도 아니였죠.
프로젝트 완성 기간도 중요하지만 왜 이걸 하는가? 가 더 중요하다고 판단. 다시 처음부터 차곡차곡 짚어가보자 라는 생각이 들었습니다.

로그인 인증과정 변천사

1. 어세스토큰을 이용한 로그인 기능

처음 구현한 로직은 아래와 같습니다.
1.
유저가 로그인을 위해 id 와 pw 를 입력한다.
2.
그 정보가 시큐리티에게 넘겨진다.
3.
시큐리티 인증이 완료되면, 유효기간을 가진 어세스토큰을 발행해준다.
4.
이 발행된 토큰은 리스폰스 바디에 담겨지며, 구조는 아래와 같다.
{ Authorization: Bearer <token> }
Plain Text
복사
이제 이 후에 요청을 보낼때마다 헤더에 키:값 = Authorization : Bearer <token> 을 담아서 보내주면 됩니다. 이를 받아서 토큰을 검증하고 유효성을 토대로 인가과정을 진행합니다.
하지만 로직을 구현하고 난 후 두가지 요청 사항이 들어왔습니다.
토큰을 리스폰스 바디가 아닌 헤더에 넣어서 보내주세요. Bearer 를 빼주세요.

토큰을 꼭 헤더에 넣어서 보내줘야 하는걸까요?

반드시 그런건 아닙니다. 다만, 처음 IETF 에 의해 JWT 가 제안된 RFC7519 문서에서 Authorization Header 를 통해 encrypted jwt 를 주고 받도록 protocol 로서 규정했기 때문에 이걸 지키는것이 좋다고 생각합니다. 일반적인 약속 이니까요!

Bearer 도 반드시 명시해줘야 하나요?

이 또한 반드시 그런건 아닙니다. Bearer 는 인증타입의 한 종류입니다.
여러가지 인증타입
Basic 사용자 아이디와 암호를 Base64로 인코딩한 값을 토큰으로 사용한다. (RFC 7617)
Bearer JWT 혹은 OAuth에 대한 토큰을 사용한다. (RFC 6750)
Digest 서버에서 난수 데이터 문자열을 클라이언트에 보낸다. 클라이언트는 사용자 정보와 nonce를 포함하는 해시값을 사용하여 응답한다 (RFC 7616)
HOBA 전자 서명 기반 인증 (RFC 7486)
Mutual 암호를 이용한 클라이언트-서버 상호 인증 (draft-ietf-httpauth-mutual)
AWS4-HMAC-SHA256 AWS 전자 서명 기반 인증
따라서 인증타입을 명시해주는것이 위의 경우처럼 일반적인 약속이기에 지키는 것이 좋지만, 저희 프로젝트에서는 따로 적시하지 않았습니다. (적시하면 포트스맨에서 테스트는 훨씬 간편해집니다.)
하 지 만 로직을 수정하고 새로운 질문이 절 반겼습니다.
어세스 토큰 유효기간 만료되면 갱신해야 하는데 리프레시 토큰은 없나요? 네?... 힝... (리..프..레시..토큰..검색)
그렇게 새로운 로직을 짜야할 순간이 왔음을 직감했습니다.

2. 리프레시 토큰을 이용하여 어세스토큰 갱신하기

리프레시 토큰이 없다면?

어세스토큰은 유효기간이 존재합니다. 이 유효기간이 끝나면 어떻게 될까요? 애석하게도 사용자는 강제 로그아웃을 당해야 합니다. 다시 로그인을 하여 어세스토큰을 발급받아야 하죠. 이는 UX 관점에서 볼 때 좋지 않은 케이스임에 분명합니다. 당장 저라도 15 분마다 로그인 하라고 하면 귀찮을 것 같습니다. 때문에 리프레시 토큰을 이용하여 어세스 토큰을 갱신하는 프로세스를 추가해야 했습니다.

리프레시 토큰은 뭔데요?

쉽게말해 어세스 토큰을 재발행 해주기 위한 도구라고 보면 됩니다. 특정 요청이 들어왔다고 가정해봅시다. 로그인 시 발행된 어세스 토큰이 요청 헤더안에 들어있을 거에요. 서버에서는 이를 꺼내 어세스토큰 안에 들어있는 유저정보를 확인하고, 유효성을 검사합니다. 유효기간이 만료되지 않았다면 안에 있는 유저정보를 꺼내 시큐리티에게 건내주죠. 이렇게 인증이 만료되면 유저는 인가를 받을 수 있으며 요청한 리소스를 확인 할 수 있게 됩니다.
근데 만약 이 과정에서 유효기간이 끝나버린다면 우리는 어떻게 해야하나요? 만약 어세스 토큰만 있었다면 로그아웃을 해야겠지만, 리프레시 토큰이 있다면 이야기가 달라집니다. 리프레시 토큰을 이용해 어세스토큰을 재발급 받을 수 있어요!🫢

리프레시 토큰은 어디에 저장해야 하나?

로직을 구현하기전, 제일 큰 고민이였습니다. DB 에 저장하는 것은 정말 아닌것 같고... 어디다 저장해야하나 알아보던 중, Redis 의 존재에 대해 알게되었습니다. Redis 는 NoSQL 인데요, 일반적인 RDBMS 와 다르게 <Key:Value> 형식으로 값을 저장합니다. 때문에 빠르게 원하는 데이터를 가져올 수 있습니다. 또한 만료일을 지정할 수 있습니다. 지정한 만료일이 지나면 데이터가 자동으로 삭제되죠.
그렇게 Redis 를 사용하기로 하였습니다. (솨리질러 )

그렇게 변경된 로직

1.
최초 로그인 시 유저의 이메일을 키값으로 리프레시 토큰을 발급한 뒤, 레디스에 저장합니다.
2.
로그인 시 어세스토큰을 발급한뒤 어세스토큰은 헤더에 넣고, 리프레시 토큰은 존재하는지 확인한 후에 쿠키에 넣어줍니다. (없으면 다시 생성해서 넣어줘요.)
3.
요청이 들어오면 먼저 토큰 필터를 거쳐 토큰의 유효성을 검증합니다.
검증결과 토큰이 유효하면 시큐리티에게 유저 정보를 건내 인증을 마칩니다. 그 후 인가를 받으면 요청한 리소스를 볼 수 있습니다.
검증결과 토큰이 유효하지 않다면 토큰 재발급을 위한 예외를 내려줍니다.
유저가 토큰 만료로 인증을 받지 못한경우
프론트 에서는 어세스 토큰 만료 오류를 캐치하여 토큰 재발급 엔드포인트로 리다이렉트 합니다.
서버 에서는 쿠키를 가져와 유저 정보를 가져오고, 이를 이용하여 어세스 토큰을 재발급 한 뒤 응답 헤더에 넣어줍니다. 또한 시큐리티에게 유저 정보도 넘겨줘야 합니다.
하 지 만 아직 끝이 아니였습니다.
리프레시 토큰도 만료해줘야 해여. 🫠 네...에... (인생이 쓰다.)

3. 리프레시 토큰 유효기간 설정하기

리프레시 토큰을 저장하기 위해 우리는 레디스를 사용했습니다. 장점중의 하나로 데이터의 유효기간을 설정할 수 있다는 것이였는데요, 이와 관련된 내용을 코드에 추가하면 됩니다. 그렇게 설정하고, 테스트를 하던 중 새로운 사실을 알게 되었습니다.

리프레시 토큰이 만료되었는데 어세스토큰이 갱신되는 이슈

분명히 레디스에서 리프레시 토큰이 만료되어 데이터가 삭제되는 것까지 확인했는데 왜 어째서 어세스 토큰이 갱신 되어버린 걸까나..?
다시 처음부터 차근차근 원인을 되짚어보기 시작했습니다. 그리고 발견한 원인! 바로 리프레시 토큰이 만료 되었을 때 쿠키를 삭제하지 않았던 것입니다. 따라서 어세스 토큰 재발급 하는 엔드포인트에서 리프레시토큰이 만료되면 관련 예외를 커스텀하여 프론트가 알 수 있도록 해야 합니다. 그러면 프론트는 로그아웃 처리를 하게되죠. 이때 서버에서도 로그아웃 처리를 같이 해줘야 합니다.
왜냐구요? 쿠키 삭제 해야죠...
이렇게 여차저차 끝난줄 알았던 인증 인가... 하지만 더 큰 산이 남아있었습니다. 저희가 웹소켓을 사용한다는 사실을 까맣게 있고 있었다... 아닙니까... 🫨
웹소켓 인증/인가 에 대해 알아보고 싶다면 를 클릭해주세요.