flask 기반 웹 프로젝트를 진행하다 해당 문제가 생겼다. 로그인 기능을 구현하며 페이지를 옮겨갈 때마다 쿠키에 저장한 jwt(json web token)을 읽어서 로그인 여부를 확인하려고 했는데, 이때 flask_jwt_extended 라이브러리에서 제공하는 데코레이터 함수 flask_jwt_extended.jwt_required()를 사용하는데 문제가 생겼다.
@app.route("/protected")
@jwt_required()
def protected():
current_user_id = get_jwt_identity()
return jsonify(logged_in_as=current_user_id), 200
if __name__ == '__main__':
app.run('0.0.0.0', port=5000, debug=True)
위 코드처럼 @jwt_required()를 사용하면 protected() 함수를 호출하기 위해선 유효한 jwt가 요구된다. 또한 get_jwt_identity() 함수는 jwt에 담긴 사용자 identity값을 리턴하는데, 이 함수를 호출하기 위해선 @jwt_required() 함수가 반드시 호출되어야만 한다.
따라서 클라이언트가 '/protected' api를 호출할 때 헤더에 jwt를 담으면 정상적으로 함수를 동작시킬 수 있다.
jwt 발급은 create_access_token()를 사용한다.
access_token = create_access_token(identity=user_id)
그러나 ajax에서 헤더에 값을 추가하여 전송하기 위해선 beforeSend 부분을 추가해야 한다.
function to_ajax(){
$.ajax({
type : 'get',
url : '/test,
dataType : 'xml',
beforeSend : function(xhr){
xhr.setRequestHeader("Authorization","JWT " + token);
},
error: function(xhr, status, error){
alert(error);
}
success : function(res){
alert(res)
},
});
}
그러나 필자는 쿠키가 아닌 웹브라우저 상의 변수 token에 jwt를 저장하는 것은 보안 이슈가 있을 것이라 판단했고, 따라서 ajax 헤더에 값을 추가하는 것 말고 다른 방법을 찾기로 했다.
로그인한 사용자를 식별하기 위해서는 쿠키에 저장된 jwt에 담긴 사용자 정보를 가져와야 한다. 그러나 flask_jwt_extended의 get_jwt_indentity()는 @jwt_required()를 사용해야만 한다. @jwt_required()를 실행하기 위해서는 Ajax 헤더에 jwt를 담아 전송해야 한다.
필자가 선택한 방법은 flask_jwt_extended.decode_token()이다. 쿠키에 담긴 jwt를 decode하여 데이터를 json형식으로 리턴한다. decode_token() 함수는 @jwt_required()를 요구하지 않는다.
또한, 로그아웃 시 해당 토큰의 식별자 jti를 jwt_blocklist에 추가하여 해당 토큰의 재사용을 막는다.
jwt_blocklist = set()
@app.route("/main", methods=['GET'])
def show_main():
jwt_token = request.cookies.get('access_token')
if jwt_token is None:
return redirect(LOCALHOST+'/'), 400
try:
# token decode 후 로그아웃여부 확인 위해 jti 저장, user 정보 저장
jti = decode_token(jwt_token)['jti']
user_id = decode_token(jwt_token).get(IDENTITY, None)
except ExpiredSignatureError:
# 쿠키 시간 만료의 경우, 로그인 페이지로
return redirect(LOCALHOST+'/'), 400
#logout된 token의 경우 login페이지 rediect
logoutCheck = jti in jwt_blocklist
if logoutCheck:
return redirect(LOCALHOST+'/'), 400
return render_template('main.html')
# 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'])
def logout_proc():
jwt_token = request.cookies.get('access_token')
jti = decode_token(jwt_token)['jti']
jwt_blocklist.add(jti) # 로그인 user의 jti를 blocklist에 등록
return jsonify({'result': 'success', 'msg': '로그아웃 성공!'})
프로젝트 마감 시간이 얼마 남지않아 요상한 방법으로 구현했는데, pyjwt 라이브러리를 썼으면 좀 더 손쉽게 구현했지 않았을까 하는 아쉬움이 남는다.