django #6 인스타그램 클론 코딩(2) - 비밀번호 암호화, JWT Token 발급

Junyoung Kim·2022년 1월 21일
0

django

목록 보기
6/10

회원가입과 로그인은 성공적으로 마쳤지만 여기에는 문제가 있었다.

로그인을 할 때 http request에 이메일과 비밀번호를 적어서 보냈는데, 비밀번호가 암호화가 되어있지 않아 보안에 취약하다는 점이다. Database가 해킹되면 유저의 개인정보가 그대로 노출되고, 해킹되지 않더라도 개발자들이 볼 수 있다는 취약점이 있다.

이것을 방지하기 위해 인증과 인가라는 개념을 회원가입과 로그인에 도입하는 내용을 배웠다.
해당 개념을 회원가입과 로그인에 실제로 적용하기 전에 간단하게 살펴보고 가겠다.




인증(Authentication)

인증

사용자의 id와 password를 확인하는 절차(identification)

  • 회원가입 시 유저가 생성한 비밀번호를 암호화 (bcrypt)
  • 로그인 시 암호화되어 저장된 비밀번호를 복호화
  • 유저가 입력한 정보와 일치하면 access token 생성
  • 로그인에 성공하면 유저는 다음부터는 access token을 첨부해 전송하면서매번 로그인 하지 않아도 됨


단방향 해쉬란?

  • 비밀번호 암호화에 쓰이는 알고리즘
  • 원본 메시지를 변환하여 암호화된 메시지인 다이제스트(digest)를 생성
  • 원본 메시지를 알면 암호화된 메시지를 구하기는 쉽지만 역은 쉽지 않아서 이것을 단방향성(one-way)라고 함


Bcrypt

단방향 해쉬 함수도 취약점이 있는데, 해시를 계산해 놓은 값을 테이블에 넣고 비교를 하면 시간이 걸리더라도 원본 비밀번호를 구할 수 있다.
이를 막기 위해 saltingkey strecting이라는 두가지 방법이 고안되었다.

salting

실제 비밀번호 이외에 추가적으로 랜덤 데이터를 더해서 해시값을 계산

Key Stretching

단방향 해시값을 계산 한 뒤 그 해시값을 또 해쉬하고 이를 반복함, Salting을 여러번 한 것이라고 볼 수 있음


그렇다면 로그인 후 발급되는 access token은 어떤 방식으로 동작하는 것일까?
발급되는 토큰에는 여러 가지가 있지만 그 중 JWT(JSON Web Token)에 대해 알아보겠다.



JWT

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을 복호화하여 해당 유저 정보를 알게 된다.
서버는 유저가 누군 지 알게 되었기 때문에 해당 유저가 매번 로그인을 하지 않아도 되는 것이다.

인가(Authorization)

인가

인가는 유저가 요청하는 request를 실행할 수 있는 권한이 있는 유저인가를 확인하는 절차다.
즉 요청에 대해 접근 권한을 식별하는 것이라고 볼 수 있다.
인가 또한 JWT를 통해서 구현될 수 있다.

인가의 절차

  1. Authentication 절차를 통해 access token을 생성한다. access token에는 유저 정보를 확인할 수 있는 정보가 들어가 있어야 한다 (예를 들어 user id).

  2. 유저가 request를 보낼때 access token을 첨부해서 보낸다.

  3. 서버에서는 유저가 보낸 access token을 복호화 한다.

  4. 복호화된 데이터를 통해 user id를 얻는다.

  5. user id를 사용해서 database에서 해당 유저의 권한(permission)을 확인한다.

  6. 유저가 충분한 권한을 가지고 있으면 해당 요청을 처리한다.

  7. 유저가 권한을 가지고 있지 않으면 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] : 입력한 비밀번호를 bcrypthashpw() 메서드를 이용해 해시한다.
    hashpw() 메서드는 byte형 자료형만 받으므로, str형인 비밀번호를 인코딩해주고, gensalt() 메서드를 이용해 salting하여 저장한다.

  • [2] : POST 메서드를 이용하여 데이터 테이블에 저장할 때 해시된 비밀번호로 저장한다. hashed_password는 byte형 자료형이므로, 테이블에 str형 자료형으로 저장하기 위해 디코딩을 해준다.

이전까지는 암호화가 되지 않고 저장되었던 비밀번호가 해싱을 통하여 다르게 저장된 것을 확인할 수 있다.



JWT Token 발급

이번에는 로그인 시 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]
  • [1] : PyJWT 모듈 import (설치는 PyJWT지만 import할 때는 jwt)
  • [2] : 토큰 해싱에 사용할 알고리즘과 시크릿 키는 보안이 중요한 파일로, git에 업로드하지 않기 위해 이전에 빼놓은 my_settings.py에 저장
# 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)
  • [4] : 토큰에 user_id를 담기 위해 get 메서드를 사용해서 사용자의 user 변수 저장
  • [5] : hashpw()와 동일하게 checkpw() 또한 byte형 자료형만 받으므로, 입력한 비밀번호를 인코딩해주고, str형으로 저장된 비밀번호 또한 인코딩해줌. 두 입력값이 일치하면 True가 반환되므로 일치하지 않을 시 에러 메세지 반환
  • [6] : 로그인에 성공 시 발급할 jwt를 생성.
    [4]번 과정에서 가져온 user_id를 [2]번 과정에서 가져온 시크릿 키와 알고리즘을 이용해 해싱

http 요청을 보면 enigma123@이라는 값은 실제 데이터 테이블에 저장된 비밀번호와 일치하지 않는다. 그렇지만 로그인 과정에서 복호화 과정을 거쳐서 로그인에 성공했다는 메시지를 반환받고, JWT가 발급된 것도 확인할 수 있다.

0개의 댓글

관련 채용 정보