用户登录功能是一个信息系统必不可少的一部分,作为博客网站,同样需要管理员登录管理后台,游客注册后登录评论等
大家好,我是落霞孤鹜
,上一篇我们已经搭建好了前后端的框架的代码,并调通了前后端接口。从这一篇开始,进入到业务功能开发进程中。
首先我们需要实现的功能是用户登录,用户登录功能虽然在系统开发中已经很成熟,但是当我们自己动手做的时候,会发现这个功能是那种典型的说起来容易,做起来复杂的功能,需要考虑和处理的点很多。
一、需求分析
1.1 完整需求
一个完整的用户登录功能,需要考虑的点如下:
- 账号和密码的格式
- 支持邮箱、账号、手机号码登录
- 手机号码支持验证码登录
- 密码错误的次数
- 忘记密码功能
- 注册功能
- 新用户首次登录自动注册功能
- 社交平台账号鉴权登录
- 支持记住账号
- 7天自动登录
- 登录状态保持
- 权限鉴定
- 登出
- 密码修改
在前后端分离的状态下,我们还需要考虑跨域问题等
1.2 博客网站需求
考虑到我们的博客系统是个人博客,用户登录的场景主要集中在游客评论,管理员登录管理后台两个场景,所以登录功能可以适当做删减。
该博客系统的登录功能主要实现以下几个点:
- 账号和密码的格式
- 支持邮箱、账号
- 忘记密码功能
- 注册功能
- 登录状态保持
- 权限鉴定
- 登出
- 密码修改
以上功能点,满足博客网站基本需求
- 未登录的游客只能留言,不能评论
- 游客登录后可以评论博客
- 游客登录后可以修改密码
- 管理员登录后可以管理博客后台
二、后端接口开发
用户登录和鉴权实际上在 Django
里面已经有完整的功能,但是由于我们使用的是前后端分离架构,在 Django
的基础上使用了 Django Rest Framework
,因此原有的 Django
登录和鉴权接口需要做改造和调整,以适应前后端分离功能。
这里需要处理几个点:
- 用户登录,账号密码校验,Session保持
API
鉴权,也即:接口是否是登录后才能使用,还是不登录也可以使用)- 密码修改和重置
2.1 配置鉴权模式
这里采用 Django Rest Framework
提供的基于 Django
的 Session
方案,如果你想采用 JWT
(介绍)方案,可以按照官网教程Authentication - Django REST framework进行配置。在 project/settings.py
中的 REST_FRAMWORK
配置项中修改如下:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication',
],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10
}
2.2 编写登录登出接口
2.2.1 增加 UserLoginSerializer
类
在 common/serializers.py
文件中,增加代码,修改后代码如下:
from rest_framework import serializers
from common.models import User
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'avatar', 'email', 'is_active', 'created_at', 'nickname']
class UserLoginSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'password']
extra_kwargs = {
'password': {
'write_only': True},
}
2.2.2 增加 UserLoginViewSet
类
在 common/views.py
中增加 UserLoginViewSet
类,使用Django
自带的 authenticate
和 login
,完成用户的登录,并返回用户登录信息,在这个过程中,Response
中会创建 Session
,保存登录后的 user
信息,生成Cookies
一并返回。方法修改后代码如下:
from django.contrib.auth import authenticate, login
from rest_framework import viewsets, permissions
from rest_framework.generics import GenericAPIView
from rest_framework.response import Response
from common.models import User
from common.serializers import UserSerializer, UserLoginSerializer
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all().order_by('username')
serializer_class = UserSerializer
permission_classes = [permissions.AllowAny]
class UserLoginViewSet(GenericAPIView):
permission_classes = [permissions.AllowAny]
serializer_class = UserLoginSerializer
queryset = User.objects.all()
def post(self, request, *args, **kwargs):
username = request.data.get('username', '')
password = request.data.get('password', '')
user = authenticate(username=username, password=password)
if user is not None and user.is_active:
login(request, user)
serializer = UserSerializer(user)
return Response(serializer.data, status=200)
else:
ret = {
'detail': 'Username or password is wrong'}
return Response(ret, status=403)
2.2.3 增加 UserLogoutViewSet
类
在 common/views.py
中增加 UserLogoutViewSet
类,使用Django
自带的 auth_logout
,完成用户的登出,并返回登出成功信息,这个过程中,Django
会自动清理 Session
和Cookies
class UserLogoutViewSet(GenericAPIView):
permission_classes = [permissions.IsAuthenticated]
serializer_class = UserLoginSerializer
def get(self, request, *args, **kwargs):
auth_logout(request)
return Response({
'detail': 'logout successful !'})
2.2.4 配置路由
在 common/urls.py
中增加 user/login
和 user/logout
路由,代码如下:
from django.conf.urls import url
from django.urls import include, path
from rest_framework import routers
from common import views
router = routers.DefaultRouter()
router.register('user', views.UserViewSet)
app_name = 'common'
urlpatterns = [
path('', include(router.urls)),
url(r'^user/login', views.UserLoginViewSet.as_view()),
url(r'^user/logout', views.UserLogoutViewSet.as_view()),
]
2.3 编写修改密码接口
2.3.1 增加 UserPasswordSerializer
类
在 common/serializers.py
文件中增加类 UserPasswordSerializer
,主要是因为修改密码时需要提供原密码和新密码,所以单独创建一个 serializer
,代码如下:
class UserPasswordSerializer(serializers.ModelSerializer):
new_password = serializers.SerializerMethodField()
class Meta:
model = User
fields = ['id', 'username', 'password', 'new_password']
@staticmethod
def get_new_password(obj):
return obj.password or ''
2.3.2 增加 PasswordUpdateViewSet
类
密码修改的方式有两种,一种是通过修改密码功能修改,这个时候需要知道自己的原密码,然后修改成自己想要的新密码,一种是通过忘记密码功能修改,这个时候不需要知道自己的密码,但需要知道自己绑定的邮箱,新密码发送到邮箱里面。
- 在
common/views.py
中增加一个方法:get_random_password
,该方法用来生成一个随即的密码,支撑忘记密码功能
def get_random_password():
import random
import string
return ''.join(random.sample(string.ascii_letters + string.digits + string.punctuation, 8))
- 安装发送邮件所需要的依赖
pip install django-smtp-ssl==1.0
- 同时在
requirements.txt
文件中增加依赖
django-smtp-ssl==1.0
- 在
project/settings.py
中增加邮箱配置,这里的EMAIL_HOST
和EMAIL_PORT
是需要依据填写的邮箱做出调整,我这里填写的是网易的163
邮箱
EMAIL_BACKEND = 'django_smtp_ssl.SSLEmailBackend'
MAILER_EMAIL_BACKEND = EMAIL_BACKEND
EMAIL_HOST = 'smtp.163.com'
EMAIL_PORT = 465
EMAIL_HOST_USER = 'zgj0607@163.com'
EMAIL_HOST_PASSWORD = 'xxxx'
EMAIL_SUBJECT_PREFIX = u'[LSS]'
EMAIL_USE_SSL = True
- 在
common/views.py
中增加PasswordUpdateViewSet
,提供请求方式的接口。post
用来完成修改密码功能。
class PasswordUpdateViewSet(GenericAPIView):
permission_classes = [permissions.IsAuthenticated]
serializer_class = UserPasswordSerializer
queryset = User.objects.all()
def post(self, request, *args, **kwargs):
user_id = request.user.id
password = request.data.get('password', '')
new_password = request.data.get('new_password', '')
user = User.objects.get(id=user_id)
if not user.check_password(password):
ret = {
'detail': 'old password is wrong !'}
return Response(ret, status=403)
user.set_password(new_password)
user.save()
return Response({
'detail': 'password changed successful !'
})
- 在
UserLoginViewSet
中增加put
方法,用于完成忘记密码功能,send_mail 使用的是from django.core.mail import send_mail
语句导入。
将忘记密码的功能放在
LoginViewSet
类下的原因是登录接口和忘记密码的接口均是在不需要登录的情况下调用的接口,因此通过请求方式的不同来区分两种接口。
class UserLoginViewSet(GenericAPIView):
permission_classes = [permissi