[Django] 인증, 인가 (feat. Bcrypt, JWT)

Jungmin Seo·2021년 7월 4일

Django

목록 보기
4/4

1. 인증 (Authentification)

이 유저가 어떤 유저인지 식별하는 작업

중요한 개인정보는 모두 암호화 / Database에 넣을때부터 암호화하기

암호화한 사람(ex. 개발자)도 복호화 할 수 없도록 하기

해쉬 함수 (Hash Function)

하나의 input(원본 메시지) 당 하나의 output(암호화된 메시지: Digest)이 있는 함수
output으로 input을 유추할 수가 없음 > 단방향

해커가 임의의 값을 입력하면서 비밀번호를 알아낼 가능성이 있는데, 이런 점을 보완하기 위해 아래 방법들이 있다.

1. Salting

랜덤한 문자열을 추가로 붙여서 해시값을 계산하는, 일명 소금 치는 작업

2. Key Stretching

해시값 계산 후, 솔팅 후 또 해시, 솔팅 후 또 해시 이렇게 계속 반복하는 방법.
일명 쭉쭉 늘려주는 것(stretching)

Bcrypt

비밀번호를 단방향 암호화 하기 위해 만들어진 해쉬함수
python에서 라이브러리로 제공된다. (다양한 언어에서 사용 가능)

왜 Bcrypt를 사용할까

  • 해쉬 함수 중 SHA(Secure Hash Algorithm)는 GPU를 이용한 연산속도가 매우 빠르기 때문에 Password 암호화에 권장되지 않는다 > 해싱속도가 빨라서 rainbow table도 빨리 만들 수 있다.
  • Bcrypt의 설계 기반인 Blowfish는 salting, key stretching과 같은 전처리 작업을 통해 작업량(=해싱시간)을 조절할 수 있다.(늘릴 수 있다)

Bcrypt를 이용하여 암호화한 password로 회원가입 / 로그인

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에 저장된 암호를 비교한다.


JWT (JSON Web Tokens)

  • 로그인에 성공하면,access token이라고 하는 암호화된 유저 정보를 첨부하여 request를 보낸다.
  • JWT는 유저 정보를 담은 JSON 데이터를 암호화해서 클라이언트와 서버 간에 주고 받는 것이다.

  • request (유저 로그인)
POST /auth HTTP/1.1
Host: localhost:5000
Content-Type: application/json

{
    "username": "joe",
    "password": "pass"
}
  • response (access token)
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 
}



2. 인가 (Authorization)

로그인한 사람이 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_KEYALGORITHM를 넣어 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)

참고 자료
https://jusths.tistory.com/158

profile
Hello World!

0개의 댓글