All Articles

django - 인증, 인가 실습

회원가입 및 비밀번호 암호화 프로세스

  1. request body를 통해 유저가 가입 시 입력한 비밀번호를 받아서 encoding
  2. bcrypt로 비밀번호 hashing 및 salting
  3. hasing + salting한 비밀번호를 decoding해서 DB에 저장

1. request body를 통해 유저가 입력한 비밀번호를 받아서 encoding

유저가 ‘user1234’라는 비밀번호로 가입을 했다고 가정하자. hashing과 salting을 해주기 위해 문자열에서 byte 타입으로 변환해준다. 이 과정을 인코딩이라 한다.

password = 'user1234'
encoded_password = password.encode('utf-8')

print(encoded_password)로 출력한 인코딩 결과값은 이렇게 나온다.

b'user1234'

2. bcrypt로 비밀번호 hashing 및 salting

hashing은 단방향 암호화 방식이다. 같은 값을 해싱하면 해싱값 또한 항상 같다. 따라서 해싱값 데이터가 쌓이면 역으로 암호가 드러나는 위험성이 있다. 이 점을 보완하기 위해 salting 과정을 추가해서 더 복잡하게 암호화 한다.

import bcrypt

hashed_password = bcrypt.hashpw(encoded_password, bcrypt.gensalt())

print(hashed_password)로 출력한 인코딩 결과값은 이렇게 나온다. 암호화에 성공했다.

b'$2b$12$PS77USI4OnMydAuPoPCoK.9rCCOghSmMrR82Xn6L3x2xRYaeA7i8m'

3. hasing + salting한 비밀번호를 decoding해서 DB에 저장

DB에 비밀번호를 저장할 때 반드시 암호화한 형태를 저장해야 한다. 2번에서 마지막 결과값을 보면 byte 타입으로 인코딩 되어있음을 알 수 있는데, DB에 저장할 때는 다시 string 타입으로 디코딩 해줘야 한다.

decoded_hashed_password = hash_password.decode('utf-8')

print(decoded_hashed_password)로 출력한 인코딩 결과값은 이렇게 나온다.

$2b$12$Q.bIZfHuJPgPICdJbcO8r.6xHBhXuhNXYSuSfQuOQXPV/c.5hF38K

이것을 DB에 저장하는 것까지 회원가입 프로세스다. 이제 유저가 로그인 시 입력한 비밀번호와 DB에 저장된 암호화된 비밀번호 일치 여부를 어떻게 확인하는지 살펴보자.


로그인 시 인증 프로세스

  1. request body를 통해 유저가 로그인 시 입력한 아이디 또는 비밀번호를 받아서 DB 테이블에서 유저 정보 찾기
  2. bcrypt로 입력 받은 비밀번호와 DB 테이블에 있는 암호화 비밀번호가 일치하는지 확인

로그인 유저 인가 프로세스

  1. 로그인 성공 시 토큰 부여

실제 구현

기존에 만들어둔 회원가입, 로그인 코드의 views.py를 아래와 같이 수정했다.

import json
from django.views import View
from django.http import JsonResponse, HttpResponse
from .models import Users

import bcrypt
import jwt

class MainView(View):
  def get(self, request):
    return JsonResponse({'Welcome to':'Westagram', 'Sign-up':'/users/sign-up', 'Log-in':'/users/log-in'}, status=200)

class SignUpView(View):
  def post(self, request):
    # 가입 시 유저가 입력한 아이디, 비밀번호 값을 request body를 통해 받는다.
    data = json.loads(request.body)
    
    # 유저가 입력한 비밀번호값 data['password']을 byte 타입으로 인코딩 한 후
    # bcrypt를 이용해 hasing과 salting을 해준다.
    hased_pw = bcrypt.hashpw(data['password'].encode('utf-8'), bcrypt.gensalt())
    # 해싱한 암호를 다시 디코딩 해서 DB에 저장 가능한 형태로 만들어준다.
    decoded_hashed_pw = hased_pw.decode('utf-8')

    try:
      if data['username'] == '':
        return JsonResponse({'message':'ID_IS_REQUIRED'}, status=401)
            
      elif data['password'] == '':
        return JsonResponse({'message':'PASSWORD_IS_REQUIRED'}, status=401)
            
      elif Users.objects.filter(username=data['username']).exists():
        return JsonResponse({'message':'ID_EXISTS'}, status=409)

      # 유저가 입력한 아이디가 기존 아이디와 겹치지 않는다면 if문 안 코드를 실행한다.
      elif not Users.objects.filter(username=data['username']).exists():
        # 유저가 입력한 아이디는 DB에 그대로 저장해주고, 비밀번호는 위에서 암호화 한 형태로 저장한다.
        Users(
          username = data['username'],
          password = decoded_hashed_pw
        ).save()
        return JsonResponse({'message':'WELCOME'}, status=200)

    except:
      return JsonResponse({'message':'INVALID_ID'}, status=401)

  # get 메서드 생략

class LogInView(View):
  def post(self, request):
    # 로그인 시 유저가 입력한 아이디, 비밀번호 값을 request body를 통해 받는다.
    data = json.loads(request.body)

    try:
      # 로그인 시 입력한 아이디 값 data['username']에 맞는 유저 정보가 DB에 존재한다면 if문 안 코드를 실행한다.
      if Users.objects.filter(username=data['username']).exists():

        # 객체 형태로 바로 리턴되는 get을 이용해 유저가 입력한 아이디 값에 해당하는 객체를 구해준다.
        user_id = Users.objects.get(username=data['username'])

        # 유저가 입력한 비밀번호와 DB에 저장된 비밀번호가 일치하는지 비교해준다.
        # 비교는 둘 다 byte 타입인 상태에서 해야한다.
        if bcrypt.checkpw(data['password'].encode('utf-8'), user_id.password.encode('utf-8')) == True:
          # user_id 객체의 PK값인 id를 특정 알고리즘을 사용해 암호화하여 토큰을 생성한다.
          # 여기서의 id는 유저 아이디가 아니라 DB 테이블 상 고유값 id다.
          access_token = jwt.encode({'id':user_id.id}, 'secret', algorithm='HS256')
          # 정상적으로 동작하면 토큰과 함께 status code 200을 리턴한다.
          return JsonResponse({'token': access_token.decode('utf-8')}, status=200)

        else:
          return JsonResponse({'message':'비밀번호가 틀립니다.'}, status=401)
          
      else:
        return JsonResponse({'message':'아이디가 없습니다.'}, status=401)
        
    except:
      return JsonResponse({'message':'INVALID_USER'}, status=401)
  
  # get 메서드 생략

어려웠던 점

처음에는 인증, 인가 과정이 views에서 일어나는지 models에서 일어나는지조차 판단하기 어려웠다. 이번 주 코드카타 짝 덕분에 아래와 같이 인증 프로세스를 정리했고, 테이블을 보니 수정/추가해야 할 코드가 명확해졌다.

process-table

유저 정보가 든 데이터 테이블 구조 자체를 바꿀 일이 없으므로 작업은 오로지 views.py에서만 진행하면 된다.