웹서비스를 진행할 때 인증(Authentication)과 인가(Authorization)를 해줘야 하는 상황이 발생한다. 특히 비밀번호를 이용해서 인증과 인가를 해주는데 이 비밀번호가 database에 간단한 str 형태로 존재한다면, 보안상의 문제가 발생할 것이다. 이때 우리는 파이썬에서 제공하는 라이브러리 단방향으로 암호화해주는 bcrypt를 사용하게 된다.
Bcrypt에는 크게 3가지의 단계로 보안을 유지한다.
- hahsing : 원본의 의미를 알 수 없게 섞어놓는 방법
- salting : 실제 비밀번호에 랜덤 값을 더해서 해시값을 계산하는 방법
- key stretching : 해쉬 값을 여러번 반복해서 해시하는 방법
Bcrypt에서 암호화할 때는 str 형태가 아니라 bytes 데이터를 이용하게 되는데 암호화할 때 암호화할 내용을 bytes 화해줘야 한다. 현재 우리는 파이썬을 이용하고 있기 때문에 파이썬 기준으로 설명하면 str을 bytes 화하는 것을 encode라 하고 반대로 bytes를 str 화하는 것을 decode라 한다.
기존에 작성한 login과 sign view에 Bcrypt를 이용해 password를 암호화해준다.
from django.http.response import JsonResponse
from django.views import View
from jwt import algorithms
from .models import *
import re, bcrypt, jwt, json
from my_settings import SECRET_KEY
class UserView(View):
def post(self, request):
try:
data = json.loads(request.body)
print(data)
Email = re.compile("^[a-zA-Z0-9+-_.]+@[a-zA-z0-9-]+\.[a-zA-z0-9-]+$")
# PW = re.compile("^(?=.*[A-Za-z])(?=.*\d)(?=.*[~!@#$%^&*()+|=])[A-Za-z\d~!@#$%^&*()+|=]{8,16}$")
PW = re.compile("[0-9]{5,20}")
encoded_pw = bcrypt.hashpw(data['password'].encode('utf-8'), bcrypt.gensalt())
if PW.match(data['password']) is None or Email.match(data['email']) is None:
return JsonResponse({'message':' INVALID_FORMAT'}, status=400)
if User.objects.filter(email = data["email"]).exists():
return JsonResponse({"message": "OVERLAP"}, status=400)
User.objects.create(
name = data['name'],
email = data['email'],
password = encoded_pw.decode('utf-8'),
phone_number = data.get('phone_number'),
web_site = data.get('website'),
nick_name = data.get('nick_name'),
address = data.get('address'),
introduce = data.get('introduce')
)
return JsonResponse({'message': "SUCCESS"}, status=201)
except KeyError:
return JsonResponse({"message": "KEY_ERROR"}, status=400)
class LoginView(View):
def post(self, request):
try:
data = json.loads(request.body)
if not User.objects.filter(email=data['email']).exists():
return JsonResponse({"message": "INVALID_USER"}, status=401)
user = User.objects.get(email=data['email'])
if not bcrypt.checkpw(data['password'].encode('utf-8'), user.password.encode('utf-8')):
return JsonResponse({"message": "UNAUTHORIZED"}, status=401)
return JsonResponse({'message': "SUCCESS"}, status=200)
except KeyError:
return JsonResponse({"message": "KEY_ERROR"}, status=400)
일반적으로 사용자가 서비스에 로그인을 진행했다면, 사용자는 다른 기능을 사용하기 위해서 다시 로그인을 해야 하는 번거로움 없이 다른 기능을 사용할 수 있게 된다. 그때마다 사용자는 로그인과 함께 발행된 토근을 무상 태인 서버에 전달해서 번거로움 없이 기능을 사용할 수 있다. 이때 사용되는 토큰을 JWT(Json Web Token)이며, 사용자의 속성 정보를 JSON 형태로 갖는 Claim 기반의 Web token이다.
서버를 확장시키는 것 뿐 아니라 로그인 정보가 사용되는 분야를 확장할 수 있다.
더이상 쿠키를 보내지 않으므로 보안상으로 더 강력해 진다.
JWT는 Header, Payload, Signature 세 부분으로 나뉘어 진다.
-Header : alg와 tpy로 구성되어 있으며, alg는 사용될 알고리즘, typ는 토큰의 타입이 지정된다.
-Payload : Payload 부분에는 토큰에 담을 정보를 담는다. 정보의 한 조각을 클레임(Claim)이라고 부르고 key:value 쌍으로 존재한다. 클레임은 크게 세 분류로 나누어져 있다.
1) 등록된 클레임 : iss, sub, aud, exp 등이 있으며 서비스에 필요한 정보가 아닌 토큰에 대한 정보를 담기 위해 이미 정해진 클레임들이다.
2) 공개 클레임 : 공개용 정보를 위해 사용되며, 충돌 방지를 위해 URI 포맷을 이용한다.
3) 비공개 클레임 : 보통 클라이언트와 서버 간의 협의하에 사용되는 클레임 이름들이다. 공개 클레임과 달리 이름이 중복될 수 있으니 사용에 유의해야 한다.
-Signature : JWT의 마지막 부분이며, BASE64로 인코딩하고, 인코딩한 값을 비밀 키를 이용해 헤더(Header)에서 정의한 알고리즘으로 해싱을 하고, 이 값을 다시 BASE64로 인코딩하여 생성한다.
import jwt
token = jwt.encode({'id': user.id}, SECRET_KEY, algorithm='HS256')