웹 서비스 구현에 있어서 로그인을 구현하는 방식은 다양한 부분들이 있지만, 크게 JWT와 세션을 이용하는 방식으로 나누어진다. 본 프로젝트에서는 JWT를 이용하여 로그인 기능을 구현하는 방식을 선택하였다.
OAuth = Open Authorization
OAuth는 사용자의 리소스에 대한 안정적인 액세스를 제어하기 위한 표준 프로토콜이다. 그 중 JWT는 토큰 기반의 인증 방식 중 하나로 사용된다. (이거를 미리 알았다면 정처기 한 문제 맞았을텐데...)
JWT = JSON Web Token
기존의 세션방식은 유저의 데이터를 쿠키를 저장하는 전통적인 방식과 다르게 json 객체에 사용자 정보의 일부를 담는다.
기존의 쿠키를 이용한 세션 방식은 몇 가지 문제점을 가지고 있고, 이에 따라서 JWT를 많이 사용하게 되었다. (그렇다고 세션 방식이 무조건 안좋은 것이 아닌 것 같다, 각각의 장단점이 있고 프로젝트의 목적과 방향성에 따라서 세션 방식을 이용하면 되는 것 같다.)
HTTP 프로토콜은 무상태(Stateless) 프로토콜로 알려져 있다. 각각의 HTTP 요청은 이전 요청과 독립적이며, 서버는 클라이언트의 상태를 기억하지 않습니다. 즉 다음 요청에 있어서 그전의 요청은 기억하지 못하고 까먹어버린다. 완전히 독립적이다. 세션은 이 한계를 극복하기 위해 쿠키를 활용하고 서버 측에 상태를 저장하고, 클라이언트에게 세션 식별자를 부여하여 상태를 연결합니다. 하지만 이러한 방식은 서버 측에 부담을 주고, 확장성 문제가 발생할 수 있습니다. 그 이유는 매번 페이지를 이동할 때마다 세션을 검증해야하고, 이는 서버의 메모리에 과부하를 걸게 된다.
또한 세션 식별자가 탈취당하거나 민감한 정보가 세션에 저장되는 경우 등에서 보안적인 측면에서 각별한 유의가 필요하다. 따라서 최근에는 JWT와 같은 OAuth방식의 로그인 방식이 널리 사용되고 있다.
그러나 이 JWT의 방식을 처음 사용해보는 나에게는 몇 가지 문제점이 있었다. 구글에 검색을 해보면 참고할 수 있는 다양한 레퍼런스들이 있었지만, 각 프로젝트마다 해당하는 코드들이 다르고, 어떤 프레임워크를 쓰느냐에 따라서 응용할 수 있는 모듈에 대한 부분과 또한 어떤 서비스냐에 따라서 토큰을 검증하고 만드는 과정이 너무 달랐기 때문이다.
본 프로젝트는 파이썬의 머신러닝을 용이하게 도와줄 tensorflow와 opencv를 활용하기 위해 프레임워크로는 flask를 채택하여 진행하였다. 그러나 flask에서 제공하는 jwt에 대한 모듈들과 구현된 메소드는 정말 많지만, 이에 대한 다양한 부분들이 존재하였다.
먼저 호환성의 문제이다. 다른 모듈들 역시 마찬가지겠지만, 특히 python의 flask에서 제공하는 flask-jwt 모듈은 버전관리와 호환성이 굉장히 중요한 부분이었다. 많은 정보를 찾아보고 다양한 버전을 통해서 flask와 flask-jwt의 호환을 시도해보았지만, request-ctx-stack 오류가 발생하였고 이에 대한 해결 방법 또한 찾는 것이 굉장히 까다로웠다. 따라서 만약 python의 flask 프레임워크로 flask-jwt모듈을 활용하여 jwt를 구현하려한다면, 비추천한다. 현재 기준으로 버전을 맞춰서 진행하는 부분이 굉장히 까다로운 작업으로 보여진다. 또한 레퍼런스의 접근성도 굉장히 까다로워서, 이 모듈에서 제공하는 메소드와 라우팅을 도와주는 데코레이터들이 원하는 대로 작동이 제대로 안될 가능성도 크기 때문이다.
그 다음은 데이터베이스에 토큰을 저장할 지/저장하지 않을 지에 대한 부분이다. 처음에 jwt에 관한 부분들을 검색해보면 많은 블로그나 정보들에서 jwt는 db에 저장하지 않는다. 저장하면 안된다. 라고 말하고 있었다. 그 이유는 여러가지가 있다. jwt는 무상태성을 강조하는데 db에 저장하는 순간 이 특성은 상실된다. 또한 토큰을 매번 검증할 때마다 DB에 접근해야하므로 세션과 마찬가지로 서버의 부담이 증가된다. 거기다가 어차피 jwt에는 중요한 정보가 들어가지 않는다. 그에 따라서 어차피 토큰의 정보를 활용하는 과정에 있어서 우리는 데이터베이스에 접근해야한다. 그렇다면 데이터베이스에 여러번 접근하는 과정 자체가 굉장히 비효율적이다. 마지막으로 보안성에 대한 부분이 많이 언급되는걸 보았는데, 이 부분은 의아하게 느껴졌다. DB에 저장된 토큰이 탈취되면 보안적으로 위험하다는 내용이었는데, 어차피 DB에 그대로 회원에 대한 정보들이 저장되어 있는데, 굳이 토큰을 탈취할 필요도 없고 오히려 DB에 저장된다면 2중 보안으로 검증하는 느낌을 받아서 반대의 내용이여야하지 않느냐를 느꼈다.
그래서 프로젝트를 진행할 때 DB에 토큰을 저장하는 사람들의 말을 들어보면, 이 내용이 정확한 내용일지는 모르겠지만 대강은 이런 것 같다. DB에 토큰을 저장하면 아까 말대로 2중 보안이다. 토큰을 만드는 과정을 본다면 생각보다 탈취하기 쉬운 구조로 되어있다. 우리는 어떤 서비스나 직접 문자열을 인코딩하여 토큰을 위조할 수 있다. 하지만 DB에 토큰을 저장하여 2중으로 확인한다면 이에 대한 부분이 조금 더 보안이 강화되는 것으로 보인다. 이러한 점보다 전자의 장점이 더 크게 느껴져서 jwt를 서버에 저장하지 않고 검증하는 방식으로 진행하였다.
구현 방법
먼저 아까 flask에서 제공하는 flask-jwt를 이용하지 않는다고 하였다. 따라서 그에 따라 파이썬에서 제공하는 모듈인 jwt를 사용하였다. 이 모듈을 이용하려면 pip install PYJWT를 설치하면 된다. 본 프로젝트에서는 모듈화를 이용하여 다른 파이썬 파일에 jwt를 컨트롤 하는 코드들을 나누어 놓았다. 그러나 login을 담당하는 부분에 class를 활용하여 코드들을 작성해놓아도 효용성이 높아보인다.
먼저 기본적인 정보들을 설정한다.
#jwt 구현 메소드
access_expires = timedelta(hours=1) # 토큰의 만료 시간 = 1시간
refresh_expires = timedelta(days=7) # 토큰의 만료 시간 = 7일
access_type = 'access'
refresh_type = 'refresh'
def encode_token(유저ID, expires, token_type) :
# 현재 시간을 UTC 기준으로 얻기
current_utc_time = datetime.utcnow()
expiration_utc_time = current_utc_time + expires
# UTC 시간을 문자열로 변환
formatted_utc_time = current_utc_time.isoformat()
# UTC 시간을 문자열로 변환
formatted_exp_utc_time = expiration_utc_time.isoformat()
payload = {
"문자열" : 발급기간과 유저의 ID를 합친 문자열,
"expir" : formatted_exp_utc_time, # 만료기간
"type" : token_type #토큰 type
}
encoded_token = jwt.encode(payload, "비밀번호 키 문자열", algorithm="암호화 방식(알고리즘)")
return encoded_token
보안을 위해서 ID를 그대로 저장하지 않고, 어떤 특정한 방식을 이용하여 유저의 ID와 발급기간을 합친 문자열을 생성한다. 이 방법은 개인의 자유대로 편한 방식으로 하면 된다. python의 jwt 모듈에서는 jwt.encode라는 메소드를 제공하고 있기 때문에 해당하는 방식으로 인코딩을 진행한다면 jwt 토큰을 만들 수 있다.
def decode_token(encoded_token) :
try :
decoded_token = jwt.decode(encoded_token, "비밀번호 키 문자열", algorithms=["암호화 방식(알고리즘)"])
except DecodeError as e:
# 토큰 디코드 오류 처리
print(f"토큰 디코드 오류: {e}")
return None
except InvalidSignatureError as e:
# 서명이 올바르지 않은 경우 처리
print(f"서명 오류: {e}")
return None
except InvalidAlgorithmError as e:
# 알고리즘이 허용되지 않는 경우 처리
print(f"알고리즘 오류: {e}")
return None
return decoded_token
from jwt.exceptions import DecodeError, InvalidSignatureError, InvalidAlgorithmError 를 import하여 예외처리를 진행한다.
이 부분은 토큰을 다시 디코딩하는 메소드이다. try/except를 이용하여 예외처리를 해주었는데 그 이유는 다음과 같다. 아까 말한대로 토큰은 우리가 편하게 만들 수 있다. 그러나 만약에 예외처리가 되지 않는다면, 임의로 누군가 이상한 토큰을 만들어 서버에 전송한다면 서버의 프로그램은 오류로 인하여 중단될 것이다. 혹은 잘못된 알고리즘을 통해서 만든 토큰일수도 있으며 비밀번호 키 문자열이 잘못된 내용으로 인코딩된 토큰일수도 있다. 이에 대한 부분을 해결하는 부분이 이 예외처리이다. 그러나 실제로 프로그램을 돌려보았을 때는 InvalidSignatureError에 대한 부분이나 InvalidAlgorithmError에 대한 부분은 거의 보지 못했다. 그러나 이러한 부분에 대한 고려가 필요한 것으로 보여진다.
그리고 마지막으로 아까 말했듯이 토큰을 검증하는 방식은 각 프로젝트의 목적성에 맞게 구현하면 된다. 일반적인 방법이란 것은 있지만, 정답이란 것은 없다고 느껴졌다. 본 프로젝트에서는 다음과 같이 구현하였다.
1. 로그인한 경우 : 엑세스 토큰 / 리프레시 토큰 발급
2. 엑세스 토큰 검증 : 엑세스 토큰이 유효한 토큰인지에 대한 것만 검증
3. 리프레시 토큰 검증 : 리프레시 토큰이 유효하다면, 엑세스 토큰 / 리프레시 토큰 발급
또한 이 토큰에서 비로그인 유저들에 대한 조회수를 구분하기 위하여 임시토큰 발급이란 것을 이용하였다. 이 토큰은 검증하는 것이 불가능하다. 그러나 굉장히 긴 유효기간을 지니고 있고, 그에 따라 유저들의 조회수에 대한 구분을 해주는 것이 용이하였다.
다음은 파이썬에서 랜덤 문자열을 생성해주는 코드이다. import random을 통해서 random 모듈을 불러와주면 다음과같이 랜덤 문자열을 만들 수 있고 이것을 임시토큰을 발급하는 데 이용하였다.
def generate_random_string(length):
alphabet = string.ascii_letters + string.digits
random_string = ''.join(random.choice(alphabet) for _ in range(length))
return random_string