서버개발자가 되는법 [3] - API만들기, 로그인부터 만들자

     

목차

 

2020/09/29 - [Study/서버] - 서버개발자가 되는법 - 목차

 

git - 링크

 

 

 

유튜브 - 빠르게 따라하실 분은 동영상을 생략하셔도 됩니다 :)

 

 

들어가기 전에

 

지난시간까지 서버 환경 구축을 열심히 했는데여, 이제 본격적으로 클라이언트와 통신 부분을 만들어보려고 합니다. 아마 여러분이 서버 개발자로 회사에 들어간다면 가장 많이 할 업무 중에 하나일 거예요. 앱에 새로운 기능을 추가한다거나, 홈페이지에 새로운 데이터를 보여주는 기능을 추가하거나 할 때 항상 서버에서 데이터를 내려줘야 하기 때문이죠. 

 

하지만 회사마다 구조가 다르고 환경이 다르기 때문에, 여기서 하는 것과 다를 수 있습니다. 운 좋게 Django를 하는 곳에 가서 Django Rest Framework를 사용해서 API를 만든다면 운이 좋은 거고, 어디는 Spring을 사용할 수 도 있고, 어디는 Ruby on Rails를 쓸 수도 있습니다. 따라서 언어를 기반으로 배운다기보다는 전체적인 흐름이 어떻게 돌아가는지를 중점적으로 배우시면 좋겠습니다. 

 

먼저 어떤 목적으로 기능을 추가해야 하는 건지 기획단계부터, 클라이언트와 어떤 방식으로 통신할 건지, 서버에서는 어떤 테이블의 어떤 데이터를 내려줘야 하는지 등 여러 가지 상황이나 업무 프로세스의 흐름 등을 보시면 좋을 것 같아요. 제 생각에 가장 중요한 것은 "왜?"입니다. 일은 하는데 "이걸 왜 만들어야 하지?"라는 물음은 항상 필요합니다. 대기업이든 스타트업이든요. 자기가 어떤 일을 하는지 의미를 부여하는 것만큼 중요한 건 없다고 생각합니다. 진행되는 일이 어떤 건지 고민을 하다 보면 프로덕트를 좀 더 이해하게 되고, 사용자 관점에서 생각하게 되며 더 효율적인 방법을 찾을 수 있거든요. 시키는 일만 하는 개발자가 되지는 맙시다!! (갑분 캠페인 ㅋㅋㅋㅋ)

 

아 그리고 제가 여기서 말하는 API(Application Process Interface) 대한 정의가 상당히 애매한데, 회사마다 부르는 말도 다릅니다ㅋㅋㅋ 기본적인 정의는 클라이언트가 서버에 호출해서 데이터를 수정하거나 조회하는 작업을 말합니다. 기본적인 개념은 RPC(Remote Procedure Call)인데, 이걸 RPC라고 부르는 데는 거의 없더라고요. 대부분 API라고 부르거나 어디는 TR(Transaction)이라고 부릅니다. 어떻게 부르든 클라이언트에서 필요로 하는 작업을 해준다는 것은 변함없기 때문에 개념만 알아두시면 될 것 같습니다.

 

 

REST를 사용하기 위해 DRF 설치하기 

 

Django 프레임워크에서 REST API를 사용하기 위해서 django-restframework라는 패키지를 설치합니다. 이걸 설치해야만 API를 만들 수 있는 건 아니지만 보다 쉽게 만들 수 있게 도와주는 패키지라고 생각하시면 됩니다. 

 

pip install djangorestframework

아래는 공홈 링쿠

https://www.django-rest-framework.org/

 

Home - Django REST framework

 

www.django-rest-framework.org

 

보통 원티드나 구인 사이트에서 서버 개발자 필요 스펙을 보면, Django 서버를 사용할 경우 항상 같이 따라오는 게 있는데 바로 "DRF"입니다. 왜 굳이 약자로 쓰는지는 모르겠는데.. Django Rest Framework의 약자입니다. 즉 Django로 서버를 사용하는 회사는 대부분 DRF를 사용한다는 거죠. 그러니까 무조건 쓰세요. (Django 안 할 거면 말고)

 

기초적인 DRF 사용법은 이전에 포스팅도 해놨고 위에 공홈에 들어가도 상세한 Tutorial이 나옵니다.

https://cholol.tistory.com/467

 

dJango로 restful API 서버만들기 [1] - django 서버 생성

소스코드 GIT : https://github.com/tkdlek11112/django_restful 강의영상 YouTube restfulAPI 서버란? API 서버인데 일종의 규칙을 적용하여 사용하기 편리하게 만든것! (자세한것은 구글링이나 영상 참조 ><) 개..

cholol.tistory.com

 

DRF를 설치하면 이제 DRF에서 제공하는 편리한 기능들을 사용해서 빠르게 API를 만들 수 있습니다. 일단 어느 앱에서나 무조건 필요한 로그인 API를 만들면서 기본적인 사용법을 알려드릴게요.

 

 

그전에 mysql 연동하기

 

이전에 포스팅에서 docker로 mysql을 띄우는 예제가 있었는데, 시작하기에 앞서 mysql과 django를 제대로 연결시키고 시작하겠습니다. API를 만드는 데 있어 DB가 중요하기 때문에,,,

 

docker run -d -it --rm --name mysql-db -p 3306:3306 -e MYSQL_ROOT_PASSWORD=admin123! -v /home/ubuntu/mysql/data:/var/lib/mysql -e MYSQL_PASSWORD=admin123! mysql:5.7 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci

 

docker가 설치되어 있다면 위 스크립트를 실행하면 자동으로 mysql이 실행됩니다. 

MYSQL_ROOT_PASSWORD=에 여러분이 설정할 패스워드를 입력하시면 됩니다. 저는 admin123!로...

위 명령어를. sh파일로 저장해 노면 나중에 복붙 하지 않고도 쉽게 실행할 수 있습니다.

 

mysql-docker.sh로 만들면 나중에 쉽게 실행할 수 있음

vi mysql-docker.sh로 파일을 만든 다음 위 명령어를 복사해놓기만 하면 됩니다.

단 실행파일로 설정되어있지 않기 때문에 그냥 실행하려면 sh mysql-docker.sh로 실행해야 하고, 그냥 파일 이름만 입력했을 때 실행되게 하려면 chmod를 살짝 바꿔줍니다.

 

chmod +x [파일이름] 을 하면 실행파일로 변합니다.

위처럼 녹색으로 변하거나 파일 이름 뒤에 *가 붙으면 실행파일로 변했다는 뜻입니다. (리눅스 버전마다 표기방법이 다름)

이제 그냥 mysql-docker.sh를 실행하면 docker가 실행됩니다.

 

이제 실제로 mysql이 실행되었는지 datagrip툴을 이용해서 붙어볼까요? DB 접속하는 툴은 Golden, orange, toad, mysql-workbench 등등이 있는데 입맛대로 쓰시면 됩니다. 저는 jetbrain 빠라서 datagrip을 씁니다. ㅋㅋㅋ

 

DB에 접속하기 전에 AWS console에 들어가서 3306 포트에 대한 방화벽을 확인해줘야 합니다. 내부에서 접속할 때는 상관없긴 한데, 외부에서 mysql에 접속하기 위해서는 3306 포트에 대한 방화벽을 해지해야 합니다.

 

역시 안되있쮸?
MYSQL/Aurora를 선택하면 3306포트로 선택됩니다. 그냥 TCP 3306해도 됩니다.

 

이제 방화벽도 풀었기 때문에 DB TOOL에서 접속할 수 있습니다.

 

HOST, ID, PASSWORD를 입력하면 끝 HOST는 AWS 주소 입력하면됩니다.

서버 정보를 입력하고 기본 계정은 root로 되어있습니다. root password는 아까 docker 실행할 때 옵션으로 주었던 password를 입력하면 됩니다.

 

연결 완료

연결되면 기본적인 mysql 시스템 테이블들만 보입니다. 

아직 Django에서 사용되는 DB가 없는 상태인데, Django setting.py에서 DB설정을 해주고 migrate를 한번 해줘야 테이블들이 생성됩니다.

 

# settings.py

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'HOST': 'choaws.duckdns.org',
        'NAME': 'server_dev',
        'USER': 'root',
        'PASSWORD': 'admin123!',
        'PORT': '3306',
        'OPTIONS': {'charset': 'utf8mb4'},
    }
}

 

settings.py에 DB 설정을 보면 아마 sqlite3으로 되어있을 겁니다. default로 sqlite3을 사용하는데 이걸 mysql로 바꿔주고 기타 정보들을 입력해주면 됩니다. NAME의 경우 migrate를 하고 나서 생성되는 DB Schema의 이름이기 때문에 취향에 맞게 설정해주시면 됩니다.

 

전부 설정하고 로컬에서 실행을 시켜보면..

 

mysqlclient 설치했니?

친절하게도 mysqlclient를 설치하라고 알려주네요. 만약 conda 가상 환경을 사용하시는 분이라면 이 메시지가 뜨지 않을 거예요. 왜냐면 conda에는 이미 설치가 되어있어서 ㅎㅎ 그냥 venv 쓰시는 분들은 에러가 뜨는데, pip를 이용해 mysqlclient라는 패키지를 다운로드하면 됩니다.

 

호락호락하게 설치 안됨

하지만 pip install mysqlclient 하면 위와 같은 에러가 나올 겁니다. 윈도우 XX#@%SD

구글링 해주면 따로 다운로드하여서 설치하라고 하네요. 

 

https://www.lfd.uci.edu/~gohlke/pythonlibs/#mysqlclient

위 url로 들어가서 mysqlclient 본인의 PC 사양에 맞게 다운로드하면 됩니다. 

저는 mysqlclient‑1.4.6‑cp38‑cp38‑win_amd64.whl 이걸 다운로드하였습니다. (python3.8, amd64이기 때문!)

다운로드한 파일을 프로젝트에 옮기고 신나게 명령어를 쳤는데...?!

 

아놔 ㅋㅋㅋㅋㅋ

여러분 맥 쓰세요

그래서 문서를 좀 읽어보니까 whl설치는 pip가 최신 버전이어야 한다고 하더라구요. ㅡㅡ

그래서 pip를 20.2.2 버전으로 올렸습니다.

 

파이참 설정에서 20.2.2를 선택해서 다운받을 수 있습니다.

 

휴 20.2.2로 올리고 나서 명령어를 다시 치니까 설치 완료!!

 

긴 싸움 이었다...

 

이제 실행해봅시다 ㅎㅎㅎ

 

server_dev 찾을 수 없음

 

이제 위 에러가 나올 텐데요, 당연히 나오는 에러입니다!! 

settings.py에 선언했던 server_dev DB가 아직 안 만들어져서 나오는 에러인데 일단 mysql에 server_dev라는 DB를 만들어주고, migrate를 진행해야 합니다.

 

 

server_dev schema 생성

일단 datagrip에서 schema 생성해주고..

 

makemigrations + migrate

migrate 하면...

 

기본 테이블 생성

Django 기본 테이블들이 만들어집니다. 이젠 앱 실행 가능!!

 

앱 실행 완료

 

 

LOGIN API 만들어보기

 

사실 LOGIN이란 API가 간단하게 만들면 한없이 간단하고, 복잡하게 만들면 한없이 복잡해지는 서비스입니다. 증권회사에서 일할 때 제일 길었던 소스 중에 하나가 LOGIN 소스였습니다 ㅋㅋㅋ

 

일단 가장 간단한 버전으로 ID와 PASSWORD를 입력하면 서버에 저장된 ID, PASSWORD와 비교해서 성공/실패를 내려주는 API를 만들어 봅니다.

 

Django에는 기본적으로 admin 사용자를 위한 사용자 테이블이 제공됩니다. 일단은 이 테이블을 사용하지 않고, 새로운 Model을 만들어서 실습해보겠습니다. 일단 앱을 만들어야겠죠?

 

startapp login

login이라는 이름으로 app을 하나 만듭니다. 이렇게 만들면 프로젝트 안에 login이라는 폴더가 자동으로 생성됩니다.

 

login 폴더 안에 여러가지 .py파일이 생깁니다.

 

우선 models.py에서 사용자의 ID와 PW를 저장할  모델 하나를 만들어줍니다.

다른 건 필요 없고 그냥 ID랑 PW만... ㅋㅋㅋ

 

#login/models.py
from django.db import models


class LoginUser(models.Model):
    user_id = models.CharField(max_length=20, null=False, default=False)
    user_pw = models.CharField(max_length=20, null=False, default=False)

    class Meta:
        db_table = 'login_user'
        verbose_name = '로그인 테스트 테이블'

 

모델 만들었습니다. 최대길이가 20바이트인 user_id와 user_pw필드를 만들었습니다. 이제 makemigrations와 migrate를 해주면 DB에 반영됩니다. migrate를 하기 전에 settings.py에서 INSTALLED_APP에 login을 추가해야 합니다.

 

#server_dev/settings.py
...
INSTALLED_APPS = [
    ...
    'rest_framework',
    'login'
]
...

 

이제 migrate를 해봅시다.

 

makemigrations + migrate

 

테이블이 생김!

 

테이블을 보면 우리가 만든 user_id, user_pw 말고도 id필드가 생겨있는 걸 볼 수 있는데, 장고 모델은 default로 pk값으로 사용하는 id 필드를 추가하기 때문에 생긴 필드입니다. 그냥 필드가 생길 때마다 1씩 자동으로 증가하는 index라고 생각하시면 됩니다.

 

이제 모델은 만들었고 여기에 id와 pw를 생성하는 api를 먼저 만들어볼까요? 아마 회원가입이겠죠?

login 앱 폴더 안에 있는 views.py 파일에 새로운 api call을 만듭니다.

 

# login/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from .models import LoginUser


class RegistUser(APIView):
    def post(self, request):
        user_id = request.data.get('user_id', "") # 클라이언트에서 올리는 user_id
        user_pw = request.data.get('user_pw', "") # 클라이언트에서 올리는 user_pw

        LoginUser.objects.create(user_id=user_id, user_pw=user_pw) # LoginUser 모델에 새로운 object 생성

        # 클라이언트한테 내려줄 데이터 정의
        data = dict(
            user_id=user_id,
            user_pw=user_pw
        )

        return Response(data=data)

 

RegistUser라는 클래스를 만들고 post로 요청이 들어올 경우 LoginUser 모델에 새로운 object를 만들게 합니다. 이제 특정 url을 통해 RegistUser를 호출할 수 있도록 urls.py을 설정합니다.

 

기본적으로 startapp을 통해 django app을 만들면 urls.py 파일은 생성되지 않습니다. 그래서 new file로 urls.py를 만들어줍니다.

# login/urls.py
from django.conf.urls import url
from . import views


urlpatterns = [
    url('regist_user', views.RegistUser.as_view(), name='regist_user'),
]
# server_dev/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('login/', include('login.urls')),
]

 

login/urls.py에 ~/regist_user를 호출하면 RegistUser를 호출하도록 설정하고, root urls.py에는 include를 통해 login앱에 있는 urls.py파일을 링크시킵니다. 

 

이제 ~/login/regist_user를 통해 POST로 요청을 날리면 DB에 데이터를 쌓을 수 있습니다.

 

 

Insomnia나 postman을 통해 호출해봅니다.

 

저는 Insomnia라는 툴을 자주 쓰는데 보통 Postman이라는 툴을 많이 쓰더라구요. 어떤 거든 상관없이 POST를 날려봅시다. 저는 user_id에 mychew를 넣고, user_pw에 password123! 을 넣었습니다.

 

DB도 확인 완료

 

아주 간단하게 API를 호출해서 사용자의 ID PW를 저장하는 CALL을 만들었는데요, 아직 문제점이 많아 보입니다. 일단 툴을 사용해서 여러 번 호출해볼까요?

 

호출할 때마다 mychew라는 user_id 생성

 

사용자의 id는 중복되는 값 없이 유니크해야 하는데, 호출할 때마다 mychew라는 user_id가 생성되었습니다. 

 

클라이언트는 현재 서버에 어떤 값들이 저장되어있는지 모르기 때문에 중복되는 값이 있는지 체크하는 로직이 서버에 필요합니다. 뿐만 아니라 ID값이 정상적인 포맷인지 검증도 해야 하는데요, 서버에서 허용하는 문자가 맞는지와 같이 여러 가지 제약사항들을 검증하는 로직이 필요합니다. 

 

서버 개발자의 역할 중 제일 중요한 역할이 바로 이런 데이터/로직 검증입니다. 사실 INPUT을 받아서 OUTPUT을 주면 되는 간단한 로직은 정말 쉽습니다. 하지만 회사에서 어떤 서비스를 하냐에 따라 업무적인 로직(Business Logic이라고 흔히 부릅니다)이 들어갈 수밖에 없는데, 서버 개발자는 이 로직을 꿰고 있어야 합니다. 

 

간단한 로그인 API를 만들기에 있어서도 사용자의 ID를 어디까지 허용할 건지, 잘못된 값이 들어오면 어떤 응답을 클라이언트에 전달할 건지, 비밀번호를 암호화해서 저장할 건지 등 정해야 할 것이 많습니다. 이것들을 결정하고 적용하는 것 역시 서버 개발자의 몫입니다. (물론 큰 조직은 업무 로직을 결정하는 팀이 따로 있습니다. 하지만 적용은 개발자의 몫~)

 

일단 동일한 user_id값을 사용하지 못하도록 로직을 설정해볼까요?

 

# login/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from .models import LoginUser


class RegistUser(APIView):
    def post(self, request):
        user_id = request.data.get('user_id', "")
        user_pw = request.data.get('user_pw', "")

        if LoginUser.objects.filter(user_id=user_id).exists():
            data = dict(
                msg="이미 존재하는 아이디입니다."
            )
            return Response(data)

        LoginUser.objects.create(user_id=user_id, user_pw=user_pw)

        data = dict(
            user_id=user_id,
            user_pw=user_pw
        )

        return Response(data=data)

 

간단하게 if문을 통해서 입력된 user_id값에 해당하는 사용자가 있는지 검사하는 로직을 추가했습니다. 이전과 같이 mychew라는 아이디를 올려보겠습니다.

 

이미 존재하는 아이디입니다.

이제 중복된 아이디 생성은 피할 수 있겠군요. 하지만 코드상에서 예외처리를 한다고 해도 DB에 같은 값이 들어갈지도 모릅니다. Model 선언 부분에서 user_id 필드에 unique값을 준다면 더 확실하게 중복을 피할 수 있습니다.

 

# login/models.py
from django.db import models


class LoginUser(models.Model):
    user_id = models.CharField(max_length=20, unique=True, null=False, default=False)
    user_pw = models.CharField(max_length=20, null=False, default=False)

    class Meta:
        db_table = 'login_user'
        verbose_name = '로그인 테스트 테이블'

 

중복처리 외에도 특수문자 불가능, 숫자로 시작 불가능, 길이 10바이트 이상 등 여러 가지 제약조건 등을 설정해주어야 할지도 모릅니다. 물론 클라이언트에서 데이터에 대한 검증을 하고 서버로 전달하겠지만, 서버 개발자는 클라이언트를 믿지 않습니다 -_-++ (종특).

 

 

사용자 id만큼 중요한 게 있는데 바로 pw입니다. 이런 개인정보가 될 수 있는 정보들은 암호화를 해서 DB에 저장해야 합니다. 특히 password 같은 경우는 서버 개발자도 모르게 단방향으로 암호화해서 저장해야 합니다. 단방향과 양방향의 차이는, 단방향의 경우 암호화할 경우 어떻게 해서도 복호화가 불가능합니다. 따라서 서버 개발자도 특정 사용자의 비밀번호를 알 수 없습니다. 만약 알 수 있다면 큰일 나겠죠?

 

회사마다 각자 암호화하는 방식이 다르긴 한데 django model field encrypt라고 검색하니까 django용 암호화 패키지가 있더라고요. 그거 사용해 봅시다.

 

https://pypi.org/project/django-searchable-encrypted-fields/

 

django-searchable-encrypted-fields

Django model fields encrypted using Pycryptodome AES-256 GCM.

pypi.org

 

설치는 간단하게 pip

pip install django-searchable-encrypted-fields

 

그리고 settings.py에 아래 추가

 

# server_dev/settings.py
INSTALLED_APPS += ["encrypted_fields"]

FIELD_ENCRYPTION_KEYS = [
    "f164ec6bd6fbc4aef5647abc15199da0f9badcc1d2127bde2087ae0d794a9a0b"
]

 

 

이제 models.py에서 user_pw를 암호화 필드로 바꿉니다. 

 

# login/models.py
from django.db import models
from encrypted_fields import fields

class LoginUser(models.Model):
    user_id = models.CharField(max_length=20, unique=True, null=False, default=False)
    # user_pw = models.CharField(max_length=20, null=False, default=False)
    user_pw = fields.EncryptedCharField(max_length=20, null=False, default=False)
    class Meta:
        db_table = 'login_user'
        verbose_name = '로그인 테스트 테이블'
# login/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from .models import LoginUser
from encrypted_fields import fields


class RegistUser(APIView):
    def post(self, request):
        user_id = request.data.get('user_id', "")
        user_pw = request.data.get('user_pw', "")

        if LoginUser.objects.filter(user_id=user_id).exists():
            # DB에 있는 값 출력할 때 어떻게 나오는지 보려고 user 객체에 담음
            user = LoginUser.objects.filter(user_id=user_id).first()
            data = dict(
                msg="이미 존재하는 아이디입니다.",
                user_id=user.user_id,
                user_pw=user.user_pw
            )
            return Response(data)

        LoginUser.objects.create(user_id=user_id, user_pw=user_pw)


        data = dict(
            user_id=user_id,
            user_pw=user_pw
        )

        return Response(data=data)

 

응답을 좀 정확하게 보기 위해서 views.py도 손봤습니다. models.py를 수정했기 때문에 migrate를 다시 해주어야 합니다. 혹시 모르니까 DB에 데이터를 다 지우고 makemigrations와 migrate를 진행합니다.

 

진행하고 나서 아까와 같이 user_id와 user_pw를 올려보겠습니다.

 

일단 성공

일단 아까와 응답은 똑같은데... DB를 봐봅니다.

 

ㅋㅋㅋ 아니 만든사람 누구냨ㅋㅋㅋㅋ

DB에 헥사 값으로 저장을 해놨네요 ㅋㅋㅋㅋㅋㅋ 패키지 만든 사람이..... 조금 의심스럽네요 ㅋㅋㅋㅋㅋㅋ

뭐 누구나 패키지를 만들 수 있긴 하지만.. 이 패키지는 못쓸 것 같네요. 

뭐 일단 암호화는 제대로 되었습니다. 하지만 이 암호화 방법은 비밀번호를 암호화 하기에는 적절하지 않습니다.

왜냐하면...

 

동일한 아이디를 올릴경우 DB에 있는 값 조회함

아까 views.py를 조금 수정해서 동일한 user_id를 올리 경우 DB에 있는 값을 그대로 내려주도록 수정했습니다. 근데 DB에 있는 값을 그대로 내려준다면 암호화된 user_pw를 내려주어야 하는데 복호화되어 내려오는 것을 확인할 수 있습니다. 일반적인 개인정보 같은 경우 이렇게 되어도 상관없지만, 비밀번호는 단방향 암호화여야 합니다. 따라서 복호화가 불가능해야 하기 때문에 이 암호화 방법은 사용할 수 없겠군요. 대신 생년월일이나 핸드폰 번호와 같은 개인정보를 암호화하는 경우는 사용 가능합니다.

 

그럼 비밀번호는 어떻게 암호화하죠? 

 

사실 Django에서 제공하는 user테이블을 사용하면 기본적으로 password가 암호화 필드로 먹혀있고 자동으로 단방향 암호화가 되기 때문에 기본 user모델을 사용하는 것도 하나의 방법입니다. 하지만 우리는 따로 테이블을 만들었기 때문에 user모델과 비슷한 기능을 하도록 user_pw필드를 바꿔봅시다. 사실 filed를 바꾸는 건 아니고 데이터를 모델에 집어넣을 때 다른 암호화 로직을 이용해 데이터를 바꿉니다.

 

# login/models.py
from django.db import models
from encrypted_fields import fields


class LoginUser(models.Model):
    user_id = models.CharField(max_length=20, unique=True, null=False, default=False)
    user_pw = models.CharField(max_length=255, null=False, default=False)
    # user_pw = fields.EncryptedCharField(max_length=20, null=False, default=False)
    class Meta:
        db_table = 'login_user'
        verbose_name = '로그인 테스트 테이블'
# login/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from .models import LoginUser
from django.contrib.auth.hashers import make_password


class RegistUser(APIView):
    def post(self, request):
        user_id = request.data.get('user_id', "")
        user_pw = request.data.get('user_pw', "")
        user_pw_crypted = make_password(user_pw)    # 암호화

        if LoginUser.objects.filter(user_id=user_id).exists():
            # DB에 있는 값 출력할 때 어떻게 나오는지 보려고 user 객체에 담음
            user = LoginUser.objects.filter(user_id=user_id).first()
            data = dict(
                msg="이미 존재하는 아이디입니다.",
                user_id=user.user_id,
                user_pw=user.user_pw
            )
            return Response(data)

        LoginUser.objects.create(user_id=user_id, user_pw=user_pw_crypted)

        data = dict(
            user_id=user_id,
            user_pw=user_pw_crypted
        )

        return Response(data=data)

 

models.py에서 user_pw 필드를 다시 charfield로 바꾸고 길이를 조금 늘려줍니다. 암호화하면 보통 길이가 길어지기 때문에 넉넉하게 늘려줍니다. views.py에서는 make_password라는 django 기본 함수를 사용해 user_pw를 암호화합니다. 이제 아까와 같이 user_id와 user_pw를 올려보겠습니다.

 

암호화~!

암호화된 게 보이시나요? DB 역시 암호화된 값으로 user_pw가 들어가 있는 것을 확인할 수 있습니다. 이 값은 절대 복호화할 수 없는 값이기 때문에 사용자의 비밀번호를 아무도 알 수 없습니다.

 

그럼 여기서 궁금증이 생기 실 텐데요. 아무도 비밀번호를 모른다면 어떻게 비밀번호를 체크해서 로그인을 시킬까요?

 

방법은 생각보다 간단합니다. 클라이언트에서 올린 user_pw를 동일한 방법으로 암호화해서 암호화된 값을 비교하는 방식으로 사용자를 검증할 수 있습니다. 복호화가 불가능할 뿐이지 같은 데이터를 암호화하면 항상 같은 데이터가 나오기 때문이죠. 따라서 사용자의 비밀번호가 어떤 값인지는 모르지만, 클라이언트에서 올라온 user_pw를 동일한 방법으로 암호화한 결괏값이 DB에 저장된 user_pw값과 같다면, 클라이언트에서 올라온 user_pw값은 초기에 사용자가 가입할 때 입력한 user_pw값과 같다는 것을 알 수 있습니다. 

 

말은 복잡하지만 django에서 제공하는 check_password로 간단하게 검증할 수 있습니다.

 

# login/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from .models import LoginUser
from django.contrib.auth.hashers import make_password, check_password


class AppLogin(APIView):
    def post(self, request):
        user_id = request.data.get('user_id', "")
        user_pw = request.data.get('user_pw', "")
        user = LoginUser.objects.filter(user_id=user_id).first()
        if user is None:
            return Response(dict(msg="해당 ID의 사용자가 없습니다."))
        if check_password(user_pw, user.user_pw):
            return Response(dict(msg="로그인 성공"))
        else:
            return Response(dict(msg="로그인 실패. 패스워드 불일치"))


class RegistUser(APIView): 
    ...
# login/urls.py
from django.conf.urls import url
from . import views


urlpatterns = [
    url('regist_user', views.RegistUser.as_view(), name='regist_user'),
    url('app_login', views.AppLogin.as_view(), name='app_login'),
]

 

check_password를 사용해보기 위해 AppLogin이라는 API를 새로 만듭니다. 실제로 클라이언트에서 올라온 user_id와 user_pw를 검증해서 로그인을 시켜주는 API입니다.

 

AppLogin 내부를 보면 클라이언트에서 올라온 user_id를 가진 사용자가 있는지 검색하고, 있을 경우 그 사용자의 user_pw가 클라이언트에서 올라온 user_pw와 일치하는지 check_password라는 함수를 통해 검증하고 있습니다. check_password는 비교하는 두 값이 같으면 True, 다르면 False를 리턴합니다. 따라서 ~/login/app_login이라는 url로 user_id와 user_pw를 호출하면 아래와 같이 결과값이 출력됩니다.

 

성공
실패

 

make_password와 check_password를 이용해 단방향 암호화 비밀번호를 구현했습니다. 물론 이 방법 말고도 암호화 방법은 매우 많습니다. 이 방법 역시 서버 개발자와 업무 로직 담당자가 결정해야 할 몫입니다 ^오^

 

 

마치며

 

이번 포스팅에서는 Django에서 REST API를 만들기 위해 DRF를 설치해보고, 간단하게 LOGIN API를 만들어보았습니다. 실제로 API를 만들 때 서버 개발자 입장에서 고려해야 할 부분이 상당히 많습니다. 단순하게 DB에 있는 데이터를 내려주는 거는 상관없는데 특정 로직이 필요한 API의 경우 데이터 검증이나 예외처리 등등 파생되는 업무들이 많아집니다. 

 

규모가 큰 서비스의 경우 데이터가 산재되어 있어 필요한 데이터를 한 번에 내려주는 것 역시 번거로운 작업이 됩니다. 여러 테이블에 나뉘어 저 있는 데이터를 어떤 key를 사용해서 조회하고 어떤 방식으로 내려줄지 결정하는 것 역시 서버 개발자의 역할인데, 이걸 잘하는 서버개발자 일 잘하는 서버 개발자입니다. 

 

이번에는 간단하게 LOGIN에서 발생할 수 있는 예외/제약사항들을 살펴봤는데 위에 언급한 것 말고도 상당히 많은 제약사항들이 존재합니다. 기본적으로 LOGIN에서 빼놓을 수 없는 것이 보안과 세션 유지인데, JWT나 OAUTH2.0과 같은 이름의 보안 기술들을 들어보셨을 겁니다. 이 것들 중 어떤 것을 어떤 방식으로 적용할지 역시 서버 개발자의 몫이기 때문에 생각보다 서버 개발자가 할 일이 많습니다. 

 

보안 같은 경우는 이번 시리즈에서 다룰지 안 다룰지 모르겠습니다만, 간단하게 이론 정도는 짚고 넘어가는 방식으로 진행할 예정입니다. 

 

다음 포스팅에서는 이번 포스팅에 이어서 LOGIN 관련 API 및 추가적인 API를 설계할 때 어떻게 접근해야 하는지에 대해 다루도록 하겠습니다.

 

 

 

 

 

 

 

반응형

댓글

Designed by JB FACTORY