Django 인증 & 인가

최창환·2022년 3월 25일
0
post-thumbnail

인증 & 인가의 개념을 파악하고 django westagram project에 인증의 필수요소인 password암호화, 인가의 필수요소인 JWT발급을 적용시켜보자


인증 (Authentication)

인증은 유저의 identification을 확인하는 절차이다. 즉 유저의 아이디와 비밀번호를 확인하는 과정이라고 할 수 있다. 우리는 인증의 과정을 통해 우리의 서비스를 누가 쓰는지 그리고 어떻게 사용하는지 추적 할 수 있다.

로그인 절차는 다음과 같다.

1. 아이디, 비밀번호 생성
2. 유저의 비밀번호를 암호화하여 DB에 저장
3. 유저가 입력한 아이디와 비밀번호를 DB의 정보와 비교
4. 일치하면 로그인 성공
5. 로그인이 성공하면 서버에서 access token을 클라이언트에 전송
6. 유저는 다음 요청 때 access token을 첨부하여 서버에 보냄

비밀번호 암호화

비밀번호를 암호화하지 않고 DB에 저장하면 해킹당했을 때 그대로 노출될 수 있기 때문에 비밀번호를 암호화하여 저장하도록 한다.

단방향 해쉬함수(One-way hash function)

원본 메시지를 변환하여 암호화된 메시지인 digest를 생성한다. 원본 메시지는 이렇게 암호화 할 수 있지만 암호화된 메시지로는 원본 메시지를 알아낼 수 없기 때문에 단방향이라고 한다.
단방향성 해쉬함수를 사용하면 값이 조금만 달라져도 digest값이 완전히 바뀌어서 추론하기 어렵게 만드는데 이를 avalance라고 한다.

단방향 해쉬함수도 물론 단점이 있다.
원본메시지가 같으면 똑같은 digest값만 만들어낸다는 것인데 많은 digest를 확보한 해커에게 해킹당할 위험이 높아진다.

salting & keystretching

단방향 해쉬함수의 단점을 보완하기 위해 이 두가지 기법이 제시되었다.
실제 원본메시지 외에 추가적으로 랜덤 데이터를 더해서 해시값을 추출하는것을 salting이라고 한다. 만약 원본 메시지를 알아내더라도 salting된 digest를 대상으로 패스워드 일치 여부를 확인하기 어렵다.

salting과 해싱을 여러번 반복하여 key를 늘리고 더욱 유추하게 어렵게하는 과정이 keystretching이다. keystretching을 사용하면 무작위 대입으로 인한 해시값 계산에 소요되는 시간을 늘릴 수 있다.

bcrypt

salting과 keystretching을 모두 구현한 해시함수 중 가장 널리 사용되고 있다. 해시값에 salting, 해시값, 반복횟수를 사용하여 같은 원본의 메시지를 해쉬해도 전혀 다른값이 나오게 된다.


인가 (Authorization)

인가는 유저가 요청한 request에 해당하는 권한이 있는지 확인하는 절차이다.

인가의 절차는 다음과 같다.

  1. Authentication 절차를 통해 access token을 생성한다. access token에는 유저 정보를 확인할 수 있는 정보가 들어가 있어야 한다 (예를 들어 user id).
  2. 유저가 request를 보낼때 access token을 첨부해서 보낸다.
  3. 서버에서는 유저가 보낸 access token을 복호화 한다.
  4. 복호화된 데이터를 통해 user id를 얻는다.
  5. user id를 사용해서 database에서 해당 유저의 권한을 확인하다.
  6. 유저가 충분한 권한을 가지고 있으면 해당 요청을 처리한다.
  7. 유저가 권한을 가지고 있지 않으면 Unauthorized Response(401) 혹은 다른 에러 코드를 보낸다.

JWT (Json Web Token)

access token을 생성하는 가장 널리 알려진 방법 중 하나이다. 유저와 서버는 Json데이터를 암호화하여 서로 주고받을 수 있다. 모든 정보를 자체적으로 가지고 있어 자기수용적이며 두 개체 사이에서 쉽게 전달될 수 있다

JWT는 Header, Payload, Signature 3가지 문자열로 구분된다.

토큰의 타입과 해시 알고리즘 정보를 가지고 있다.

payload

토큰에 담을 정보가 들어가며 정보의 한 부분을 name:value로 이루어진 claim이라고 부른다.
claim의 종류는 다음과 같다.

Registered Claim : 미리 정의된 claim
Public Claim       : 공개용 정보 전달을 목적으로 하는 claim
Private Claim      : 클라이언트와 서버간 협의하에 사용하는 claim

signature

header의 인코딩값과 payload의 인코딩값 그리고 주어진 secret_key를 해싱하여 생성한다.

westagram 인증 구현

class SignUpView(View):
    def post(self, request):
        try:
            data            = json.loads(request.body)
            name            = data['name']
            email           = data['email']
            password        = data['password']
            phone_number    = data['phone_number']

            hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
            decode_password = hashed_password.decode('utf-8')

            validate_email(email)
            validate_password(password)

            if User.objects.filter(email=email).exists():
                return JsonResponse({'message':'Your email is already exists'}, status=400)

            User.objects.create(
                name         = name,
                email        = email,
                password     = decode_password,
                phone_number = phone_number
            )

            return JsonResponse({'message':'SUCCESS'}, status=201)

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

        except ValidationError as e:
            return JsonResponse({'message':(e.message)}, status=400)
hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
decode_password = hashed_password.decode('utf-8')

암호화 함수는 Bytes데이터만 암호화 할 수 있으므로 password를 encode하여 Bytes 데이터로 바꿔준다. encode된 passwod 데이터는 DB에 저장될 수 없기 때문에 decode하여 Bytes 데이터에서 str 데이터로 바꿔준다.
최종적으로 decode된 해쉬값을 DB에 저장한다.

westagram 인가 구현

from django.conf            import settings

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)
    
            if not bcrypt.checkpw(password.encode('utf-8'), user.password.encode('utf-8')):       
                return JsonResponse({"message":"INVALID_USER"}, status=401)

            access_token = jwt.encode({'id':user.id}, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
            
            return JsonResponse({'access_token':access_token}, status=200)

        except KeyError:
            return JsonResponse({'message':'KEY_ERROR'}, status=400)

        except User.DoesNotExist:
            return JsonResponse({"message":"INVALID_USER"}, status=401)
user     = User.objects.get(email=email)
    
            if not bcrypt.checkpw(password.encode('utf-8'), user.password.encode('utf-8')):       
                return JsonResponse({"message":"INVALID_USER"}, status=401)

            access_token = jwt.encode({'id':user.id}, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
            
            return JsonResponse({'access_token':access_token}, status=200)

SECRET_KEY와 ALGORITHM과 같은 환경변수를 만들어 my_settings.py에서 관리하고 있다. my_settings.py 파일은 환경변수 관리로서의 역할만 하는 파일로서 views.py 또는 models.py에서 직접 import해서 사용하면 views.py 파일이 my_settings.py 파일에 의존성이 생기게 된다. 만약에 환경변수 파일을 my_settings.py말고 다른 방식으로 관리하게 된다면 views.py의 코드들도 모두 수정을 해야 한다.

따라서 my_settings.py의 환경변수를 import할때에는 직접 import하지않고 from django.conf import settings를 사용하여 settings.py에서 import한다.

bcrypt.checkpw 함수는 bcrypt.checkpw(사용자로부터 입력받은 password, 저장된 암호화된 password)의 형식을 가지고 있다.
따라서 bcrypt.checkpw 함수를 사용하여 두 password를 비교하여 다를 경우 False를 반환하고 False일 때 401 에러 메시지를 반환한다.
두 password가 같을 경우에 access token을 만들고 반환해준다.

profile
포기하지 않는 개발자

0개의 댓글