정글에 입소하자마자 부여된 과제는 3박 4일간 미니 프로젝트를 만드는 것이었다.
그 결과 완성된 우리 팀의 사이트 😊
믈론 완성된 사이트를 보여주는 것이 이번 포스팅의 목적은 아니다.
첫 주차 과제의 요건은 아래와 같았는데, 이 요건에 따라 구현하다보니 좀 더 공부하고 싶은 키워드들이 몇 가지 생겼다.
이번 포스팅에서는 그 중에서 제일 궁금했던 회원가입, 로그인 기능에 대해 먼저 살펴보고자 한다.
구체적으로 세션을 통한 인증, JWT를 통한 인증, 비밀번호의 암호화 등에 대해 공부해보았다.
필수 포함 사항
- 로그인 기능
- Jinja2 템플릿 엔진을 이용한 서버사이드 렌더링
더 고민해볼 키워드
- Bootstrap을 대체할 CSS 라이브러리 사용하기 (Bulma, Tailwind 등)
- JWT 인증 방식으로 로그인을 구현하기
세션은 많은 웹 애플리케이션에서 사용되는 오래된 인증 방식인데, 작동 방식을 정리하면 아래와 같다.
먼저 사용자가 로그인을 하면 서버는 세션 DB에 해당 사용자의 세션을 생성한다.
세션이 생성되면 서버는 해당 세션의 고유한 ID를 쿠키에 포함하여 브라우저에 전달한다.
그러면 브라우저는 해당 세션 ID를 쿠키에 저장해두고, 이후 요청 헤더에 쿠키를 담아서 보낸다.
브라우저로부터 요청을 들어오면 서버는 쿠키에 포함된 세션 ID가 세션 DB에 있는지 살펴보고, 해당되는 데이터가 있으면 인가를 한다.
JWT는 JSON Web Token의 약자인데, 말 그대로 사용자의 권한 관련 정보를 JSON 형태로 표현한 토큰이다.
Flask에서는 아래와 같이 flask_jwt_extended
라이브러리의 create_access_token
함수를 사용해서 JWT 토큰을 생성할 수 있다.
identity
에는 사용자의 고유한 식별자, expires_delta
에는 만료 시간을 넣어주면 된다.
이렇게 생성된 JWT 토큰은 세션과 유사한 방식으로 브라우저의 쿠키에 저장해두고 사용하면 된다.
# 로그인
@app.route("/signin", methods=["POST"])
def signin():
user_id = request.form["user_id"]
password = request.form["password"]
found_user = db.users.find_one({"user_id": user_id})
if found_user is None:
return jsonify({"result": "fail", "message": "가입되지 않은 아이디입니다 😢"})
if bcrypt.check_password_hash(found_user["password"], password):
expires_delta = timedelta(hours=1)
access_token = create_access_token(identity=user_id, expires_delta=expires_delta)
return jsonify({"result": "success", "token": access_token})
else:
return jsonify({"result": "fail", "message": "잘못된 비밀번호입니다 😢"})
JWT 공식 홈페이지에 들어가면 JWT 토큰을 decode해볼 수 있다.
쿠키에 저장된 JWT 토큰을 복사해서 decode해보면 아래와 같이 Header, Payload, Signature 부분으로 구성된 것을 확인할 수 있다.
이 토큰에는 사용자의 식별자, 토큰의 만료시간 등이 포함되어 있다.
그렇다면 JWT 토큰을 통한 인가(Authorization)는 어떻게 진행될까?
접근이 제한된 리소스의 경우 브라우저는 Authorization 헤더에 Bearer 스키마로 JWT 토큰을 포함하여 요청을 보내야 한다.
Authorization: Bearer <jwt-token>
그러면 서버는 JWT Token을 해독하여 사용자 식별자와 만료시간 등을 살펴보고 유효한 토큰인 경우 접근을 허용한다.
Flask의 경우 접근 권한 관리가 필요한 API에 @jwt_required
를 붙여주면 된다.
그렇다면 Session과 JWT 토큰 중에 무엇을 사용하는 것이 좋을까?
뻔한 말이지만 각각의 장단점을 고려해서 적절한 방법을 선택하는 수밖에 없을 것 같다.
먼저 세션의 경우 로그인된 유저들의 정보를 모두 저장하기 때문에 추가적인 기능을 구현할 수 있는 여지가 있다.
예를 들어 로그인된 디바이스 목록을 보여주고 선택된 디바이스를 강제 로그아웃 시키는 기능, 넷플릭스처럼 계정 공유 숫자를 제한하는 기능 등의 구현이 가능할 것이다.
하지만 세션의 경우 요청이 들어올 때마다 세션 DB를 조회해야 하기 때문에 유저가 늘어날수록 필요한 DB 리소스도 증가한다는 단점이 있다.
반면 JWT의 경우 별도의 DB를 구축할 필요가 없기 때문에 비용을 절약할 수 있다는 장점이 있다.
하지만 JWT는 발급된 토큰들을 별도로 관리하지 않기 때문에 아직 만료시간이 지나지 않은 토큰이 유출되었을 때 이를 무효화하지 못한다는 단점이 있다.
따라서 기능 요구 사항, 보안 요구 사항 등을 고려해서 적절한 인증 방식을 선택하는 것이 필요하다.
이번 미니 프로젝트에서 아쉬웠던 점은 우리 사이트에 https를 적용하지 못한 것이었다.
https를 적용하지 못한 것이 아쉬웠던 이유는 회원가입 기능과 관련 있다.
회원가입에서 입력된 비밀번호는 일반적으로 보안을 위해 해시함수를 사용하여 암호화하고 저장한다.
우리 팀의 경우 아래와 같이 bcrypt
라이브러리를 통해 Flask 서버에서 비밀번호를 암호화하고 DB에 저장했다.
# 비밀번호 암호화
password_hash = bcrypt.generate_password_hash(password).decode("utf-8")
그런데 https를 적용하지 않은 상태로 서버에서 암호화를 진행하면 보안과 관련하여 큰 취약점이 생긴다.
https를 적용하지 않으면 데이터가 암호화되지 않은 상태로 비밀번호가 서버에 전송되기 때문에, 만약 네트워크 스니핑이 일어나는 경우에 비밀번호가 유출될 위험성이 있다.
조만간 https를 웹사이트에 적용하는 법에 대해 공부하고 보완해야겠다 💪
정글 Week00 개발일지 끝!!