TIL - Django - 회원 가입 및 로그인 처리(피드백)

김영훈·2021년 4월 8일
2

Django

목록 보기
4/11

# models.py

  • 필드 이름을 useremail에서 email로 변경

    • 모델 class 이름인 User가 이미 user에 관한 정보를 담고 있어서, 필드 이름에서 굳이 user를 반복할 필요가 없다.
  • clone 대상인 Instagram에서 회원가입 시 email, password 이외에 성명, 휴대폰 번호 정보를 요구하므로, 필드를 추가

    • mysql에서 table삭제 후 makemigrations가 적용되지 않을 때 해경 방법

      • app 폴더 안의 migrations 폴더에서 ~_initial.py 파일을 모두 삭제

      • mysql 데이터베이스에 접속하여 다음 명령어를 입력

        • delete from django_migrations where app = '앱 이름'

# views.py(회원 가입)

  • 회원가입 처리에 관한 class view 이름은 일반적으로 register보다 signup이라는 표현이 사용된다.

    • register라는 표현은 회원 등록, 상품 등록 등 여러 가지 의미를 함축하기 때문에, 정확한 의미 전달이 되지 않음
  • password 재입력 및 확인 기능은 주로 프론트엔드에서 구현하는 validation(유효성 검사)이므로 삭제해도 된다.

  • 요청을 통해 전달된 email 주소가 기존 데이터베이스의 내용과 중복되는지 확인하는 validations에선 get() 대신 filter().exists()를 사용하는 것이 효율적이다.

    • 이유: filter().exists()는 조건과 일치하는 데이터가 존재하지 않는 경우, error를 반환하지 않는다. 대신 빈 queryset 객체를 반환하므로, 처리가 편리하다.
  • validation(유효성 검사)에 대한 흐름 처리는 elif 대신 if문을 사용하며, return을 통해 조건에 만족한 경우에 대해 처리하는 것이 일반적이다.

    • 유효성 검사를 통과하지 못한 경우, response(응답)으로 보낼 상태 코드400
  • 비밀번호 길이 제한을 숫자로 지정하면, 추후 값(비밀번호 길이 제한 숫자)을 바꿀 때 어려울 수 있다. 그러므로 상수생성한 뒤 그곳에 데이터를 지정하여 사용하는 것이 좋다.

    • 권장하는 상수 입력 위치: 일반적으로 쉽게 발견할 수 있는 코드의 상단에 위치
  • return 메시지는 클라이언트가 보는 것이 아니고 프론트엔드가 보는 메시지이므로 간단하게 표현하는 것을 권장(메시지 형식 통일시키기 ex) 대문자, 언더바 등)

    • "Please enter at least 8 characters"(X)
    • "At least 8 characters"(O)
    PASSWORD_LENGTH = 8
    if len(password) < PASSWORD_LENGTH:
        return JsonResponse({"message":"At least 8 characters"}, status = 400)
  • if조건문에 만족하는 경우에 대해 return으로 흐름 처리를 하면, if조건문 하단에 else문 사용이 필요하지 않다.

    • 이유: return이 함수가 종료시키기 때문에 else: 없이도, if조건문에 만족하지 않 경우에 대한 흐름 처리가 가능

    • elif나 else는 필요하지 않다면 지나친 사용은 자제하자.

  • KeyError

    • python에서 KeyError가 발생하는 이유: 딕셔너리에서 key값이 존재하지 않는 경우

    • '회원 가입'에서 KeyError가 발생하는 이유: 회원 가입 요청 시 body에 담겨 서버로 전달되어야 할 값'key:value'형태로 전달되지 않은 경우

    • '회원 가입'에서 KeyError를 처리하는 방법: try-except문으로 처리하며, except KeyError:로 예외처리를 한다.
      ** 가독성을 위해 except문은 함수의 가장 하단배치하는 것이 좋다.

  • 요청받은 email 데이터와 DB 내부 데이터의 중복 여부를 검사하는 코드email 입력 유효성 검사비밀번호 입력 유효성 검사 아래배치

    • 이유: 입력 요건에 어긋나는 body 메시지에 대한 불필요한 유효성 검사방지하기 위해
    • 유효성 검사의 순서'형식'과 관련된 처리가 먼저 이뤄져야 한다.
  • body 메시지의 key에 해당하는 'name''phone_number'에 대해 빈 값(value)이 전달 됐을 경우에 대한 유효성 검사추가해야 함

    • 이유
      • 빈 값을 입력받았음에도 회원 가입이 정상적으로 처리되는 현상이 발생
      • 'email''password'는 입력 유효성 검사에 의해 error 처리가 이뤄지기 때문에, 빈 값이 입력된 경우에 대한 유효성 검사를 추가할 필요가 없다.

# bcrypt로 password 암호화하기(회원 가입)

  • 요청자로부터 입력받은 password를 암호화하여 저장하는 작업은 개인정보 보호를 위해 필수
  1. bcrypt설치

    • pip install bcrypt
  2. views.py에서 bcrypt 불러오기

    • import bcrypt
  3. bcrypt는 string 데이터가 아닌 Bytes 데이터를 암호화하기 때문에 입력받은 password 데이터를 bytes로 형변환 필요

    • string 데이터.encode('UTF-8')
  4. bcrypt 라이브러리로 패스워드 암호화하기

    • bcrypt로 암호화된 비밀번호는 bytes type으로, 모델의 필드 타입이 charfield인 데이터베이스에 저장하려면 문자열로 형변환을 해줘야 한다. 이를 위해 decode()가 사용된다. 만약 문자열로 바꾸지 않고 DB에 저장하면, 로그인 처리 시 애를 먹게 되므로 반드시 형변환을 해주자!
        password = '1234'
        hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode()
# 회원 가입 기능 구현 코드
class SignupView(View):
    def post(self, request):
        data            = json.loads(request.body)
        PASSWORD_LENGTH = 8

        try:
            email        = data['email']
            password     = data['password']
            name         = data['name']
            phone_number = data['phone_number']

            if name == '' or phone_number == '':
                return JsonResponse({'message' : "Empty name or phone_number"}, status =400)

            if not ('@' in email) or not ('.' in email ):
                return JsonResponse({"message":"Enter a valid useremail"}, status = 400)
            
            if len(password) < PASSWORD_LENGTH:
                return JsonResponse({"message":"At least 8 characters"}, status = 400)

            if User.objects.filter(email=email).exists():
                return JsonResponse({"message":"Duplicate_Useremail"}, status = 400)

            hashed_password  = bcrypt.hashpw(password.encode('UTF-8'), bcrypt.gensalt()).decode()
            User.objects.create(
                email        = email,
                password     = hashed_password,
                name         = name,
                phone_number = phone_number,
            )
        
        except KeyError:
            return JsonResponse({"message":"KEY_ERROR"}, status = 400)
        
        return JsonResponse({"message":"SUCCESS"}, status = 200)

# views.py(로그인 처리)

  • get()으로 딕셔너리 값 가져오기

    • 형태: 딕셔너리 객체.get(key, default값)
    • 용도: 존재하지 않는 key값을 인자로 입력받아도 KeyError를 반환하지 않고, 대신 인자로 입력받은 default값을 반환한다.
  • KeyError 유효성 검사실행하고 싶지 않은 데이터가 있다면, 딕셔너리 객체['key'] 대신 딕셔너리 객체.get('key')를 사용하면 된다. default값을 반환하기 때문에, except문에 의해 처리되지 않는다.

  • 일치하는 계정이 존재하지 않거나, 비밀번호가 맞지 않을 경우 상태 코드401을 반환한다.

    • 400 Bad Requesst

      client가 잘못된 요청을 보냈음을 믜미

    • 401 Unauthorized

      인증되지 않은 사용자가 인증이 필요한 리소스를 요청하는 경우 인증의 필요성을 알려주는 상태 코드. 보통 로그인이 필요한 API를 비 로그인 사용자가 호출했을 때 많이 사용된다.

  • 요청받은 email과 일치하는 사용자password(필드값)접근하는 코드의 위치는 '사용자 존재 여부를 확인하는 유효성 검사' 다음에 배치

    • 이유: 일치하는 사용자가 존재하지 않을 경우 password값에 접근하지 못하게 되고, 이는 500error의 원인이 된다.
    user = User.objects.filter(email=email)

    if not user.exists(): # 사용자 존재 여부 확인하는 유효성 검사
        return JsonResponse({"message":"INVALID_USER"}, status = 401)
 
    user_password = user.first().password  # 존재하는 사용자의 password 데이터에 접근

# bcrypt로 password 일치 여부 검증하기(로그인 처리)

  1. bcrypt는 Bytes 데이터를 인자로 받으므로, 로그인 요청 시 입력받은 password 데이터를 bytes로 형변환

    • password.encode('UTF-8')
  2. 로그인 요청자와 일치하는 User정보를 DB에서 가져오기

    • user = User.objects.filter(email=email)
  3. 일치하는 User정보(객체)가 DB에 존재하는 경우, 해당 객체의 password값에 접근한다.

    • user_password = user.first().password

    • filter()로 불러온 queryset객체 내부 요소(class User의 인스턴스 객체)에 접근할 때 주의사항

      • 인덱스를 사용하여 데이터를 가져오는 것은 좋지 않으므로 권장x

        • user_password = user[0].password (권장 X)
      • for문으로 내부 요소값 꺼내기

      • filter().first()활용

        • first(): queryset 객체의 첫 번째 요소를 가져옴
        • last(): queryset 객체의 마지막 요소를 가져옴
  4. 요청 시 입력받은 passwordUser객체의 password가 **일치하는지 확인

    • bcrypt가 Bytes 데이터를 인자로 받는 것을 고려하여 user_password를 bytes로 형변환
        if not bcrypt.checkpw(password.encode('utf-8'), user_password.encode('UTF-8')):
           return JsonResponse({"message":"INVALID_USER"}, status = 401)
        

# pyjwt로 Token 발행하기(로그인 처리)

  • JWT는 사용자의 로그인이 이뤄진 후, 지속해서 매번 로그인할 필요가 없도록 해주는 access token(암호화된 유저 정보)을 생성하여 사용자에게 전달하기 위해 사용된다.

  • 인가(Authorization)JWT를 통해 구현될 수 있다.

    • 인가가 필요한 이유: Http가 stateless하기 때문에
    • Authorization은 유저가 요청하는 request를 실행할 수 있는 권한이 유저 본인에게 있는지 확인하는 절차
    • access token를 통해 해당 유저 정보를 얻을 수 있기 때문에, 해당 유저가 가진 권한(permission)도 확인할 수 있다.
  1. pyjwt 설치

    • pip install pyjwt
  2. pyjwt views.py로 불러오기

    • import jwt
  3. 사용자의 로그인이 문제없이 이뤄졌을 경우 access_token 생성

    • jwt를 통해 생성된 access_token의 결괏값은 pyjwt의 버전에 따라 bytes 타입(ver.1.7)이거나 str 타입(ver.2.0이상)이다.
    • 기본 형태: jwt.decode(json 타입의 값, 시크릿 키번호, algorithm='HS256')
  4. 발급된 token을 프론트엔드 엔지니어에게 전달

    • JsonResponse에 token을 추가
        access_token = jwt.encode({'id' : user.first().id}, my_settings.SECRET['secret'], algorithm='HS256')
        return JsonResponse({"message":"SUCCESS",'token' : access_token, 'user_email' : user.first().email},  status = 200)
# 로그인 처리 기능 구현 코드
class  LoginView(View):
    def post(self, request):
        data = json.loads(request.body)
        try:
            email         = data['email']
            password      = data['password']
            name          = data.get('name')
            phone_number  = data.get('phone_number')

            user          = User.objects.filter(email=email)

            if not user.exists():
               return JsonResponse({"message":"INVALID_USER"}, status = 401)
 
            user_password = user.first().password 

            if not bcrypt.checkpw(password.encode('utf-8'), user_password.encode('UTF-8')):
                return JsonResponse({"message":"INVALID_USER"}, status = 401)
                
        except KeyError:
            return JsonResponse({"message":"KEY_ERROR"}, status = 400)

        access_token = jwt.encode({'id' : user.first().id}, my_settings.SECRET['secret'], algorithm='HS256')

        return JsonResponse({"message":"SUCCESS",'token' : access_token, 'user_email' : user.first().email},  status = 200)

# 자기 컴퓨터 아이피로 django웹 서버 열기

  • mac에서 자신의 IP 주소 보는 방법
    ipconfig getifaddr en0
  • 자신의 IP로 django웹 서버 열기
    python manage.py runserver 자신의 IP 주소:8000

# 예외처리 체크리스트

  • 회원가입
    • Key error
    • 중복 계정
  • 로그인
    • key error
    • 없는 사용자
    • 비밀번호가 틀렸을 경우
profile
Difference & Repetition

0개의 댓글