크래프톤 정글에 입소하고 바로 미니프로젝트를 진행하는데,
JWT 인증 방식으로 로그인 구현하는 부분이 있었다.
팀에서는 내가 해당 기능 구현을 맡기로 했다.
나에게는 기술적 챌린지었다.
해당 기능을 구현하기 위해서는
JWT 인증 방식이 무엇인지 알아야 했다.
-> 그래, 그러면 로그인한 유저가 누구인지 식별하려고 하는구나..
-> http 프로토콜은 연결 상태를 유지하지 않는 비 연결성이기 때문에, 웹 페이지가 새롭게 로딩되더라도 로그인한 유저가 누구인지 알기 위해 필요하다.
-> 그렇지 않다. 쿠키와 세션도 사용 할 수 있다.
-> 쿠키에는 key-value 형식의 문자열인데, 쿠키를 사용하여 로그인 유저를 식별한다면, 사용자의 id와 password 정보가 쿠키 저장하여 클라이언트가 요청을 보낼 때마다 저장된 쿠키를 해더에 cookie에 담아 서버에 보낸다. 그러면 쿠키 값 그대로 보내기 때문에 유출 및 조작 당할 위험이 있다. (추후 쿠키에 대해서 더 자세히 알아봐야겠다. 우선은 이래서 쿠키를 안쓴다는 것은 알았다.)
그리고 세션을 사용하여 유저를 식별한다면, 세션은 쿠키가 아니라 서버에 사용자의 id와 password 를 저장한다. 그래서 서버는 클라이언트의 로그인 요청이 오면, 해당 id 와 password 정보를 서버에 저장하고 사용자의 식별자ID를 담아 응답을 보낸다.
그리고 매번 클라이언트가 요청할 때마다 식별자 ID를 담아 같이 서버에 보내면, 서버는 일치하는 유저 정보를 가져와서 처음에 로그인한 유저가 맞는지 확인하고, 페이지가 이동될때마다 이 작업이 반복된다. 즉 사용자가 많아지면 서버의 과부화가 나타날 수 있다는 단점이 있다.
쿠키와 세션을 쓰지 않고 JWT를 사용하는 이유는 두 개의 단점을 보완할 수 있기 때문이다.
우선 로그인 시에 서버에서 해당 유저의 토큰을 만들어서 클라이언트로 보내준다.
그래서 직접적으로 id와 password를 전달하지 않게된다.
그 다음 클라이언트는 토큰을 보관하고 있다가, 요청을 보낼 때마다 헤더에 토큰을 실어서 서버에 보낸다. 그리고 서버는 토큰을 검증하여 응답해준다.
즉 세션처럼 DB조회를 하지 않아도 되기 때문에 서버가 DB 서버가 터져서 서버도 같이 죽는 경우를 피할 수 있다!
미니 프로젝트는 주어진 시간 내에 서비스가 돌아가야 한다. 그래서 사실 완벽한 개념을 이해하고 사용하기 보다는 처음엔 그냥 필요한 라이브러리 가져오고 이미 많이 배포된 예제들을 가져다가 복사하여 구현을 했다.
그리고.. 다시 미니프로젝트가 끝난 후, jwt에 대해서 다시 공부했는데, 맙소사 내가 구현한 기능들은.. jwt를 제대로 사용한게 아니었다!!
프로젝트 당시에는 access token과 refresh token을 쿠키에 저장하고,
DB에는 refresh token을 저장했다.
그리고 유저가 식별되어야 하는 api를 실행할 때, 그냥 쿠키에 access token이 있는지 없는지 여부만 확인하고, 디비에서 유저의 데이터를 불러올때 쿠키에 저장 된 refresh token 을 가져와서 매칭되는 유저의 정보를 찾았다...
나는 flask 서버를 사용했고, jwt 토큰 발행을 위해서 flask_jwt_extended 라이브러리를 사용하여 토큰을 생성하고 지우고 했다.
그래서 실제로는 클라이언트에서 쿠키에 access token 정보를 요청과 함께 보내주면,
get_jwt_identity()
flaskjwt_extended의 내장함수 get_jwt_identity(identity="사용자 입력"_)를 사용하여, 반환된 identity로 디비 유저 정보를 접근할 수 있는 것이었다.
바보같이.. access token은 사용하지 않고 나는 refresh token 으로 유저 정보를 찾고 있었다..
사실 프로젝트 진행할 때에도 토큰을 발급할 때, identity를 설정하는게 있었는데.. 이게 어디서 쓰이는 거지? 하고 스쳐간 생각이 있었는데... 역시나..쓰이는 곳이 있었다..
우선 내가 구현했던 로그인 기능 구현 부분의 코드를 정리해보겠다.
@auth_bp.route('/login', methods=['POST'])
def login():
id = request.form['id']
password = request.form['password']
user = db.users.find_one({'id' : id})
if (user == None) or (user['password'] != password) :
flash('아이디와 패스워드가 일치하지 않습니다.')
return redirect("/")
else :
# DB에 access_token, refresh token 생성하기
access_token = create_access_token(identity=id)
refresh_token = create_refresh_token(identity=id)
# DB에 refresh token 저장하기
db.users.update_one({'id' : id},{'$set': {'token': refresh_token}})
# 로그인이 되면 다음에 연결될 웹페이지 파일을 response 에 담기
response = make_response(render_template('menu.html'))
# 쿠키에 access token, refresh token 저장하기
response.set_cookie('access_token', value=access_token)
response.set_cookie('refresh_token', value=refresh_token)
return response
그 다음 flask app 설정을 해주었다.
app.secret_key = "비밀~"
# 토큰이 저장될 위치를 설정
app.config["JWT_TOKEN_LOCATION"] = ["cookies"]
# CSRF 보호를 비활성화
# jwt 토큰을 사용하면 비교적 안전해서 비활성화 해도 된다고 함
app.config["JWT_COOKIE_CSRF_PROTECT"] = False
# 토큰을 확인할 필요가 있는 모든 라우트에서 쿠키가 전송되도록 설정하려고 "/" 설정
app.config["JWT_ACCESS_COOKIE_PATH"] = "/"
# 토큰을 불러올때, 쿠키에 저장된 이름 설정
app.config["JWT_ACCESS_COOKIE_NAME"] = "access_token"
그리고 토큰 검증이 필요한 api에
@jwt_required()
를 붙여주었다. 그리고 원래 쿠키에 저장했던 refresh token으로 DB에 일치한 유저들을 불러왔다면, 이제는
id = get_jwt_identity()
사용하여 유저 아이디를 불러와서 DB에서 필요한 데이터를 찾을 수 있게 되었다.
refresh 토큰은 access token 이 만료되었을 때, access token을 재발급해주기 위한 토큰이어서 DB에 저장해두었는데, 재발급 코드는 만들지 못했다.
곧 주말에 시간이나면 추가하여 수정해보도록 하겠다.
--- 추가로 jwt 의 구조 그리고 인증 방법에 대하여 더 공부했다.
링크는 https://velog.io/@minpic/JWT-구조-그리고-인증