인증(Authentication) & 인가 (Authorization)🛡

인증과 인가는 API에서 가장 자주 구현되는 기능중 하나

Private and Public API 둘다 기본적인 인증과 인가를 요구

Authentication⚙️

유저의 identification을 확인하는 절차, (유저의 아이디와 비번을 확인하는 절차)

로그인 절차:
1. 유저 아이디와 비번 생성
2. 유저 비번 암호화 해서 DB에 저장.
3. 유저 로그인 -> 아이디와 비밀번호 입력
4. 유저가 입력한 비밀번호 암호화 한후 암호화되서 DB에 저정된 유저 비밀번호와 비교.
5. 일치하면 로그인 성공
6. 로그인 성공하면 access token을 클라이언트에게 전송.
7. 유저는 로그인 성공후 다음부터는 access token을 첨부해서 request를 서버에 전송함으로서 매번 로그인 해도 되지 않도록 한다.

:: 유저 비밀번호 암호화

  • 유저의 비밀번호는 절대 비밀번호 그대로 DB에 저장 하지 않는다.

    • DB가 해킹을 당하면 유저의 비밀번호도 그대로 노출 된다.
    • 외부 해킹이 아니더라도 내부 개발자나 인력이 유저들의 비밀번호를 볼 수 있다.
  • 유저의 비밀번호는 꼭 암호화 해서 저장 해야 한다.

    • 그럼으로 DB가 해킹을 당해도 비밀번호가 그대로 노출되지 않으며
    • 내부 인력도 비밀번호를 알 수가 없음.
  • 비밀번호 암호에는 단방향 해쉬 함수(one-way hash function)가 일반적으로 쓰인다.

    • 단방향 해시 함수는 원본 메시지를 변환하여 암호화된 메시지인 다이제스트(digest)를 생성한다. 원본 메시지를 알면 암호화된 메시지를 구하기는 쉽지만 암호화된 메시지로는 원본 메시지를 구할 수 없어서 단방향성(one-way) 이라고 한다.
    • 예를 들어, "test password"를 hash256이라는 해쉬 함수를 사용하면 0b47c69b1033498d5f33f5f7d97bb6a3126134751629f4d0185c115db44c094e 값이 나온다.
    • 만일 "test password2"를 hash256 해쉬 함수를 사용하면 d34b32af5c7bc7f54153e2fdddf251550e7011e846b465e64207e8ccda4c1aeb 값이 나온다. 실제 비밀번호는 비슷하지만 해쉬 함수 값은 완전히 틀린것을 볼 수 있다. 이러한 효과를 avalance라고 하는데 비밀번호 해쉬 값을 해킹을 어렵게 만드는 하나의 요소이다.

    Bcrypt

  • Salting

    • 실제 비밀번호 이외에 추가적으로 랜덤 데이터를 더해서 해시값을 계산하는 방법.
  • Key Stretching

    • 단방향 해쉬값을 계산 한 후 그 해쉬값을 또 또 해쉬 하고, 또 이를 반복하는 것을 말한다.

    JWT(JSON Web Tokens)

    앞서 언급했듯이 유저가 로그인에 성공한 후에는 access token이라고 하는 암호화된 유저 정보를 첨부해서 request를 보내게 된다.

:: 인가(Authorization)

  • Authorization은 유저가 요청하는 request를 실행할 수 있는 권한이 있는 유저인가를 확인하는 절차 이다.
  • 예를 들어, 해당 유저는 고객 정보를 볼 수 있는 있지만 수정 할 수는 없다 등.
  • Authroization도 JWT를 통해서 구현 될 수 있다.
    • access token을 통해 해당 유저 정보를 얻을 수 있음으로 해당 유저가 가지고 있는 권한(permission)도 확인 할 수 있다.

Authorization 절차🗄

  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 more details

pip install bcrypt

import bcrypt

bcrypt의 암호화 방법!

bcrypt는 str 데이터가 아닌 Bytes 데이터를 암호화 합니다.

따라서 암호화시에 bytes화 해야합니다.

파이썬에서는 str 을 encode하면 bytes(이진화) 되고, Bytes 를 decode하면 str 화 합니다.

encode, decode시에는 우리가 인식할 수 있는 형태로 변환하기 위해 'UTF-8' 유니코드 문자 규격을 사용합니다.

본격적으로 bcrypt 라이브러리를 사용하겠습니다.

먼저 패스워드를 암호화 해보겠습니다.
암호화된 데이터의 타입은 무엇일까요? 당연히 Bytes 입니다!
암호화된 데이터의 타입은 무엇일까요? 당연히 Bytes 입니다!
bcrypt.checkpw() 메소드가 바로 그 일을 하고 있습니다.

이 메소드에는 규칙이 있습니다. (입력받은 패스워드, 저장된 암호화된 패스워드)

둘 다 데이터 타입은 Bytes 여야 합니다.

인증을 위한 수단인 JSON Web Token 이하 JWT 구현

jwt또한 라이브러리를 사용해서 구현

먼저 PyJWT 라이브러리를 설치

pip install pyjwt

import jwt #패키지명은 pyjwt이지만 임포트할때의 이름은 jwt입니다.

SECRET = 'secret' #'랜덤한 조합의 키' 예제이므로 단순하게 'secret'이라고 하겠습니다.

access_token = jwt.encode({'id' : 1}, SECRET, algorithm = 'HS256')
print(access_token)
b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MX0.-xXA0iKB4mVNvWLYFtt2xNiYkFpObF54J9lj2RwduAI'

위와 같이 라이브러리를 동작시킬 수 있습니다.

jwt의 결과물도 bytes 타입 이네요.

그럼 이제 인증을 위한 매개체를 만들었습니다. 바로 jwt token 일명 access_token입니다.

이렇게 발급된 토큰은 어떻게 사용할 수 있을까요? 바로 프론트엔드 엔지니어에게 전달해야 합니다.

그렇다면 로그인이 성공하면 위와 같은 방식으로 발행한 토큰을 전달하면 되겠죠!
그렇다면 이제 인증을 위한 기본 요건이 달성 되었으니, 인증을 통과한 사용자만 접근하려면 토큰을 받아서 다시 우리가 발행한 토큰이 맞는지 확인 해야겠죠?

header = jwt.decode(access_token, SECRET, algorithm = 'HS256')
print(header)
{'id': 1}

decode 결과는 바로 우리가 encode 할 때 넘겼던 header값인 { 'id' : 1}입니다.

이 말은 우리가 담아 넘기는 header 값을 통해 유저를 식별할 수 있다는 이야기입니다.

인증하는 코드는 어디에 구현해야 할까요?

바로 엔드포인트에 데코레이터를 구현해야 합니다.

데코레이터 구현은 보통 user app에 utils.py를 만들어서 작성합니다.






오늘 까지 한 것.

회원가입 : email validation 추가됨
로그인 : 비밀번호 암호화 및 토큰 부여 가능

import json
import bcrypt
import jwt
import re
from django.views                import View
from django.http                 import JsonResponse, HttpResponse
from project_westagram.settings  import SECRET_KEY
from .models                     import Account

# 치킨먹고싶다...
class SignUpView(View):
    def post(self, request): #포스트로 받을 시 저장
        data = json.loads(request.body)
        signupdb = Account.objects.all() #유저 테이블정보 다 가져와
        try:        
            user = Account(
                name         = data['name'],
                phone_number = data['phone_number'],
                email        = data['email'],
                password     = data['password'])
            email_valid = re.compile('^[a-zA-Z0-9+-_.]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$')
            if email_valid.match(data['email']) == None:
                return JsonResponse({"message" : "email_error"}, status = 400)
            elif len(data['password']) < 8 :
                return JsonResponse({"message" : "short_password"}, status = 400)
            elif Account.objects.filter(email = data['email']).exists() or Account.objects.filter(name = data['name']).exists() or Account.objects.filter(phone_number = data['phone_number']).exists() :
                return JsonResponse({"message" : "duplicated_information"}, status = 401)
            else:
                Account.objects.create(
                    name         = data['name'],
                    phone_number = data['phone_number'],
                    email        = data['email'],
                    password     = bcrypt.hashpw(data['password'].encode("UTF-8"), bcrypt.gensalt()).decode("UTF-8")).save()
                return JsonResponse({"message" : "SUCCESS"}, status = 201)
        except KeyError:
            return JsonResponse({"message" : "KEY_ERROR"}, status = 400)

    def get(self, request): #겟으로 받을시 유저테이블에 있는것 출력
        account_data = Account.objects.values()
        return JsonResponse({'Account Data' : list(account_data)}, status = 200)

class LoginView(View):
    def post(self, request):
        data        = json.loads(request.body)
        user_password = data['password'] #헷갈릴까바 안썼음

        try:
            if 'name' not in data and 'email' not in data and 'phone_number' not in data:
                return JsonResponse({'message' : 'KEY_ERROR'},   status = 400)
            elif 'password' not in data:
                return JsonResponse({'message' : 'KEY_ERROR'},   status = 400)

            elif Account.objects.filter(email = data['email']).exists():
                exist_user = Account.objects.get(email = data['email'])
                if bcrypt.checkpw(data['password'].encode('UTF-8'), exist_user.password.encode('UTF-8')):
                    token = jwt.encode({'user_id' : exist_user.id}, SECRET_KEY, algorithm='HS256').decode('UTF-8')
                    header = jwt.decode(token, SECRET_KEY, algorithm = 'HS256')
                    print(header)
                    return JsonResponse({'token' : token}, status = 200)
                return JsonResponse({"message" : "INVALID_USER"}, status = 401)
            return JsonResponse({"message" : "No_Exist_User"}, status = 401)
        except KeyError:
            return JsonResponse({'message' : 'KEY_ERROR'},  status = 400)
        

    def get(self, request): #get메쏘드로 회원가입된 데이터들을 list형태로 반환
        account_data = Account.objects.values()
        return JsonResponse({'Login' : list(account_data)}, status = 200)
        ```
profile
🇰🇷🇺🇸 #Back-End Engineer

0개의 댓글