회원가입과 로그인은 성공적으로 마쳤지만 여기에는 문제가 있었다.
로그인을 할 때 http request에 이메일과 비밀번호를 적어서 보냈는데, 비밀번호가 암호화가 되어있지 않아 보안에 취약하다는 점이다. Database가 해킹되면 유저의 개인정보가 그대로 노출되고, 해킹되지 않더라도 개발자들이 볼 수 있다는 취약점이 있다.
이것을 방지하기 위해 인증과 인가라는 개념을 회원가입과 로그인에 도입하는 내용을 배웠다.
해당 개념을 회원가입과 로그인에 실제로 적용하기 전에 간단하게 살펴보고 가겠다.
사용자의 id와 password를 확인하는 절차(identification)
access token
생성access token
을 첨부해 전송하면서매번 로그인 하지 않아도 됨다이제스트(digest)
를 생성단방향성(one-way)
라고 함단방향 해쉬 함수도 취약점이 있는데, 해시를 계산해 놓은 값을 테이블에 넣고 비교를 하면 시간이 걸리더라도 원본 비밀번호를 구할 수 있다.
이를 막기 위해 salting
과 key strecting
이라는 두가지 방법이 고안되었다.
salting
실제 비밀번호 이외에 추가적으로 랜덤 데이터를 더해서 해시값을 계산
Key Stretching
단방향 해시값을 계산 한 뒤 그 해시값을 또 해쉬하고 이를 반복함, Salting
을 여러번 한 것이라고 볼 수 있음
그렇다면 로그인 후 발급되는 access token
은 어떤 방식으로 동작하는 것일까?
발급되는 토큰에는 여러 가지가 있지만 그 중 JWT(JSON Web Token)
에 대해 알아보겠다.
JWT는 유저 정보 데이터를 담은 JSON 데이터를 암호화해서 클라이언트와 서버간에 주고 받는 것이다.
유저가 로그인에 성공하게 되면 access token
을 발급받는데, 실제 스크립트는 다음과 같다.
POST /auth HTTP/1.1
Host: localhost:5000
Content-Type: application/json
{
"username": "joe",
"password": "pass"
}
access token
발급HTTP/1.1 200 OK
Content-Type: application/json
{
"access_token" : "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGl0eSI6MSwiaWF0IjoxNDQ0OTE3NjQwLCJuYmYiOjE0NDQ5MTc2NDAsImV4cCI6MTQ0NDkxNzk0MH0.KPmI6WSjRjlpzecPvs3q_T3cJQvAgJvaQAPtk1abC_E"
}
서버에서는 받은 access token
을 복호화하여 해당 유저 정보를 알게 된다.
서버는 유저가 누군 지 알게 되었기 때문에 해당 유저가 매번 로그인을 하지 않아도 되는 것이다.
인가는 유저가 요청하는 request를 실행할 수 있는 권한이 있는 유저인가를 확인하는 절차다.
즉 요청에 대해 접근 권한을 식별하는 것이라고 볼 수 있다.
인가 또한 JWT를 통해서 구현될 수 있다.
Authentication 절차를 통해 access token
을 생성한다. access token
에는 유저 정보를 확인할 수 있는 정보가 들어가 있어야 한다 (예를 들어 user id).
유저가 request를 보낼때 access token
을 첨부해서 보낸다.
서버에서는 유저가 보낸 access token
을 복호화 한다.
복호화된 데이터를 통해 user id를 얻는다.
user id를 사용해서 database에서 해당 유저의 권한(permission)을 확인한다.
유저가 충분한 권한을 가지고 있으면 해당 요청을 처리한다.
유저가 권한을 가지고 있지 않으면 Unauthorized Response(401) 혹은 다른 에러 코드를 보낸다.
이제 인증과 인가의 개념을 확인했으니 실제 코드에 적용해 보겠다.
암호화를 하기 위해 bcrypt 패키지를 설치한다. 패키지가 추가되었으니 requirements.txt도 수정한다.
pip install bcrypt
설치가 완료되었다면 이제는 users/views.py에 적용된 회원가입과 로그인 절차를 인증/인가를 도입하여 수정한다.
# users/views.py
import json, re, bcrypt
from django.views import View
from django.http import JsonResponse
/......
password = data['password']
phone_number = data['phone_number']
hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()) [1]
User.objects.create(
name = data["name"],
email = email,
password = hashed_password.decode('utf-8'), [2]
phone_number = phone_number
)
return JsonResponse({"message" : "SUCCESS"}, status = 201)
[1] : 입력한 비밀번호를 bcrypt
의 hashpw()
메서드를 이용해 해시한다.
hashpw()
메서드는 byte형 자료형만 받으므로, str형인 비밀번호를 인코딩해주고, gensalt()
메서드를 이용해 salting
하여 저장한다.
[2] : POST 메서드를 이용하여 데이터 테이블에 저장할 때 해시된 비밀번호로 저장한다. hashed_password
는 byte형 자료형이므로, 테이블에 str형 자료형으로 저장하기 위해 디코딩을 해준다.
이전까지는 암호화가 되지 않고 저장되었던 비밀번호가 해싱을 통하여 다르게 저장된 것을 확인할 수 있다.
이번에는 로그인 시 JWT Token
을 발급하는 것을 실제 코드에 적용해보겠다.
django에서 JWT 토큰 발급을 할 때 에는 PyJWT라는 패키지가 필요하므로 설치하겠다. requirements도 수정한다.
pip install PyJWT
# users/views.py
import json, re, bcrypt, jwt [1]
from django.views import View
from django.http import JsonResponse
from users.models import User
from my_settings import SECRET_KEY, ALGORITHM [2]
# users/views.py
/.....
class SignInView(View):
def post(self,request):
try:
data = json.loads(request.body)
email = data['email']
password = data['password']
user = User.objects.get(email = email) [4]
if not bcrypt.checkpw(password.encode('utf-8'), user.password.encode('utf-8')): [5]
return JsonResponse({"message" : "INVALID_PASSWORD"}, status = 401)
access_token = jwt.encode({'user_id' : user.id}, SECRET_KEY, ALGORITHM) [6]
return JsonResponse({"message" : "LOGIN SUCCESS" , "JWT" : access_token}, status = 201)
except KeyError as e:
return JsonResponse({"message" : f"KEY_ERROR : {e.args[0].upper()}"}, status = 400)
except User.DoesNotExist:
return JsonResponse({"message" : "INVALID_USER"}, status = 401)
hashpw()
와 동일하게 checkpw()
또한 byte형 자료형만 받으므로, 입력한 비밀번호를 인코딩해주고, str형으로 저장된 비밀번호 또한 인코딩해줌. 두 입력값이 일치하면 True가 반환되므로 일치하지 않을 시 에러 메세지 반환http 요청을 보면 enigma123@
이라는 값은 실제 데이터 테이블에 저장된 비밀번호와 일치하지 않는다. 그렇지만 로그인 과정에서 복호화 과정을 거쳐서 로그인에 성공했다는 메시지를 반환받고, JWT가 발급된 것도 확인할 수 있다.