Dev/Django

Django 응용하기 Authentication & Authorization(인증&인가 - Bcrypt와 JWT)

sincerely10 2020. 7. 19. 16:23
반응형

지난 포스트에서는 Django의 기본으로 MTV model과 자주 사용되는 파일인 models.py, urls.py, views.py에 대해서 학습하였습니다.

이번 포스트에서는 이를 조금 더 응용한 Authentication & Authorization(인증&인가)에 대해서 하나씩 확인해보겠습니다. 인증과 인가의 개념이 약간 햇갈릴 수 있는데 인증(Authentication)은 로그인하는 User의 identification을 확인 하는 것 이고, 인가(Authorization)는 다른 말로는 권한부여, 즉 로그인한 유저에게 특정 권한을 가하는 것입니다.
그 다음 Django에서 Header 값을 받아와 인가된 유저를 확인하는 Decorator를 구현해보겠습니다.

1. Authentication(인증) - Bcrypt

1.1 인증에 대해서

먼저 인증입니다. 보통 인증을 하는 것은 유저가 입력한 패스워드가 동일한지에 대한 확인을 말 합니다.
그리고 로그인을 한다는 것은 그 전에 계정이 생성되어야 함을 의미 합니다.

1.2 DB 패스워드 암호화

회원가입 때 입력되는 패스워드는 반드시 DB 저장 시, 암호화를 해야 합니다.
그 이유로 첫 째는 DB 해킹 시 패스워드 노출을 막고자 함에 있습니다.
두 번째는 내부 개발자의 정보 침해가 일어나지 않도록 막고자 함에 있습니다.

패스워드 암호화에는 일반적으로 One-Way hash function(단방향 해쉬 함수)이 사용됩니다. One-Way hash function은  암호화 메세지인 Digest를 생성합니다. 이 Digest로 원본 메세지를 구할 수 없기 때문에 단방향성을 갖습니다.

단방향 해쉬 함수를 사용하면 hash256이면 256자의 전혀 알아볼 수 없는 암호가 얻어집니다.

1.3 bcrypt

One-Way hash function도 한계를 갖습니다. Hash 값을 미리 계산해 놓은 Rainbow Table이라는 것이 있는데, 특정 장비를 통해 단순한 패스워드의 경우 금방 찾을 수 있습니다.

1초당 56억개의 다이제스트를 대입할 수 있기 때문입니다. 그 이유는 본래 Hash Function이 암호화를 위해 사용된 것이 아닌 빠른 처리 속도를 위해 설계 되었기 때문입니다.

이러한 취약점을 해소하기 위한 방법이 두 가지가 있습니다.

* Salting
- 실제 비밀번호 이외에 추가적으로 랜덤 데이터를 더해 Hash 값을 계산하는 방법

* Key Stretching
- 단방향 해쉬값을 계산하고 그 해쉬값에 대해 해쉬하는 것을 반복하는 과정

이 두 방법이 구현된 해쉬 함수 중 가장 많이 사용되는 것이 bcrypt 입니다. bcrypt는 처음부터 패스워드의 단방향 암호화를 위해 만들어졌습니다.
그러면 실제 사용을 확인해보겠습니다.
간단하게 '1234'라는 패스워드를 가정 해보겠습니다.

>>> password = '1234'
>>> hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
>>> hashed_password
b'$2b$12$abMvsZQDKnokMqEGzBHHCu3jVEZIYuY6bheQerYS4E2zDm290/Y7e'

hashed_password = hashed_password.decode('utf-8')

코드를 분석해보겠습니다.
두 번째 라인에서 hashpw는 패스워드를 암호화 하는 함수 입니다.
인자(arguments)로 password를 utf-8로 인코딩한 값과 bcrypt method 중 하나인 gensalt()로 생성한 gensalt를 인자로 합니다. 

그렇다면 왜 password 값을 encoding 할까요?
hash 함수를 적용하기 위해서는 'utf-8'로 인코딩(byte 객체로 변환) 해야 합니다. 비교 또한 마찬가지이죠.

인코딩(이진화)을 하면 byte 형태가 됩니다. 이를 단순저장해서 사람이 보면 /u1204/... 이런 형태로 말이죠.
그렇기에 올바른 비교를 하기 위해 DB에 넣을 때는 암호화 + encoding 한 패스워드를 다시 decode(string 객체로 변환) 해야 합니다.

다시 Decoding 하는 과정은 6번 Line에 기입 하였습니다.

password를 저장하는 것이 전부가 아닌 hash로 만들고 Decoding한 패스워드를 입력한 패스워드와 비교해야 합니다.
이 과정은 bcrypt의 checkpw를 통해 이뤄집니다.
예시를 통해 확인 해보겠습니다.

>>> password = '1234'
>>> hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
b'$2b$12$abMvsZQDKnokMqEGzBHHCu3jVEZIYuY6bheQerYS4E2zDm290/Y7e'
>>> new_password = '1234'
>>> bcrypt.checkpw(new_password.encode('utf-8'),hashed_password)
True

조금 전 패스워드와 다르게 new_password를 지정하였습니다.
new_password도 비교를 해야하기 때문에 utf-8 format으로 encoding 해줍니다.
입력받은 패스워드와 조금전 bcrypt의 hashpw로 만든 패스워드를 비교합니다.
그 결과 값은 True or False로 return 됩니다.

encoding/decoding으로 string byte 변환하는 이미지

2. Authorization - JWT

웹 서비스를 이용 해보시면 자연스럽게 아시겠지만, 매번 로그인하지 않아도 현재 로그인된 상태를 알고 있습니다. 이는 로그인 후 Local Storage 또는 Session Storage에 저장하고 로그인이 필요한 서비스 요청 시 Header에 Authorization이라는 Keyaccess token이라는 value가 key-value 형태로 암호화 되어 정보를 요청하기 때문입니다.
예를 들면, Authorization:{"access_token":{"id":3}의 암호화 후 utf-8로 decode 해준 값} 이런 형태가 됩니다.
JWT는 {"id":3} 이 내용을 암호화 하는데 있습니다.

JWT(Jason Web Token)은 사용자의 로그인 이후 지속해서 로그인 상태라는 것을 확인하기 위한 Access Token을 전달하기 위해 사용됩니다. Json 형태의 web token은 복호화해서 유저가 확인할 수 있습니다.
예제 코드로 확인해보겠습니다.

>>> import jwt
>>> SECRET = 'secret'
>>> access_token = jwt.encode({'id' : 1}, SECRET, algorithm = 'HS256')
>>> access_token
b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MX0.-xXA0iKB4mVNvWLYFtt2xNiYkFpObF54J9lj2RwduAI'

>>> Authorization={"access_token":access_token.decode('utf-8')}
>>> Authorization["access_token"]
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MX0.-xXA0iKB4mVNvWLYFtt2xNiYkFpObF54J9lj2RwduAI'
>>> jwt.decode(Authorization["access_token"],SECRET,algorithm='HS256')
{'id': 1}

jwt의 encode method를 사용할 때, 첫 번째 argument는 key-value pair의 dict 값, 두 번째는 암호화 해주는 secret key 그리고 마지막에는 알고리즘 형태입니다.

이렇게 암호화를 거친 access_token은 b'이 붙은 즉 byte 형태입니다.
최종적으로는 decode하여 전달해야 합니다. 인가를 확인할 때 string으로 값을 가져오기 때문입니다.

그리고 7번 라인에서 Header에서 가져오는 형태처럼 구성하였습니다.
access_token이라는 key를 통해 value를 얻고 decode method로 최종적으로 {'id':1}이라는 결과 값을 얻습니다.

이 {"id":1}은 인가에서 너무나도 중요한 정보입니다. 유저가 자신임을 알려주어야 하기 때문입니다. 특정 유저가 보아야 하는 피드만 봐야하고 업로드 해야 하기 때문입니다.

특히 한 번 인가를 하는 과정은 다양한 API에서 구현되기 때문에 Decorator 함수로 여러번 사용할 수 있습니다.
다음 포스트에서는 Decorator로 인가 확인하기를 해보겠습니다. 

반응형