dJango로 restful API 서버만들기 [3] - 웹에서 로그인 화면 만들어서 날려보기
- Study/python
- 2019. 11. 10. 03:03
들어가기 전에
지난 시간에는 간단히 restAPI서버를 만들었습니다. 로그인 기능도 살짝 넣어봤구요. 이번시간에는 장고에서 기본으로 제공하는 User모델을 이용해서 사용자 인증하는 기능을 만들어보겠습니다. 추가로 장고도 템플릿기능을 제공하기 때문에 ID와 PW를 입력할 수 있는 웹페이지를 만들어서 실제로 ID와 PW를 입력받아 테스트 해볼 수 있도록 만들예정입니다.
소스는 GITHUB, 자세한 설명을 원하시면 유튜브 동영상을 참조하시고 빠르게 보고싶으시면 블로그만 보시면 됩니다.
이전 글
2019/10/14 - [Study/python] - dJango로 restful API 서버만들기 [1] - django 서버 생성
2019/10/17 - [Study/python] - dJango로 restful API 서버만들기 [2] - rest framework 적용
동영상으로 설명듣기 -
소스 - https://github.com/tkdlek11112/django_restful
django에서 로그인 화면 만들어보기
장고도 일단은 웹서버 프레임워크이기 때문에 웹페이지를 만들 수 있습니다. html로 간단하게 로그인 페이지를 만들어서 화면에 띄워보도록 할게요.
<!DOCTYPE html>
<html lang="en">
<body>
<form class="form-signin" method="POST" action="/login/">
<h1 class="h3 mb-3 font-weight-normal">로그인 테스트 화면</h1>
<label for="inputId" class="sr-only">ID</label>
<input type="text" id="userid" name="userid" class="form-control" placeholder="ID" autofocus=""/>
<label for="inputPassword" class="sr-only">Password</label>
<input type="password" id="userpw" name="userpw" class="form-control" placeholder="Password"/>
<div class="checkbox mb-3">
<label>
<input type="checkbox" value="remember-me"> Remember me
</label>
</div>
<button class="btn btn-lg btn-primary btn-block" type="submit">로그인</button>
</form>
</body>
</html>
대충 이런 소스.. 사실 화면 만드는 건 너무 못해서... 복붙입니다 ㅋㅋㅋㅋ 뭐 중요한 건 이 로그인 테스트 페이지가 로그인 버튼을 누를 때 "POST" 방식으로 "/login/" url을 호출한다는 것만 알면 됩니다. 일단 이 html을 화면에 띄우려면 프로젝트에 넣어야 되는데 django에서는 template라는 폴더를 사용합니다. settings.py소스를 가보면 프로젝트에 기본 template 경로를 설정하는 소스가 있습니다.
# restfulapiserver/settings.py
..
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR,'templates')],
'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',
],
},
},
]
..
이런 식으로 코드가 적혀있습니다. 일반적으로 templates폴더 역시 앱별로 관리가 되는데요, 보통 templates 폴더 안에 앱 이름별로 폴더를 만들어 관리합니다. 그래서 폴더 구조가 상당히 이상한 구조가 되는데 "프로젝트 이름 -> 앱 이름 -> 템플릿 -> 앱 이름" 같은 앱 이름을 갖는 폴더가 두 개 생기게 됩니다. 왜 이렇게 만들었는지는 모르겠는데... 아무튼 이렇게 생깁니다 ㅋㅋㅋㅋ 저는 그냥 루트 경로에 템플릿 폴더를 만들고 그 안에 앱 이름으로 구분되어 관리하도록 하겠습니다. 'DIRS'에 루트 경로의 templates를 추가함으로써 루트 경로의 templates 폴더에 접근할 수 있습니다.
그럼 프로젝트에서 templates 폴더를 만들고, 그 안에 앱 이름을 써줍니다. 제가 만들어놓은 앱 이름은 addresses라서 templates/addresses 폴더를 만들면 됩니다.
이렇게 폴더를 만들고 html문서도 만들어 줬다면, 특정 url로 접속했을 때 login.html을 브라우저에 띄우는 작업을 해야겠죠? 장고에서 모든 url관리는 urls.py~!
#restfulapiserver/urls.py
urlpatterns = [
url('^$', views.login_page),
..
]
그냥 쌩 url만 입력했을 경우 views.login_page를 실행하도록 설정해놨습니다. 이제 views.py에 가서 login_page 함수를 만들고 login.html 페이지를 띄워주면 됩니다.
#addresses/views.py
..
def login_page(request):
return render(request, "addresses/login.html")
그냥 브라우저에 html 보여주는 거라 소스가 아~~~주 간단합니다. login_page를 타고 들어오면 render를 이용해 특정 page를 호출하면 끝~! templates 폴더는 settings에서 설정했기 때문에 templates라는 이름은 생략합니다. 이제 runserver를 하고 웹서버에 들어가 봅시다.
딱히 CSS 스타일을 먹이지 않아서... 정말 담백한 로그인 페이지가 우리를 반겨줍니다. 아이디랑 비밀번호를 입력하고 로그인 버튼을 똭~ 누르면 입력한 데이터와 함께 /login/ url을 호출하게 됩니다. login url은 지난 시간에 저희가 views.py에 만들어놓은 로그인 함수인데요. 그때는 id와 pw데이터가 아니라 그냥 이름하고 전화번호로 비교했기 때문에 서버 쪽 코드도 약간의 수정이 필요합니다. 하지만 그전에 우리가 입력한 데이터가 제대로 가는지 로그를 찍어보겠습니다.
#addresses/views.py
..
def login(request):
if request.method == 'POST':
print("request "+ str(request))
print("body "+ str(request.body))
userid = request.POST.get("userid", "")
userpw = request.POST.get("userpw", "")
print("userid = " + userid + " userpw = " + userpw)
# data = JSONParser().parse(request.body)
# search_name = data['name']
# print(search_name)
# obj = Addresses.objects.get(name=search_name)
# # print(obj.phone_number)
#
# if data['phone_number'] == obj.phone_number:
# return HttpResponse(status=200)
# else:
# return HttpResponse(status=400)
return HttpResponse(status=200)
일단 이전 코드들은 주석처리를 하고 가장 먼저 request와 request.body가 어떤 식으로 올라오는지 찍어봅시다. 클라이언트에서 데이터를 올리는 형식에 따라 서버에서 처리해야 하는 게 매번 바뀌니 저는 항상 request를 찍어보는 게 버릇이 됐습니다. 왜냐면 클라이언트를 못 믿기 때문에..
body를 찍어보면 실제로 데이터가 어떤 형태인지 알 수 있습니다. 지난 시간에 테스트했을 때는 클라이언트(insomnia 툴)에서 json 형태로 데이터를 보냈기 때문에 json으로 파싱 했는데, 이번엔 단순한 POST형태로 데이터를 보내기 때문에 request.POST.get()을 이용해 데이터를 꺼내 줍니다. 파이썬에서 POST 데이터 꺼낼 때는 request.POST['key'] or request.POST.get('key', 'default')를 쓰는데, 전자의 경우 값이 안 들어오면 에러가 납니다. 후자는 값이 없을 경우를 대비해 디폴트 값을 세팅할 수 있습니다. get을 쓰세요~
아이디와 비밀번호가 잘 찍혔다면 통신이 제대로 되고 있는 거예요~!
User model 만들기
장고에는 디폴트로 유저 모델이 있습니다. 이전 시간에 sqlite3 파일을 읽었던 것 기억나시나요? 거기에 Tables를 보시면 auth_user라는 테이블이 만들어져 있습니다. 출력을 한번 해보면
위와 같이 나온다. 이걸로 대충 user 모델이 어떻게 잡혀있는지는 알 수 있는데, 정확히 알고 싶으면 레퍼런스를 참고하면 된다.
class AbstractUser(AbstractBaseUser, PermissionsMixin):
"""
An abstract base class implementing a fully featured User model with
admin-compliant permissions.
Username and password are required. Other fields are optional.
"""
username_validator = UnicodeUsernameValidator()
username = models.CharField(
_('username'),
max_length=150,
unique=True,
help_text=_('Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.'),
validators=[username_validator],
error_messages={
'unique': _("A user with that username already exists."),
},
)
first_name = models.CharField(_('first name'), max_length=30, blank=True)
last_name = models.CharField(_('last name'), max_length=150, blank=True)
email = models.EmailField(_('email address'), blank=True)
is_staff = models.BooleanField(
_('staff status'),
default=False,
help_text=_('Designates whether the user can log into this admin site.'),
)
is_active = models.BooleanField(
_('active'),
default=True,
help_text=_(
'Designates whether this user should be treated as active. '
'Unselect this instead of deleting accounts.'
),
)
date_joined = models.DateTimeField(_('date joined'), default=timezone.now)
objects = UserManager()
EMAIL_FIELD = 'email'
USERNAME_FIELD = 'username'
REQUIRED_FIELDS = ['email']
class Meta:
verbose_name = _('user')
verbose_name_plural = _('users')
abstract = True
def clean(self):
super().clean()
self.email = self.__class__.objects.normalize_email(self.email)
def get_full_name(self):
"""
Return the first_name plus the last_name, with a space in between.
"""
full_name = '%s %s' % (self.first_name, self.last_name)
return full_name.strip()
def get_short_name(self):
"""Return the short name for the user."""
return self.first_name
def email_user(self, subject, message, from_email=None, **kwargs):
"""Send an email to this user."""
send_mail(subject, message, from_email, [self.email], **kwargs)
이게 장고에서 디폴트로 제공하는 User모델입니다. 이 유저 모델은 ~/Admin으로 관리자 화면에 들어가면 새로 생성하거나 관리할 수 있어요. urls.py에 다음과 같이 admin을 import 하고 path를 넣어주면 됩니다. 아마 처음에 디폴트로 만들어주긴 할 건데 제가 지워서 다시 넣었습니다~!
..
from django.contrib import admin
urlpatterns = [
..
path('admin/', admin.site.urls),
..
]
어드민 페이지는 열었는데... 가입 창이 없죠..? 처음에 로그인은 어떻게 하지? 생각하실 거예요 ㅎㅎㅎ 가장 먼저 초기 ID이자 관리자 ID인 superuser를 만들어야 합니다. 터미널에서 다음과 같이 입력해서 만들 수 있습니다.
슈퍼유저 만들었으면 로그인을 해봅시다.
로그인하면 Groups와 User가 있습니다. 이것으로 추측해보건대, User모델 말고 Group모델도 디폴트로 만들어주나 봅니다. User를 선택하면 실제 User모델이 뭐가 있는지 조회할 수 있습니다. 즉 장고는 디폴트로 User모델을 제공해주고, Admin페이지를 이용해 관리할 수 있게 되어있습니다. 하지만 기본으로 제공해주는 User모델에는 상~~ 당히 기본정보 칼럼밖에 없습니다. 이메일이나 이름, 스태프 유무 정도예요. 만약 사용하고 싶은 칼럼이 더 있다면 어떻게 할까요?
장고는 디폴트 User모델을 확장할 수 있도록 제공해주고 있습니다.(독려해준다고 해야 되나...) 실제 공식 문서에서도 나와있는데 자세한 사항은 아래 urs을 참조하세요.
https://docs.djangoproject.com/en/2.2/topics/auth/customizing/
저 역시 디폴트 user모델을 사용하는 것이 더 편하다고 생각합니다. 나중에 세션 체크할 때나 인증 체크할 때도 별도의 함수를 만들 필요 없이 제공되는 함수로 체크 가능하거든요~!
한 가지 중요한 점은, User모델을 사용하다가 중간에 바꾸면 마이그레이션 시 에러가 발생할 수 있습니다. 따라서 데이터가 들어가기 전에 User모델을 어떻게 확장할 건지 확정하고 데이터를 밀어 넣는 게 좋아요~!
저희는 지금 ID, PW 칼럼만 필요하기 때문에 따로 커스터마이징 하지는 않겠습니다. 그냥 단순히 User 모델에서 비밀번호 체크하는 로직만 넣어볼게요~!
User Model에서 비밀번호 체크하기
기본 User 모델로 ID, PW를 검증하는 방법은 아주 간단합니다. DB를 보셔서 아시겠지만, 패스워드는 암호화돼서 저장되므로 일반적으로 입력된 패스워드와 비교가 불가합니다. 암호를 풀어야 하지요~ 하지만 패스워드는 일방향 암호화하는 게 대부분이라 복호화는 할 수 없게 되어 있습니다. 즉, DB에 저장된 패스워드를 복호화하여 비교하지는 못하고, 입력된 패스워드를 암호화하여 DB에 저장된 암호화된 패스워드와 비교해야 합니다. 근데 암호화하는 방법도 모르고, 어떤 방식을 썼는지도 모르니 막막하죠? 이런 고민 할 필요 없이 authenticate 함수만 import 하면 만사 ok입니다.
from django.contrib.auth import authenticate
@csrf_exempt
def login(request):
if request.method == 'POST':
print("request "+ str(request))
print("body "+ str(request.body))
userid = request.POST.get("userid", "")
userpw = request.POST.get("userpw", "")
login_result = authenticate(username=userid, password=userpw)
print("userid = " + userid + " result = " + str(login_result))
if login_result:
return HttpResponse(status=200)
else:
return render(request, "addresses/login.html", status=401)
return render(request, "addresses/login.html")
auth에서 authenticate를 불러온 다음 username과 password를 파라미터 값으로 넣으면 됩니다. 만약 패스워드가 일치하면 userid를 리턴하고 일치하지 않으면 None을 리턴합니다. 보통은 성공하면 200 인증 실패면 401 코드를 내려주기 때문에 리턴 status값을 200과 401로 설정했습니다.
이제 로그인 화면에서 로그인을 했을 때 성공하면 200, 실패하면 401이 찍히는 것을 확인할 수 있습니다.
마치며...
이번 시간에는 장고에서 로긴 페이지를 만들어서 로그인 처리를 구현했습니다. 사실 서버에서 200이나 401의 응답을 주면 화면에서 로그인 성공했다는 메시지나 로그인 실패했다는 메시지를 뿌리는 게 정상인데, 이 부분은 프론트 영역이라 생략했습니다. (구글링 하면 나올 거예요 ><)
다음 시간에는 python은 잠시 접고, 안드로이드에서 로그인 화면을 만들어 통신해보도록 하겠습니다. 회원가입도 만들어 보구요 ^^
'Study > python' 카테고리의 다른 글
anaconda 가상환경 설치 및 python 프로젝트 초기 세팅 (0) | 2020.03.01 |
---|---|
[ISSUE] gensim 설치시 Command "python setup.py egg_info" failed with error code 1 문제 (0) | 2020.02.16 |
dJango로 restful API 서버만들기 [2] - rest framework 적용 (16) | 2019.10.17 |
dJango로 restful API 서버만들기 [1] - django 서버 생성 (0) | 2019.10.14 |
python 문자열에서 HTTP 링크(URL) 탐지해서 링크생성하기 <a> 태그 이용 (0) | 2019.06.13 |