로그인 추가 구현 및 채팅 응용
이번에는 로그인을 구현하여 채팅방에 들어왔을 때 “유저이름님이 들어왔습니다.”와 누가 채팅했는지 구분할 수 있도록 할 예정이다.
먼저 로그인을 구현하기 위해 로그인 페이지와 User모델을 생성해야하므로 다음 명령어를 통해 account app을 하나 만든다.
python manage.py startapp account
이후 socekt_project폴더 내의 setting.py와 urls.py에 다음과 같이 추가한다.
# socket_project/setting.py
INSTALLED_APPS = [
'account',
'chat',
'channels',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
# socket_project/urls.py
urlpatterns = [
path('admin/', admin.site.urls),
path('chat/',include('chat.urls')),
path('account/',include('account.urls')),
# path('',include('chat.urls')),
]
코드 기입이 끝났다면, User모델만들것이다. Django에서는 기본적인 User모델을 지원해주지만, 공부를 위해 Custom User모델을 만들어 진행할 예정이다.
account폴더안에 models.py를 만들어 다음과 같이 코드를 작성해준다.
# account/models.py
from django.db import models
from django.contrib.auth.models import (
BaseUserManager, AbstractBaseUser, PermissionsMixin
)
from django.utils.translation import ugettext_lazy as _
class UserManager(BaseUserManager):
def create_user(self, email, username, password=None):
"""
주어진 이메일, 닉네임, 비밀번호 등 개인정보로 User 인스턴스 생성
"""
if not email:
raise ValueError(_('Users must have an email address'))
user = self.model(
email=self.normalize_email(email),
username=username,
)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, email, username, password):
"""
주어진 이메일, 닉네임, 비밀번호 등 개인정보로 User 인스턴스 생성
단, 최상위 사용자이므로 권한을 부여한다.
"""
user = self.create_user(
email=email,
password=password,
username=username,
)
user.is_superuser = True
user.save(using=self._db)
return user
class User(AbstractBaseUser, PermissionsMixin):
email = models.EmailField(
verbose_name=_('Email address'),
max_length=255,
unique=True,
)
username = models.CharField(
verbose_name=_('Username'),
max_length=30,
unique=True
)
is_active = models.BooleanField(
verbose_name=_('Is active'),
default=True
)
objects = UserManager()
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username', ]
class Meta:
verbose_name = _('user')
verbose_name_plural = _('users')
def __str__(self):
return self.username
def get_full_name(self):
return self.username
def get_short_name(self):
return self.username
@property
def is_staff(self):
"Is the user a member of staff?"
return self.is_superuser
get_full_name.short_description = _('Full name')
우리가 현재 user폼에 필요한 것은 로그인에 사용할 email과 닉네임으로 사용할 username으로 그 외는 제외한 User모델이다.
이때 나중에 다시 확인하겠지만 def str(self)를 통해 scope에 저장된 User객체의 username을 가져올 것이다.
만약 ugettext_lazy가 import되지 않는다면 django 버전이 4.0 이상일 것이다. gettext_lazy로 바꿔주면 정상적으로 동작한다.
User클래스를 작성했다면, Django에서 제공하는 User와 바꾸기위해 다음과 같은 코드를 추가해준다.
# socket_project/settings.py
AUTH_USER_MODEL = 'account.User'
# account/admin.py
from django.contrib import admin
from .models import User
admin.site.register(User)
model을 만들었으니 아래 명령어를 통해 db에 migration을 시켜준다.
python manage.py makemigrations
python manage.py migrate
이번 글에서는 회원가입 페이지를 만들지 않기에 superuser를 생성해 로그인을 진행할 것이다.
커맨드창에 다음과 같이 명령어를 입력해준다.
python manage.py createsuperuser
다음과 같이 입력하면 이메일주소, 유저이름, 비밀번호가 순서대로 나오고 이를 입력해주면 superuser 생성이 끝난다.
이후 accout폴더내에 forms.py를 만들어 UserForm을 생성해준다.
from django import forms
from .models import User, UserManager
from django.contrib.auth.forms import ReadOnlyPasswordHashField
from django.utils.translation import ugettext_lazy as _
class UserForm(UserCreationForm):
email = forms.EmailField(label="이메일")
class Meta:
model = User
fields = ("username", "password1", "password2", "email")
class UserChangeForm(forms.ModelForm):
# 비밀번호 변경 폼
password = ReadOnlyPasswordHashField(
label=_('Password')
)
class Meta:
model = User
fields = ('email', 'username','password','is_active', 'is_superuser')
def clean_password(self):
# Regardless of what the user provides, return the initial value.
# This is done here, rather than on the field, because the
# field does not have access to the initial value
return self.initial["password"]
account 폴더 내에 templates/account에 login.html파일을 만들어 다음과 같은 코드를 입력한다.
#account/templates/account/login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Login</title>
<style>
.login {
width: 500px;
margin: 200px auto auto auto;
}
.title{
margin: 0px 0px 0px 150px;
}
.id{
margin: 20px 0px 0px 160px;
}
.id input{
margin-left: 10px;
}
.pw{
margin: 20px 0px 0px 160px;
}
.button{
margin: 20px 0px 0px 160px;
}
.signup{
display: inline-block;
margin-left: 80%;
margin-top : 0;
}
</style>
</head>
<body>
<div class = "login">
<h1 class = "title">로그인 페이지</h1>
<hr>
<form method="post" action="{% url 'account:login' %}">
{% csrf_token %}
<p class = "id">ID: <input type="text" name="email" id="email"/></p>
<p class = "pw">PW: <input type="password" name="password" id="password"/></p>
<p class = "button"><input type="submit" value="로그인 하기"></p>
</form>
<hr>
<p class = signup> <a href="./signup.html">회원가입하기</a></p>
</div>
</body>
</html>
(아주 간단한 로그인 페이지다...ㅎㅎ)
이후 views.py에서 로그인을 구현하기 위한 코드를 작성한다.
#account/views.py
from django.shortcuts import redirect, render
from django.contrib.auth import authenticate
from django.contrib.auth import login as auth_login
from django.contrib.auth import logout as auth_logout
from django.views.decorators.csrf import csrf_protect
@csrf_protect
def login(request):
if request.method == "POST":
email = request.POST.get('email')
password = request.POST.get('password')
request.session['email'] = email
user = authenticate(username=email, password=password)
if user:
print("login success")
auth_login(request,user=user)
return redirect('chat:index')
return render(request, 'account/login.html')
@csrf_protect
def logout(request):
auth_logout(request)
return redirect('chat:index')
이때 csrf_protect를 처음보는 사람이 있을 것이다. csrf_protect는 Cross-site request forgery를 보호하는 것이며, CSRF는 사용자가 자신의 의지와는 무관하게 공격자가 의도한 행위(수정, 삭제, 등록 등)를 특정 웹사이트에 요청하게 하는 공격을 말한다.
Django에서는 기본적으로 지원한다.
authenticate함수는 request로 받아온 로그인 정보가 존재하는지 확인하고 올바르다면 User객체를 아니라면 None을 반환한다.
이후 auth_login(기본은 login)을 통해 로그인에 성공한 User객체를 세션에 저장한다.
반대로 auth_logout은 저장된 User객체를 세션에서 제외한다.
views.py는 여기까지 작성하면 끝이고, urls.py에 다음과 같이 경로를 추가해준다.
# account/urls.py
from . import views
from django.urls import path
app_name = 'account'
urlpatterns = [
path('login/',views.login,name="login"),
path('logout/',views.logout,name="logout"),
]
app_name변수가 있는 것을 알 수 있는데, 이는 redirect에 chat:index와 같이 이동할 때, app단위로 건너기 위해 존재한다.
따라서 chat폴더의 urls.py도 다음과 같이 app_name을 추가해준다.
from . import views
from django.urls import path
app_name='chat'
urlpatterns = [
path('',views.index,name="index"),
path("<str:room_name>/",views.room,name="room"),
]
마지막으로 consumer.py를 다음과 같이 수정하면 원하는 결과를 얻게 된다.
import json
from channels.generic.websocket import AsyncWebsocketConsumer
# from django.contrib.sessions.backends.db import SessionStore
class ChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.room_name = self.scope['url_route']['kwargs']['room_name']
self.user = self.scope['user']
self.room_group_name = 'chat_%s' % self.room_name
# Join room group
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
await self.accept()
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'chat_message',
'message': f"{self.user}님이 들어왔습니다."
}
)
async def disconnect(self, close_code):
# Leave room group
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'chat_message',
'message': f"{self.user}님이 나갔습니다."
}
)
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)
# Receive message from WebSocket
async def receive(self, text_data):
text_data_json = json.loads(text_data)
message = self.user + " : " + text_data_json['message']
# Send message to room group
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'chat_message',
'message': message
}
)
# Receive message from room group
async def chat_message(self, event):
message = event['message']
# Send message to WebSocket
await self.send(text_data=json.dumps({
'message': message
}))
self.scope['user'] 이 코드가 세션에 담긴 User정보를 가져오는 부분이며, 우리가 User객체에 대해 __str__메서드를 통해 username을 바로 사용할 수 있게 만들었다.
참고
HTML관련 코드를 어디서 참고했는데 노션에 링크를 적어두지 않아 기억이 안납니다..ㅠㅠ
https://wayhome25.github.io/django/2017/05/06/django-model-form/
'Django > Django Channels' 카테고리의 다른 글
Django Channels(Web Socket) - 채팅방스럽게 다듬기 (0) | 2023.01.16 |
---|---|
Django Channels(Web Socket) - 회원가입 기능 추가하기 (0) | 2023.01.16 |
Django Channels(Web Socket) - 동기를 비동기로 (0) | 2023.01.15 |
Django Channels(Web Socket) - Channels 사용해보기 (0) | 2023.01.15 |
Django Channels(Web Socket) - 개발환경 설정 (0) | 2023.01.15 |