TIL82. Django Westagram

Jaeyeon·2021년 4월 7일
0
post-thumbnail

🤔 무엇을 구현 했는가?!

1. Django 초기세팅

  • settings.py 초기세팅
  • mysettings.py 만들어 데이터베이스와 Secret Key이동
  • urls.py 및 views.py에서 필요없는 코드 제거

2. SignUp 구현

  • 회원가입과 로그인에 쓸 models.py 모델링 : username, name, email, password, phone_number만 필요하여 테이블은 하나만 만들었다.
  • 필수입력사항은 name, phone_number, email, username, password이였다.
  • 비밀번호를 bcrypt (단방향 해쉬 + salting + key stretching) 처리하여 개발자도 회원가입을 한 유저의 비밀번호를 모르도록 그리고 해커의 침입에 최대한 시간을 끌기 위해 구현

3. SignIn 구현

  • 회원가입과 다루는 데이터가 똑같아 models.py는 손대지 않았다.
  • 필수입력사항은 username or email or phone_number, password이다.
  • bcrypt의 메소드를 사용하여 데이터베이스에 저장되어있는 암호화된 비밀번호와 로그인때 입력받은 비밀번호가 맞는지 대조
  • 만약 로그인이 된다면 페이지를 이동할 때 마다 로그인상태가 유지되게 하도록 token을 생성하여 Frontend에게 전달

🤔 Code

settings.py


import my_settings		
from pathlib import Path

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = my_settings.SECRET_KEY
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = ['*']		


# Application definition

INSTALLED_APPS = [
   #  'django.contrib.admin',
   #  'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'corsheaders',
    'accounts',
    'feeds'
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
   #  'django.middleware.csrf.CsrfViewMiddleware',
   #  'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'corsheaders.middleware.CorsMiddleware'

]

ROOT_URLCONF = 'project_westagram.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'project_westagram.wsgi.application'


# Database
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases

DATABASES = my_settings.DATABASES


# Password validation
# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/3.1/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.1/howto/static-files/

STATIC_URL = '/static/'

#REMOVE_APPEND_SLASH_WARNING
APPEND_SLASH = False

##CORS
CORS_ORIGIN_ALLOW_ALL=True
CORS_ALLOW_CREDENTIALS = True

CORS_ALLOW_METHODS = (
    'DELETE',
    'GET',
    'OPTIONS',
    'PATCH',
    'POST',
    'PUT',
)

CORS_ALLOW_HEADERS = (
    'accept',
    'accept-encoding',
    'authorization',
    'content-type',
    'dnt',
    'origin',
    'user-agent',
    'x-csrftoken',
    'x-requested-with',
		#만약 허용해야할 추가적인 헤더키가 있다면?(사용자정의 키) 여기에 추가하면 됩니다.
)
  1. allowed hosts를 모두 허용시켰다.
  2. admin과정을 직접 구현 하기위해 Installed_apps와 Middleware에서 로그인과 관련된 코드를 주석처리 하였다.
  3. Installed apps와 middleware에 corsheaders관련 코드를 삽입
  4. 데이터베이스와 시크릿키는 해킹의 위험이 있기 때문에 my_settings.py에 따로 옮겼고 맨 위에 import my_settings를 해주었다. my_settings는 꼭 manage.py와 같은 위치에 만들어야 한다.
  5. 마지막 줄에 역시 corsheaders관련 코드를 삽입

models.py

from django.db import models
class User(models.Model):
    phone_number = models.CharField(max_length=20)
    email        = models.EmailField(max_length=100)
    name         = models.CharField(max_length=20)
    username     = models.CharField(max_length=40)
    password     = models.CharField(max_length=100)

    class Meta:
        db_table = 'users'

<프로젝트를 하며 배운 점>

  1. 코드 가독성 향상을 위해 제일 긴 글자 기준 한칸을 띄고 등호를 정렬할 것.

상위 urls.py

from django.urls import path,include

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

하위 urls.py

from django.urls import path
from .views import SignupView,SigninView
urlpatterns = [
    path('/signup',SignupView.as_view()),
    path('/signin',SigninView.as_view())
    ]

회원가입 Views.py

import json		# python 객체를 JSON 데이터로 만들어서 쓰기 위해서 import
import re		# 정규표현식을 위해 import
import bcrypt		# 암호화를 위해 import
import jwt		# token 생성을 위해 import

from django.views import View		# 장고 내부의 View를 import
from django.http  import JsonResponse	# json형태로 response하기 위해 import

from .models      import User		# modeling한 것을 import

class SignupView(View):			# 회원가입 View 클래스 구현
    def post(self,request):		# http로 request를 받는다.
        data = json.loads(request.body)	# json형태로 온 body내용을 파이썬형태로 푼다.

        try:
            name                = data['name']
            phone_number        = data['phone_number']
            email               = data['email']
            username            = data['username']
            password            = data['password']
            email_vaildation    = re.match('[a-zA-Z0-9._+-]+@[a-z0-9-]+\.[a-z.]+',email)		# 이메일 유효성검사 정규표현식
            password_vaildation = re.match('^(?=.*[a-zA-Z0-9.,-]).{8,}$',password)		# 패스워드 유효성검사 정규표현식
            
            if name =='' or (phone_number=='' and email=='') or username == '' or password =='':
                return JsonResponse({'message' : 'Check your Input'}, status = 400)

            if not email_vaildation:
                return JsonResponse({'messsage' : 'Check your email'}, status = 400)

            if not password_vaildation:
                return JsonResponse({'message' : 'Check your password'}, status = 400)

            if User.objects.filter(phone_number = phone_number).exists():
                return JsonResponse({'message' : 'Already exists phone_number'}, status = 400)

            if User.objects.filter(username = username).exists():
                return JsonResponse({'message' : 'Already exists username'}, status = 400)

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

            hashed_password     = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')     # b까지 스트링으로 치기 때문에 encode후에 다시 decode하는 것!

            User.objects.create(
                name         = name,
                phone_number = phone_number,
                email        = email,
                username     = username,
                password     = hashed_password
                )
            return JsonResponse({'message' : 'Success!'}, status = 201)

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

<프로젝트를 하며 배운 점>

  1. import도 파이썬에서 import 해온 모듈, django에서 import해온 모듈, 내부 앱에서 import 해온 모듈을 나눠서 정리
  2. 처음은 exists()를 사용하지 않아서 메모리를 더 많이 사용했다. filter().exists()를 이용하여 T or F 만 사용하여 메모리를 효율적으로 다루기
  3. hashed_password를 위에 변수 선언했지만 굳이 잘못 접근한 클라이언트의 비밀번호까지 암호화를 해야되나? 라는 말씀을 하셔서 모든 if문을 거친 클라이언트에 한하여 패스워드 암호화 진행
  4. string형태의 password를 byte형태로 인코딩을 했는데 왜 다시 디코딩을 하는지?
    = 데이터베이스에 인코딩 된 상태로 들어가면 b''까지 패스워드로 인식하게 되어 오류 발생 따라서 무조건 인코딩 후 디코딩까지 실시하기!
  5. 메시지는 유저에게 전달하는 것이 아닌 Frontend 에게 전달을 해주는 것이기 때문에 친절하게 한글로 적을 필요없이 영어로 적기.
  6. 에러메시지의 형태는 통일하기 ('_'를 사용할 것인가, 모두 대문자로 적을것인가 등등)
  7. if와 if 사이에는 한칸 띄어 가독성을 향상 시키기
  8. phone_number = data.get('phone_number','None')을 사용하면 request 받은 body에 phone_number에 대한 내용이 있으면 그 내용을 데이터에 저장, 없으면 None으로 저장한다.

로그인 Views.py

class SigninView(View):
    def post(self,request):
        data = json.loads(request.body)
        try:
            user     = data['user']
            password = data['password']

            if User.objects.filter(username=user).exists():
                user = User.objects.get(username=user)

            elif User.objects.filter(email=user).exists():
                user = User.objects.get(email=user)

            elif User.objects.filter(phone_number=user).exists():
                user = User.objects.get(phone_number=user)

            else:
                return JsonResponse({'message': 'Check your ID'},status = 404)
         
            if bcrypt.checkpw(password.encode('utf-8'), user.password.encode('utf-8')):
                token = jwt.encode({'user_id': user.id}, 'secret', algorithm='HS256')
                return JsonResponse({'token' : token,'message' : 'Success!'}, status=200)

            else:
                return JsonResponse({'message': 'Check your password'}, status = 401)

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

<프로젝트를 하며 배운 점>

  1. 처음 코드는 중복되는 코드가 많아서 완전 갈아 엎었다...
  2. 아이디를 잘못 기입한 유저는 잘못된 접근이기 때문에 401 보다는 404를 사용하기
  3. token은 해킹의 위험이 있기 때문에 개인정보는 담으면 안된다.
  4. bcrypt.checkpw()를 이용하여 유저로부터 입력받은 패스워드를 인코딩 한값(byte형태로 변환)과 데이터베이스 안에있는 값을 인코딩 한값과 맞는지 확인하기

비밀번호 암호화 후 나의 DB

🤔 전체 Views.py

import json
import re
import bcrypt
import jwt

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

from .models      import User

class SignupView(View):
    def post(self,request):
        data = json.loads(request.body)

        try:
            name                = data['name']
            phone_number        = data['phone_number']
            email               = data['email']
            username            = data['username']
            password            = data['password']
            email_vaildation    = re.match('[a-zA-Z0-9._+-]+@[a-z0-9-]+\.[a-z.]+',email)
            password_vaildation = re.match('^(?=.*[a-zA-Z0-9.,-]).{8,}$',password)
            
            if name =='' or (phone_number=='' and email=='') or username == '' or password =='':
                return JsonResponse({'message' : 'Check your Input'}, status = 400)

            if not email_vaildation:
                return JsonResponse({'messsage' : 'Check your email'}, status = 400)

            if not password_vaildation:
                return JsonResponse({'message' : 'Check your password'}, status = 400)

            if User.objects.filter(phone_number = phone_number).exists():
                return JsonResponse({'message' : 'Already exists phone_number'}, status = 400)

            if User.objects.filter(username = username).exists():
                return JsonResponse({'message' : 'Already exists username'}, status = 400)

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

            hashed_password     = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')     # b까지 스트링으로 치기 때문에 encode후에 다시 decode하는 것!

            User.objects.create(
                name         = name,
                phone_number = phone_number,
                email        = email,
                username     = username,
                password     = hashed_password
                )
            return JsonResponse({'message' : 'Success!'}, status = 201)

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

class SigninView(View):
    def post(self,request):
        data = json.loads(request.body)
        try:
            user     = data['user']
            password = data['password']

            if User.objects.filter(username=user).exists():
                user = User.objects.get(username=user)

            elif User.objects.filter(email=user).exists():
                user = User.objects.get(email=user)

            elif User.objects.filter(phone_number=user).exists():
                user = User.objects.get(phone_number=user)

            else:
                return JsonResponse({'message': 'Check your ID'},status = 404)
         
            if bcrypt.checkpw(password.encode('utf-8'), user.password.encode('utf-8')):
                token = jwt.encode({'user_id': user.id}, 'secret', algorithm='HS256')
                return JsonResponse({'token' : token,'message' : 'Success!'}, status=200)

            else:
                return JsonResponse({'message': 'Check your password'}, status = 401)

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

🤔 서버를 돌릴때

  1. 나 혼자만 테스트 할 때
    python manage.py runserver
  2. 같은 IP (공유기)를 사용하고 있는 사람에게 서버를 개방할 때
    python manage.py runserver 0:8000

🤔 나의 IP 확인하기

ipconfig getifaddr en0

🤔 Frontend에게 api address를 줄때는?

http://본인아이피주소:8000/지정된 path

🤔 401 error와 403 error의 차이?

둘다 권한이 없어서 생기는 error로 알고 있는데..
무슨 차이인지 궁금하여 찾아보았다.

  • 401: 익명의 사용자 즉, 당신이 누구인지 모릅니다.
  • 403: 로그인은 하였으나 권한이 없는 사용자
    즉, 당신이 누구인지는 알고 있으나 여기에 접근할 권한이 없다.
profile
생각하는 개발자 되기

0개의 댓글