AJAX와 flask_jwt_extended로 로그인 유지하기

KimCookieYa·2023년 4월 7일
0

버그 및 에러

목록 보기
2/4

문제 상황


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 라이브러리를 썼으면 좀 더 손쉽게 구현했지 않았을까 하는 아쉬움이 남는다.

profile
무엇이 나를 살아있게 만드는가

0개의 댓글