[정글] WEEK00 - WIL : JWT, jinja2

WEEK00 - mini project

0주차에는 입학시험때 공부한 내용을 바탕으로 간단한 프로젝트를 진행했다.
그 중에서 로그인 구현 및 템플릿을 활용한 SSR(서버사이드 렌더링) 구현을 위해 새롭게 공부한 JWT와 jinja2에 대해 간략하게 WIL(Weekly I Learned)을 작성하려 한다.

JWT

사용자에게 서비스를 개인화하여 제공하기 위해서는 로그인/로그아웃 등의 회원관리 기능이 필요하다.
JWT는 이 로그인/로그아웃 기능을 구현하는 방법 중 하나이다.

회원의 로그인 상태를 유지하기 위해서는 일단 Client에서는 로그인 되었다는 정보를 가지고 있어야 하고 Server 또한 이 사용자가 로그인에 성공했다는 정보를 가지고 있어야 한다. 여기서 Client가 가지고 있어야하는 정보는 쿠키에 저장되며, Server가 가지고 있어야하는 정보는 세션으로 저장된다. 즉, 사용자가 로그인 정보(id/pw)를 입력하여 Server로 전달하면 Server는 database에 저장된 회원의 정보와 비교하여 로그인 정보의 유효성을 판단한 후, 유효하다면 해당 정보에 대한 세션을 생성하여 저장하며 Client로 이에 매칭되는 data를 전달한다. Client에서는 이 data를 browser의 쿠키에 저장했다가 Server에 요청을 보낼때마다 쿠키를 다시 전달하고 Server에서 이 쿠키와 세션을 확인하여 로그인상태를 확인하게 된다.

이 쿠키-세션 방식은 사이트의 서버가 1개로 동작할때는 문제가 없지만 사용자가 많아져 서버가 여러개가 되면 로그인 유지에 문제가 발생할 수 있다. A,B,C 서버가 있다고 가정했을때, A서버에서 로그인에 성공하여 세션이 A서버에 생성되었을 경우 이후에 B,C 서버로 접속한다면 세션이 없어 로그인을 다시 해야하는 경우가 생길 수 있는 것이다. 이를 해결하기 위한 여러가지 방법 중 하나가 JWT 인증 방식이다. (추가로 쿠키-세션 방식의 경우 해킹의 위험성도 있다고 한다.)

JWT 인증방식의 경우 로그인 성공시 Client에서 JWT token을 가지고 있게 되며 Server에서는 Secret key를 가지고 있으나 세션 등을 따로 저장하지는 않는다. 로그인 정보가 유효하면 Server에서는 Secret key를 이용하여 JWT token을 생성하고 Client에 전달하며, Client는 이 token을 저장한다. 이후 Server에 요청을 보낼때 token을 전달하면 Server에서 다시 Secret키를 가지고 token을 decode하여 유효성을 확인하게 된다.

Python에서 JWT 인증방식을 구현하기 위해 사용할 수 있는 라이브러리는 pyjwt와 flask_jwt_xtended 가 있는데, 필자는 후자를 사용했다. flask_jwt_extended의 기본적인 사용방법은 아래와 같다.
(세팅, 함수 인자 등 자세한 정보는 공식 Docs 참고)

# import
from flask import Flask
from flask_jwt_extended import *

# flask객체 생성
app = Flask(__name__)

# JWT 매니저 활성화
jwt = JWTManager(app)

# secret key 세팅
app.config.update(
    DEBUG=True,
    JWT_SECRET_KEY="JUNGLERSSPORTS",
)

# 로그인 api
@app.route('/login', methods=['POST'])
def user_login() :
    # 로그인 정보 유효성 확인 코드 생략
    # 로그인 정보 유효할 시 token 생성 후 client로 전달
    return jsonify(
                {"result": "success",
                "token": create_access_token(identity=userId, expires_delta= False)}
                
# 로그인이 필요한 api 접근시
@app.route('/register', methods=['POST'])
@jwt_required()
def register() :
    user = get_jwt_identity() # 로그인 상태 확인 후 user의 identity 정보 return
    

로그아웃 기능의 경우 쿠키에 저장된 JWT token을 지움으로써 구현하는 방법이 있고, 로그아웃 요청시 들어온 token을 blocklist에 등록하여 구현하는 방법이 있다. 아래는 blocklist에 등록하여 구현하는 코드이다.

# blocklist 생성. 중복 방지 위해 set 자료형 사용
jwt_blocklist = set()

# blocklist 기능 사용을 위한 세팅
@jwt.token_in_blocklist_loader
def check_if_token_is_revoked(jwt_header, jwt_payload) :
	jti = jwt_payload['jti']
	return jti in jwt_blocklist

# 로그아웃 처리 api
@app.route('/logout', methods=['GET'])
@jwt_required()
def user_logout() :
    jti = get_jwt()['jti'] 
    jwt_blocklist.add(jti) # 로그인 user의 jti를 blocklist에 등록

flask_jwt_extended 를 사용하여 로그인이 필요한 api에 적용할 경우 해당 api에 jwt_required 데코레이션을 적용하여 구현 가능했다. 이 경우에는 Client에서 api 요청을 보낼 때 header 부분에 Authorization으로 jwt token을 전달해주어야 한다(참고). 그런데 api로 요청하는 것이 아니라 page에 직접 접속하는 경우에는 header를 입력해 줄 방법이 없다. 공식문서에서는 jwt를 확인할 위치를 header가 아니라 쿠키로 설정해줄 수 있는 것으로 보이나 필자의 경우 적용되지 않았다. (문서를 잘못 이해했거나 쓰는 방법을 잘 모르는 걸수도...) 여기서 애를 많이 썼는데, 결국엔 함수를 추적해서 쿠키에서 token을 가져와 직접 decode 해주는 방식으로 해결했다. (사실 PyJWT를 사용했으면 발생하지 않았을 문제일수도 있다)

# import
from flask_jwt_extended.config import config
from jwt.exceptions import ExpiredSignatureError

@app.route('/mypage')
def show_mypage():
	# 쿠키에서 token 가져오기
    jwtToken = request.cookies.get('jwt-token')
    # token 없을경우 login페이지 rediect
    if jwtToken is None :
        return render_template('login.html')

	#token 만료된 경우 login페이지 rediect
    try:
        # token decode 후 로그아웃여부 확인 위해 jti 저장, user 정보 저장
        jti = decode_token(jwtToken)['jti']
        user = decode_token(jwtToken).get(config.identity_claim_key, None)
    except ExpiredSignatureError: 
        return render_template('login.html')
    
    #logout된 token의 경우 login페이지 rediect
    logoutCheck = jti in jwt_blocklist
    if logoutCheck :
        return render_template('login.html')

jinja2

웹페이지를 렌더링하는 방식에는 CSR(Client Side Rendering)과 SSR(Server Side Rendering)이 있다.

CSR은 페이지의 변경사항을 Client에서 처리하는 방식으로, 페이지의 정보를 바꾸기 위해서는 페이지를 새로 받아오거나 reload 하지 않고 api 요청으로 data를 받아오고 javascript로 페이지 내용을 변경해주는 SPA(Single Page Application) 방식이다. 이 방식은 페이지의 reload가 거의 발생하지 않아 화면의 번쩍거림 (사라졌다가 다시 나타나는 현상) 이 없어 화면이 빠릿하고 자연스러우나 처음에 한번에 코드를 받아와야한다는 단점이 있다.

반대로 SSR은 페이지의 변경사항을 Server에서 처리하는 방식으로, page를 바꾸기 위해서는 Server로 html 요청이 필요하며 Server에서는 페이지의 내용을 완성한 뒤 html로 내려주는 방식이다. 이는 한번에 코드를 받지 않아도 되어서 속도는 빠를 수 있으나 매번 페이지의 reload가 발생하여 화면이 번쩍거리게 된다.

jinja2는 python에서 SSR을 구현하는 방법 중의 하나이다. flask는 기본적으로 jinja2를 포함하고있어 따로 설치가 필요하지 않다. jinja2가 요구하는 방식으로 html을 작성하고 render_template() 으로 해당 html을 내려주면 된다.

jinja2 입력 방식 - 공식 Docs 참고
변수 : {{ 변수이름 }}
코드(if, for 등) : {% 코드 내용 %}

jinja2를 활용하면 하나의 html 템플릿으로 여러가지의 페이지를 쉽게 만들어줄 수 있어서 편리하다. 예를 들어 회원에 따라 개인화된 my page를 제공하려면 제공하려는 틀을 html로 작성하고 회원의 정보 및 관련 data를 jinja2로 내려주면 된다.

정리

간략하게 정리하려 했으나 쓰다보니 쓸데없이 길어진건 아닌가 싶다.
공식문서나 여러가지 구글링으로 쉽게 찾아볼 수 있는 부분들은 대부분 생략하려 하였고,
개인적으로 이해하기 어렵거나 찾기 어려웠던 부분들은 필자가 이해하거나 구현한 방식으로 자세히 작성해보았다.
이해한 내용을 바탕으로 작성하다보니 틀린부분이 있을지도 모르겠으나 누군가에게 작은 도움이 되었으면 좋겠다.

좋은 웹페이지 즐겨찾기