이 유저가 어떤 유저인지 식별하는 작업
암호화한 사람(ex. 개발자)도 복호화 할 수 없도록 하기
하나의 input(원본 메시지) 당 하나의 output(암호화된 메시지: Digest)이 있는 함수
output으로 input을 유추할 수가 없음 > 단방향
해커가 임의의 값을 입력하면서 비밀번호를 알아낼 가능성이 있는데, 이런 점을 보완하기 위해 아래 방법들이 있다.
랜덤한 문자열을 추가로 붙여서 해시값을 계산하는, 일명 소금 치는 작업
해시값 계산 후, 솔팅 후 또 해시, 솔팅 후 또 해시 이렇게 계속 반복하는 방법.
일명 쭉쭉 늘려주는 것(stretching)
비밀번호를 단방향 암호화 하기 위해 만들어진 해쉬함수
python에서 라이브러리로 제공된다. (다양한 언어에서 사용 가능)
왜 Bcrypt를 사용할까
- 해쉬 함수 중 SHA(Secure Hash Algorithm)는 GPU를 이용한 연산속도가 매우 빠르기 때문에 Password 암호화에 권장되지 않는다 > 해싱속도가 빨라서 rainbow table도 빨리 만들 수 있다.
- Bcrypt의 설계 기반인 Blowfish는 salting, key stretching과 같은 전처리 작업을 통해 작업량(=해싱시간)을 조절할 수 있다.(늘릴 수 있다)
import bcrypt
data = json.loads(request.body)
# 암호를 해쉬 후 회원가입
password = bcrypt.hashpw(data['password'].encode('utf-8'), bcrypt.gensalt())
User.objects.create(
email = data['email'],
password = hashed_password.decode('utf-8'),
phone = data['phone'],
nickname = data['nickname']
)
# db에 저장되어 있는 암호와 입력한 암호를 비교하여, 일치하면 로그인
if not bcrypt.checkpw(data['password'].encode('utf-8'), user.password.encode('utf-8')):
return JsonResponse({'message': 'INVALID_USER'}, status=401)
해쉬한 암호를 db에 저장할 때는, 다시 decode해서 저장한다.
User 모델에서 password를 CharField로 정의했고, 해쉬한 암호(byte 타입)를 그대로 db에 넣으면 byte 포맷의 암호가 그대로 str 타입으로 저장된다.
>>> password = '12345678'
>>> hashed_password = bcrypt.hashpw(data['password'].encode('utf-8'), bcrypt.gensalt())
>>> hashed_password
b'$2b$12$vDmpMPnXnSvyFCnpzUaLSOA11WCtDoBgElDkD85/fWAbEdosXYXb6'
# decode하지 않고 해쉬한 암호 그대로 db에 저장하는 경우
>>> User.objects.create(
email='ohoh@ohoh.com', password=hashed_password, phone='01077777777', nickname='ohoh')
3번 user의 password는 decode하여 저장한 값, 4번 user의 password는 decode하지 않고 저장한 값이다.

>>> User.objects.get(id=4).password
"b'$2b$12$vDmpMPnXnSvyFCnpzUaLSOA11WCtDoBgElDkD85/fWAbEdosXYXb6'"
>>> type(User.objects.get(id=4).password)
<class 'str'>
이렇게 포맷은 byte 타입의 포맷을 하고 있지만, db에 저장될 때 str 타입으로 저장이 되었고
db에서 다시 불러올 때도 str 타입으로 불러온다.
>>> p = User.objects.get(id=4).password # str타입
>>> p.encode()
b"b'$2b$12$vDmpMPnXnSvyFCnpzUaLSOA11WCtDoBgElDkD85/fWAbEdosXYXb6'"
db에서 불러온 byte타입의 포맷을 한 str타입의 암호를 encode 해본 결과,
b"b'$2b$12$vDmpMPnXnSvyFCnpzUaLSOA11WCtDoBgElDkD85/fWAbEdosXYXb6'" 이런 값이 되어
bcrypt.checkpw로 암호 값을 비교할 수 없게 된다.
그래서 꼭 db에 저장할 때부터 decode한 값(str 타입)을 저장하고
bcrypt.checkpw를 이용할 때, 그 str 타입의 암호를 불러와 encode 한 후 사용한다.
로그인 시, bcrypt.checkpw의 작동 방법
해시되어 저장된 암호를 복호화하는 것이 아니라, (단방향이라 복호화가 불가능하다)
이전에 암호를 암호화할 때 붙였던 salt와 같은 salt를 입력된 암호에 붙여 해쉬한 값과 db에 저장된 암호를 비교한다.
access token이라고 하는 암호화된 유저 정보를 첨부하여 request를 보낸다.POST /auth HTTP/1.1
Host: localhost:5000
Content-Type: application/json
{
"username": "joe",
"password": "pass"
}
HTTP/1.1 200 OK
Content-Type: application/json
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGl0eSI6MSwiaWF0IjoxNDQ0OTE3NjQwLCJuYmYiOjE0NDQ5MTc2NDAsImV4cCI6MTQ0NDkxNzk0MH0.KPmI6WSjRjlpzecPvs3q_T3cJQvAgJvaQAPtk1abC_E"
}
access_token = jwt.encode({'user': user.id}, my_settings.SECRET_KEY, my_settings.ALGORITHM)
{'user': user.id}: 데이터의 내용과 관련 없는, user를 식별하기 위한 id를 사용한다.
(만에 하나, 유출된다 해도 전혀 의미가 없는 값)
SECRET_KEY: 장고 프로젝트 자체의 SECRET_KEY를 사용한다.
ALGORITHM: 이 프로젝트에서는 'HS256'을 사용했고, 사용한 알고리즘의 종류는 외부로 공개되면 안되는 정보이기 때문에 숨겨둔다.
위 토큰을 복호화하면 아래와 같은 정보를 얻는다.
{
user = 1 # user.id = 1
}
로그인한 사람이 token을 이용해 내가 그 유저라는걸 인증하고 허가 받는 것
유저 권한이 필요한 request의 경우, access token을 포함한 request를 받고 그 유저인지 확인하는 작업을 한다.
이를 위해, user_access decorator를 사용한다.
def user_access(func):
def wrapper_func(self, request, *args, **kwargs):
try:
access_token = request.headers['Authorization']
payload = jwt.decode(access_token, SECRET_KEY, ALGORITHM)
request.user = User.objects.get(id=payload['user'])
except jwt.exceptions.DecodeError:
return JsonResponse({'message': 'INVALID_TOKEN'}, status=400)
except User.DoesNotExist:
return JsonResponse({'message': 'INVALID_USER'}, status=400)
return func(self, request, *args, **kwargs)
return wrapper_func
받은 request의 header에서 토큰을 받아오고,
encode 시 사용했던 SECRET_KEY와 ALGORITHM를 넣어 decode 하고 payload를 얻는다.
payload의 user 값(id)을 가진 db의 유저 데이터와 request의 유저가 일치하면 인가를 받는다.
-> 다음 동작으로 넘어간다.
class CommentView(View):
@user_access # 유저 인증 데코레이터
def post(self, request):
data = json.loads(request.body)
Comment.objects.create(
post = Posting.objects.get(id=data['post']),
user = request.user,
text = data['text']
)
return JsonResponse({'message': 'SUCCESS'}, status=201)