[Django] 회원가입, 로그인 기능 (Instagram)

Django를 시작하면서 인스타그램을 기준으로 인스타그램의 회원가입 및 로그인 기능을 비슷하게 구현해보았습니다.

Django 프로젝트(Project) 및 앱(App) 생성

django-admin startproject project_westagram
django-admin startapp user

project_westagram이라는 django 프로젝트를 만들고, user라는 이름의 앱을 생성했습니다.

앱을 생성하고 settings.py에 추가합니다.

INSTALLED_APPS = [
    ...
    'user'
]

모델(models.py) 작성

django의 모델은 객체의 특별한 종류입니다. 이 모델을 저장하면 그 내용이 데이터베이스(DB)에 저장되는 점이 특별한 점입니다.

먼저 인스타그램의 회원가입은 어떤 형태로 구성되어있는지 확인합니다.

인스타그램에 회원가입을 할 때는 위 이미지와 같이 이름, 인스타그램에서 사용할 아이디(닉네임), 휴대폰 번호 혹은 이메일 주소 그리고 비밀번호 형태로 구성되어있습니다.

from django.db import models

class Account(models.Model):
    email    = models.CharField(max_length = 45)
    name     = models.CharField(max_length = 45)
    phone    = models.CharField(max_length = 45)
    password = models.CharField(max_length = 200)

저는 위와 같은 형태로 모델을 작성했습니다.

사용자가 입력할 때 이메일, 사용할 아이디, 휴대폰 번호 총 3가지를 입력할 수 있게만들고
패스워드를 입력할 수 있게 만들었습니다.

CharField는 문자열을 저장할 때 주로 사용하고 최대길이를 지정해줄 수 있습니다.
패스워드의 길이만 혼자 200인 이유는 암호화를 했을 때 문자열의 길이가 길어지기 때문에 길게 설정했습니다.

python manage.py makemigrations user
python manage.py migrate user

models.py를 작성하고 터미널에서 마이그레이션을 해주면 연결되어있는 데이터베이스에 우리가 작성한 model 기준에 맞춰서 테이블이 생성됩니다.

뷰(views.py) 작성

뷰는 작성된 모델을 바탕으로, 데이터들을 어떻게 처리하는지에 대한 부분을 작성하게 됩니다.

즉, 유저가 회원가입과 로그인을 시도할 때에 어떠한 방법으로 처리할 것인지 views.py에 작성합니다.

import json, re, bcrypt, jwt

from django.views import View
from django.http import JsonResponse

from project_westagram.settings import SECRET_KEY
from user.models import Account

http를 통해 받은 요청(request)을 파이썬이 읽을 수 있는 형태로 변환할 수 있는 json 모듈을 임포트하고, json형태로 응답(response)을 해줄 수 있는 JsonResponse 모듈을 불러옵니다.

그리고 정규식에 사용할 re, 암호화에 사용할 bcrypt, jwt을 임포트하고 SECRET_KEY는 settings.py에 있는 것을 이용했습니다.

마지막으로 모델에서 작성한 Account 클래스도 불러옵니다.

회원가입(SignUp)

class SignUpView(View): #회원가입
    def post(self, request):
        data = json.loads(request.body)

	try:
[1]	    if (data['email'] == '') and (data['phone'] == ''):
		return JsonResponse({'MESSAGE':'Enter Your Email or Phone Number'}, status = 400)

	    if data['password'] == '' or data['name'] == '':
		return JsonResponse({'MESSAGE':'Enter Your User Name or Password'}, status = 400)

[2]	    p = re.compile('^[a-zA-Z0-9+-_.]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$')
	    if data['email'] != '' and (p.match(str(data['email'])) != None) == False:
		return JsonResponse({'MESSAGE':'EMAIL_VALIDATION'}, status = 400)

[3]	    if Account.objects.filter(name = data['name']).exists() and (data['name'] != ''):
		return JsonResponse({'MESSAGE':'NAME_DUPLICATED'}, status = 400)
	    elif Account.objects.filter(email = data['email']).exists() and (data['email'] != ''):
		return JsonResponse({'MESSAGE':'EMAIL_DUPLICATED'}, status = 400)
	    elif Account.objects.filter(phone = data['phone']).exists() and (data['phone'] != ''):
		return JsonResponse({'MESSAGE':'PHONE_DUPLICATED'}, status = 400)

[4]	    if (len(data['password']) < 8):
		return JsonResponse({'MESSAGE':'PASSWORD_VALIDATION'}, status = 400)

[5]	    password = data['password']
	    hashed_pw = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
	    Account.objects.create(
			name 	 = data['name'], 
			email 	 = data['email'], 
			phone 	 = data['phone'], 
			password = hashed_pw.decode('utf-8')
	    )

[6]	    return JsonResponse({'MESSAGE':'SUCCESS'}, status = 200)
[7]	except KeyError as ex:
	    return JsonResponse({'MESSAGE':'KEY_ERROR_' + str.upper(ex.args[0])}, status = 400)

회원가입(SignUp) 뷰는 위와 같이 작성해보았습니다.

  1. 회원가입할 때는 이메일이나 휴대폰 번호 둘 중 하나는 있어야 되는 조건을 걸었고 유저네임이나 패스워드는 둘 다 입력을 해야만하기 때문에 위처럼 예외처리를 했습니다.

  2. 그리고 그 다음 이메일을 입력했는데 이메일의 형식에 맞는지를 확인하기 위해서 정규식을 이용해서 조건을 작성했습니다.

  3. 이메일 정규식까지 확인했다면 다음은 유저네임, 이메일, 핸드폰번호 등이 기존의 데이터베이스들과 비교했을 때 중복되었는지 확인하는 부분입니다.
    exists() 메소드를 이용해서 기존에 존재하는지 여부를 확인합니다.

  4. 패스워드는 최소 8자리 이상을 입력하게끔 했습니다.

  5. 패스워드를 입력받았다면 bcrypt를 이용해서 암호화한 다음 이제 위에서 입력받았던 값들을 데이터베이스에 저장합니다.

  6. 회원 가입에 성공했다면 SUCCESS 메세지와 status 200 코드를 리턴합니다.

  7. 만약 위에서 KeyError가 발생했다면 KEY ERROR 메시지와 어떤 키의 에러인지 메시지를 리턴합니다.
    ex) 'MESSAGE' : 'KEY_ERROR_EMAIL'

로그인(SignIn)

Instagram의 로그인 하는 부분을 보면 회원가입과는 조금 다릅니다.

회원가입할 때는 최대 3가지를 입력했었는데 로그인 할 때는 이 3가지 중 한 가지만 있어도 로그인이 가능한 것입니다.

class SignInView(View): #로그인
    def post(self, request):
	data = json.loads(request.body)
		
	try:
    	    password = data['password']
            account  = data['account']

[1]	    if account == '' or password == '':
		return JsonResponse({'MESSAGE':'Enter Your User Name or Password'}, status = 400)

[2]	    if Account.objects.filter(email = account).exists():
		account_data = Account.objects.get(email = account)

[3]	        if bcrypt.checkpw(password.encode('utf-8'), account_data.password.encode('utf-8')) == False:
		    return JsonResponse({'MESSAGE':'INVALID_USER'}, status = 401)

	    	access_token = jwt.encode({'email': account}, SECRET_KEY, algorithm = 'HS256')

	    elif Account.objects.filter(phone = account).exists():
		account_data = Account.objects.get(phone = account)

	    	if bcrypt.checkpw(password.encode('utf-8'), account_data.password.encode('utf-8')) == False:
		    return JsonResponse({'MESSAGE':'INVALID_USER'}, status = 401)
				
		access_token = jwt.encode({'phone': account}, SECRET_KEY, algorithm = 'HS256')

	    elif Account.objects.filter(name = account).exists():
		account_data = Account.objects.get(name = account)

		if bcrypt.checkpw(password.encode('utf-8'), account_data.password.encode('utf-8')) == False:
		    return JsonResponse({'MESSAGE':'INVALID_USER'}, status = 401)

		access_token = jwt.encode({'name': account}, SECRET_KEY, algorithm = 'HS256')
				
[4]	    else:
		return JsonResponse({'MESSAGE':'INVALID_USER'}, status = 401)

[5]	    return JsonResponse({'MESSAGE':'SUCCESS', 'Authorization':access_token.decode('utf-8')}, status = 200)
[6]	except KeyError as ex:
	    return JsonResponse({'MESSAGE':'KEY_ERROR_' + str.upper(ex.args[0])}, status = 400)

로그인(SignIn)는 위와 같이 작성했습니다.

유저네임, 이메일, 휴대폰 번호 세 가지중 하나만 입력을 받아서 로그인을 하기 때문에 account와 password 두 가지의 키를 이용해서 작성했습니다.

  1. 로그인 역시 아이디, 패스워드를 입력하지 않았다면 예외처리를 해줍니다.

  2. 회원가입할 때는 세 개였으나 로그인 할 때는 하나만 입력받으니 이 부분이 유저네임인지 이메일인지 휴대폰번호인지 식별해야 하기때문에 기존 데이터베이스에서 exists() 메소드로 각각 존재하는지 체크합니다.

  3. 만약 이메일이 일치했다면 이번에는 패스워드를 확인합니다. 암호화된 패스워드와 사용자가 입력한 패스워드가 맞는지 확인한 후 서로 다르면 INVALID_USER 메시지를 리턴합니다.
    그리고 패스워드까지 일치했다면 그에 해당하는 토큰을 생성합니다.
    마찬가지로 휴대폰 번호, 유저 네임도 같은 방법으로 체크합니다.

  4. 만약 세 가지 조건에서 데이터베이스에 일치하는 값을 찾지 못했다면, 아이디가 등록되지 않은 것이므로 에러메시지를 리턴합니다.

  5. 로그인에 성공했다면 SUCCESS메시지와 함께 인증된 토큰을 같이 전달합니다.

  6. 회원가입 때와 마찬가지로 KeyError를 예외처리 해줍니다.

경로(urls.py) 작성

# settings.py
ROOT_URLCONF = 'project_westagram.urls'

settings.py에 최초 경로를 확인할 수 있습니다.

# project_westagram/urls.py
from django.urls import path, include

urlpatterns = [
    path('user/', include('user.urls')),
]

그리고 urls.py에서는 include를 추가해줬습니다.

즉, http://localhost:8000/user/
user 라는 경로에 들어오면 그 이후에는 user app 내에 있는 urls.py에서 관리한다는 뜻입니다.

# user/urls.py
from django.urls import path

from user.views import SignUpView, SignInView

urlpatterns = [
    path('signup', SignUpView.as_view()),
    path('signin', SignInView.as_view()),
]

처음 app을 생성해도 urls.py는 따로 없습니다. 그래서 urls.py 파일을 새로 생성한 후에 작업을 진행합니다.

http://localhost:8000/user/signup 회원가입
http://localhost:8000/user/signin 로그인

위의 경로로 들어온 데이터 요청을 위에서 만든 SignUpView, SignInView 클래스를 통해 처리합니다.

뷰 클래스는 내장 함수를 반환하는 as_view() 클래스 메소드를 제공하고, 모든 클래스 기반 뷰는 이 클래스를 직간접적으로 상속받아 사용합니다.

SignUp, SignIn 테스트

간단한 회원가입, 로그인 앱을 생성했고 이제 잘 실행되는지 확인합니다.

테스트는 httpie를 이용해서 진행해보겠습니다.

python manage.py runserver

먼저 위의 명령어를 실행시키고 서버가 잘 작동하는지 확인합니다.

회원가입(SignUp) 테스트

서버가 작동되었고 이제 httpie를 통해서 회원가입부터 테스트 해봅니다.

http -v POST localhost:8000/user/signup name='hukim_test' phone='01011111111' email='[email protected]' password='12345678'

회원가입에 성공했다는 메시지를 받았습니다.

기타 예외처리 한 부분도 잘 되었는지 테스트를 통해서 확인해봅니다.

그리고 데이터베이스에 잘 저장되었는지 확인해봅니다.

password도 암호화되어 데이터베이스에 잘 저장되었습니다.

로그인(SignIn) 테스트

위에서 회원가입했던 아이디로 로그인 해보겠습니다.

로그인에 성공했다는 메시지와 토큰을 반환하는 것을 확인했습니다.

좋은 웹페이지 즐겨찾기