[Django] westagram 암호화 적용

Magit·2020년 4월 15일
0

westagram

목록 보기
4/4

이전에 코드 리뷰를 받은 상태에서 이제 암호화를 적용해보자.

먼저 필요한게 뭔지 생각해보자.
1. 일단 회원가입을 한 유저의 비밀번호를 암호화해야한다.
2. 로그인을 할 때, 입력된 비밀번호가 암호화된 비밀번호와 맞는지 확인해줘야한다. 여기까지가 인증에 관해서이다.
3. 그리고 인증이 완료된 유저가 어떠한 행동을 할 때 인증받고서 권한이 있는 유저의 요청인지 판단하는 인가 기능이 필요하다.

대략적으로 이렇게이다.
그렇다면 이걸 코드로 어떻게 표현할 것이고, 인가인증 기능을 쓰려면 뭐가 필요한지 알아야한다.


암호화 - bcrypt 이용하기

먼저 이전에 비밀번호를 저장할 때 암호화(인코딩+디코드해서 db에 밀어넣기)까지 해보겠다.
먼저 이전에 리뷰를 받았던 코드를 끌고와보자.

(당연히 import bcrypt 는 해야한다!)

class SignUpView(View):
    def post(self, request):
        data = json.loads(request.body)
        try:
            if Account.objects.filter(email=data['email']).exists():
                return HttpResponse(status=409)
            
            Account.objects.create(
                email=data['email'],
                password=data['password'],
            )
            return HttpResponse(status=200)

        except KeyError:
            return JsonResponse({"message": "INVALID_KEYS"}, status=400)
	)

순서는 id(여기서는 email)를 확인한 코드 다음에 들어온 패스워드를 암호화(+인코딩) -> DB에 저장할 때는 디코딩하고 저장 을 하면 될것이다.

그럼 수정해보면

class SignUpView(View):
    def post(self, request):
        data = json.loads(request.body)
        try:
            if Account.objects.filter(email=data['email']).exists():
                return HttpResponse(status=409)
            hashed_password = bcrypt.hashpw(
                data['password'].encode('utf-8'), bcrypt.gensalt())
            Account.objects.create(
                email=data['email'],
                password=hashed_password.decode('utf-8'),
            )
            return HttpResponse(status=200)

        except KeyError:
            return JsonResponse({"message": "INVALID_KEYS"}, status=400)

이런식으로 변하게된다.
그럼 이제 암호화를 해서 데이터베이스에 밀어넣는거 까지는 완료했다.
그렇다면 이제 로그인을 할 때 사용자가 입력한 비밀번호와 우리가 담고 있는 비밀번호가 같은지도 확인할 수 있게 새로운 코드를 작성해야한다!
이번에는 로그인 부분의 코드를 갖고와보겠다.

class SignInView(View):
    def post(self, request):
        data = json.loads(request.body)

        try:
            if Account.objects.filter(email=data['email']).exists():
                user = Account.objects.get(email=data['email'])

                if user.password == data['password']:
                    return HttpResponse(status=200)
                return HttpResponse(status=401)
            return HttpResponse(status=401)

        except KeyError:
            return JsonResponse({"message": "INVALID_KEYS"}, status=400)

여기서 요청으로 들어온 password와 갖고있던 password를 비교해줘야한다.
유저가 입력한 암호, 암호화된 암호 모두 bytes로 인코딩된 상태여야 비교가 가능하다!
그러므로 입력한 패스워드도 데이터베이스에 저장된 패스워드도 문자열일테니 bytes로 인코딩하고서 비교해줘야한다.

class SignInView(View):
    def post(self, request):
        data = json.loads(request.body)

        try:
            if Account.objects.filter(email=data['email']).exists():
                user = Account.objects.get(email=data['email'])

                if bcrypt.checkpw(data['password'].encode('utf-8'), user.password.encode('utf-8')):
                    return HttpResponse(status=200)
                return HttpResponse(status=401)
            return HttpResponse(status=401)

        except KeyError:
            return JsonResponse({"message": "INVALID_KEYS"}, status=400)

일단 이렇게 작성해봤다.
checkpw() 메서드로 비교를 하는데, 모두 bytes로 타입을 바꿔준뒤 비교를 했다.
실제로 httpie를 통해서 계정 생성 및, 로그인 성공 / 실패하기를 해봤다.

(westagram)  ~/django-example/westagram_project   master ●  http -v localhost:8000/account/sign-in email=mag@naver.com password=1234
POST /account/sign-in HTTP/1.1
Accept: application/json, */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 49
Content-Type: application/json
Host: localhost:8000
User-Agent: HTTPie/2.0.0

{
    "email": "mag@naver.com",
    "password": "1234"
}

HTTP/1.1 200 OK
Content-Length: 0
Content-Type: text/html; charset=utf-8
Date: Wed, 15 Apr 2020 14:53:06 GMT
Server: WSGIServer/0.2 CPython/3.8.2
X-Content-Type-Options: nosniff
X-Frame-Options: DENY



(westagram)  ~/django-example/westagram_project   master ●  http -v localhost:8000/account/sign-in email=mag@naver.com password=12345
POST /account/sign-in HTTP/1.1
Accept: application/json, */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 50
Content-Type: application/json
Host: localhost:8000
User-Agent: HTTPie/2.0.0

{
    "email": "mag@naver.com",
    "password": "12345"
}

HTTP/1.1 401 Unauthorized
Content-Length: 0
Content-Type: text/html; charset=utf-8
Date: Wed, 15 Apr 2020 14:53:10 GMT
Server: WSGIServer/0.2 CPython/3.8.2
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

python shell에서도 확인해보자.

>>> from account.models import Account
>>> Account.objects.filter(email="garam5@naver.com").values()
<QuerySet [{'id': 5, 'email': 'garam5@naver.com', 'password': '$2b$12$QfT1GCEWFL1J0FIP8FoHRuxNip0sGNVw.AgF4yOzVSpd4ED1dEJVy', 'created_at': datetime.datetime(2020, 4, 15, 14, 50, 20, 909138, tzinfo=<UTC>), 'updated_at': datetime.datetime(2020, 4, 15, 14, 50, 20, 909166, tzinfo=<UTC>)}]>

비밀번호가 암호화되있는게 보이는가? 성공!

여기까지가 인증 을 한 것이다. 이제 인가 를 해보자.




인가를 위해서 PyJWT 설치와 import jwt는 기본적으로 완료한다.
지속적으로 자격이 있는 유저인지 아닌지 확인하는 방법은 로그인을 할 때, 로그인이 성공하면 그 때 토큰을 발행 하는 것이다.

그럼 다시 로그인 view 코드에서 일부를 갖고와보자.

                if bcrypt.checkpw(data['password'].encode('utf-8'), user.password.encode('utf-8')):
                    return HttpResponse(status=200)
                return HttpResponse(status=401)
            return HttpResponse(status=401)

패스워드를 확인한 뒤에 성공하면 토큰을 발행하면 될것이다.

token = jwt.encode({'id': user.id}, SECRET_KEY, algorithm='HS256').decode('utf-8')

해당 코드를 추가했다.
그러면 토큰이 발행된다.
https://jwt.io/ 이 사이트에서 발행된 토큰을 디코드해볼 수 있다.

내가 예시로 발행했던 토큰을 디코드 해보면
헤더에 타입/알고리즘
페이로드에 데이터베이스의 프라이머리키
시그니쳐에 헤더와 페이로드를 인코딩한 값과 내가 몰래 설정해둔 SECRET_KEY로 해쉬했다는 정보가 뜬다.

SECRET_KET 는 갑자기 어디서 나타났을까?
시그니쳐에 SECRET_KEY는 작업자만 알 수 있도록 해야한다.
그러므로 이런 view와 같은 곳에 입력하기보다는 settingㄴ.py같은곳에 작성해놓고, import 해와서 쓰곤 한다.
settings.py 에 SECRET_KEY = '나만 알지롱' 같이 작성하고서 view에서 from westagram_project.settings import SECRET_KEY 같이 끌고와서 쓰는 형식이다.



인증이 필요한 상황이 굉장히 자주 올텐데 그럴때는 어떻게할까?
보통 인증을하는 데코레이터를 하나 만들어두고(이 데코레이터는 보통 user app 디렉토리의 utils.py 안에 작성해둔다.), 인증이 필요한 views.py에 데코레이터를 지정하는 형식으로 한다.

파이썬을 배울 때 데코레이터 개념이 약해서 지금 좀 머리가 아프다..
장고도 결국 파이썬! 파이썬을 다시 훑어보자!

profile
이제 막 배우기 시작한 개발자입니다.

0개의 댓글