注册登录流程(前后端未分离)
最近在跟着视频学习django,写个博客记录一下。
视频地址
https://www.bilibili.com/video/BV1uA411b77M?t=178
项目目录结构
bugManagerSys/
├── bugManagerSys
│ ├── __init__.py
│ ├── local_settings.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── db.sqlite3
├── list.txt
├── manage.py
├── templates
├── utils
│ ├── Calibri.ttf
│ ├── encrypt.py
│ ├── errorfile
│ │ └── web_error.py
│ ├── image.py
│ ├── Moderat Medium.otf
│ └── tencent
│ └── sms.py
└── web
├── admin.py
├── apps.py
├── form
│ ├── account.py
│ ├── bootstrap.py
├── __init__.py
├── middleware
│ ├── __init__.py
│ ├── middleware.py
│ └── __pycache__
│ ├── __init__.cpython-36.pyc
│ └── middleware.cpython-36.pyc
├── migrations
│ ├── 0001_initial.py
│ ├── __init__.py
│ └── __pycache__
│ ├── 0001_initial.cpython-36.pyc
│ └── __init__.cpython-36.pyc
├── models.py
├── __pycache__
│ ├── admin.cpython-36.pyc
│ ├── apps.cpython-36.pyc
│ ├── __init__.cpython-36.pyc
│ ├── models.cpython-36.pyc
│ └── urls.cpython-36.pyc
├── static
│ ├── css
│ ├── img
│ ├── js
│ │ └── jquery-3.4.1.min.js
│ └── plugin
│ ├── bootscrap
│ └── font-awesome-4.7.0
├── templates
│ └── web
│ ├── home.html
│ ├── layout
│ │ └── basic.html
│ ├── login.html
│ ├── register.html
│ └── sms_login.html
├── tests.py
├── urls.py
└── views
├── account.py
├── __init__.py
- 访问注册页面
- ajax提交注册页面数据
``·
流程概述
一、web请求url
-
请求方式可以为浏览器输入url,用get的方式请求该接口。
-
请求方式可以为点击页面某个按钮,将该按钮绑定事件,由ajax的方式访问指定的url,提交form表单
注意:绑定的按钮不能是submit属性,否则提交数据后浏览器会自动再发送一个GET请求
<script> //页面加载完成会执行的函数 $(function () { bindClickregisterBtn(); }) /* * 点击注册按钮 绑定事件 * */ function bindClickregisterBtn() { $('#registerSubmitBtn').click(function () { // event.preventDefault(); //取出浏览器的默认行为(submit点击提交数据) console.log("进入bindClickregisterBtn 绑定事件成功") $.ajax({ // "{% url "url别名" %}" django 前端html访问指定name的url的方法 url: "{% url "register" %}", type: "POST", data: $('#register_form').serialize(), // 获取表单全部字段和值 + csrf_token dataType: "JSON", success: function (res) { if (res.status) { window.location.href = res.data; } else { console.log(res); $.each(res.error, function (key, value) { // 构造id 获取元素并给存放错误信息的 span写入内容 $('#id_' + key).next().text(value[0]); }) } } } ) }) }- form 表单自动提交,提交按钮的属性必须为submit,后端响应数据要用render或者redirect。提交表单的url在form标签的action属性中定义。
二 、中间件会根据请求的数据,查找该session id在系统中是否存在合法登录数据,如果存在则继续后续处理,如果不存在,转跳到首页(或者登录页面)
-
定义中间件,将请求对应的session在系统查找相关数据,检查是否登录
from django.utils.deprecation import MiddlewareMixin from web import models # 自定义的中间件需要去setting中注册 class AuthMiddleWare(MiddlewareMixin): # 定义一个鉴权中间件,所有请求都会来判断其session中的user_id是否存在 def process_request(self, request): # 取出user_id 这个动作是服务器给浏览器响应中要求浏览器返回userid吗? # 建立连接后,浏览器会得到服务器给得session id,以后的请求中会携带该id访问 # 如果该id能够在服务器侧找到 user_id,则说明该用户已经登录。 user_id = request.session.get('user_id', 0) user_obj = models.UserInfo.objects.filter(id=user_id).first() # user_id 为空则给默认值0,根据这个id去数据库中是不会找到用户信息的, # 此时request.auth = None,用户为登出状态 request.auth = user_obj print(f"MiddlewareMixin ==============>>>>>{user_obj}") -
将中间件注册到setting中
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', 'web.middleware.middleware.AuthMiddleWare', # 注册自定义的中间件 ] -
此后每个请求过来都会先进行中间件中定义的判断(是否登录)
-
前端根据登录判断,决定首页展示个人信息(已登录),还是注册、登录入口(未登录)
<ul class="nav navbar-nav navbar-right"> <!--如果中间件request.auth存在 则展示用户个人信息,否则展示登录、注册--> {% if request.auth %} <li class="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">{{ request.auth.username }} <span class="caret"></span></a> <ul class="dropdown-menu"> <li><a href="#">个人信息</a></li> <li><a href="#">修改密码</a></li> <li><a href="{% url 'logout' %}">退出登录</a></li> </ul> </li> {% else %} <li><a href="{% url "login" %}">登 录</a></li> <li><a href="{% url "register" %}">注 册</a></li> {% endif %} </ul>
三、urls.py 收到后对url进行解析,并将请求发送给相关view函数
-
django路由分发
# 一级路由---->创建项目时生成的urls.py文件 from django.conf.urls import url, include from django.contrib import admin # incude(),路由分发,namespace 避免分发的路由出现重名的 # include() 只能使用include('xxx.urls','app_namespase',namespace='xxxx')的方式,否则会报错。应该是版本问题 urlpatterns = [ url(r'^admin/', admin.site.urls), # url(r'^web/', include(('web.urls', 'web'), namespace='web_namespace')), url(r'^web/', include('web.urls')), ] # 二级路由-------->创建APP时,手动在APP目录下创建的urls.py from django.conf.urls import url from django.contrib import admin from web.views import account ''' 增加name属性是为了方便反向解析url name属性,在view函数中可以通过reverse('name')的方式反向解析name对应的url; 在html函数中用{% url 'name' %}的方式反向解析name对应的url ''' urlpatterns = [ # url(r'^admin/', admin.site.urls, name="web_admin"), url(r'^register/$', account.register, name="register"), url(r'^send/sms/$', account.send_sms, name="send_sms"), url(r'^login_sms/$', account.sms_login, name="sms_login"), url(r'^login/$', account.login, name="login"), url(r'^logout/$', account.logout, name="logout"), url(r'^image_code/$', account.image_code, name="image_code"), url(r'^home/$', account.home, name="web_home"), ]
view 对请求进行处理
-
请求数据传递,request参数会携带前端请求的相关数据
reuqest.method 请求方法
request.method.GET get请求的参数内容
request.method.POST post请求的参数内容
from django.shortcuts import render, reverse, HttpResponse, redirect # view 中定义的函数必须带request参数 def register(request): # 调用form定义的类,获取类属性作为前端展示的内容 form = account.RegisterModelForm() print(form) if request.method == 'POST': print(f"post content -------------->{request.POST}") form = account.RegisterModelForm(data=request.POST) # 将用户提交的数据,传给RegisterModelForm if form.is_valid(): # 开启字段校验,并判断校验结果是否为True(通过) form.save() # 校验成功(注册成功)后,返回一个页面,让前端可以跳转到登录页面 return JsonResponse({'status': True, reverse('login')) else: print("校验没通过,返回false") return JsonResponse({'status': False, 'error': form.errors}) return render(request, 'web/register.html', {'form': form}) -
如果要操作session,可以考虑在review函数中处理校验
# 账号密码校验,在view函数中判断账号密码是否正确 def login(request): if request.method == "GET": form = account.LoginModelForm() return render(request, 'web/login.html', {'form': form}) # 需要操作session时,建议将账号密码校验放到view,方便处理session form = account.LoginModelForm(request, data=request.POST) if form.is_valid(): print(f'form ------------->{form}') username = form.cleaned_data['username'] password = form.cleaned_data['password'] # import django.db.models import Q .多条件查询 user_obj = models.UserInfo.objects.filter(Q(mail=username) | Q(mobile_phone=username)).filter( Q(password=password)).first() # 操作session,登录成功后将用户id存入session并设置失效时长 if user_obj : request.session['user_id'] = user_obj.id request.session.set_expiry(60 * 60 * 24 * 14) return redirect(reverse('web_home')) form.add_error('password', "账号或者密码错误")) return render(request, 'web/login.html', {'form': form}) # 手机号验证码登录,在form中进行账号密码是否一致的判断 def sms_login(request): form = account.SmsLoginForm() if request.method == "POST": form = account.SmsLoginForm(request.POST) if form.is_valid(): # form 返回的mobile_phone是查询结果 mobile_phone = form.cleaned_data['mobile_phone'] # 登录成功需要将session中存入用户id信息 request.session['user_id'] = form.cleaned_data['mobile_phone'].id request.session.set_expiry(60 * 60 * 24 * 14) # 前端通过ajax提交的数据,所以用JsonResponse返回json数据给前端处理 return JsonResponse({'status': True, 'data': "/web/home/"}) else: return JsonResponse({'status': False, 'error': form.errors}) return render(request, 'web/sms_login.html', {'form': form})
四、通常将字段判断等内容交给form函数处理,处理完将数据返回给view
-
希望将数据库字段返回到前端页面的,在form中可以继承forms.ModelForm
# 注册页面展示的字段内容都是ModelForm弄出来的 ''' view函数通过form=RegisterModelForm()的方式获取展示字段。 view函数通过form=RegisterModelForm(request.POST)的方式将前端的参数传递给form。 form.field.name 字段名 username form.field.label 前端展示的文本框标题 ‘用户名’ form.cleaned_data form校验通过的字段信息 :{'orm.field.name':'clean_name()函数的返回值'} ''' class RegisterModelForm(Bootstrp, forms.ModelForm): username = forms.CharField(label='用户名', min_length=6, max_length=32, error_messages={ 'min_length': web_error.username_min_error, 'max_length': web_error.username_max_error, }) mobile_phone = forms.CharField(label="手机号", validators=[RegexValidator(r'^(1[3456789])\d{9}', message='手机号格式错误')]) password = forms.CharField(label='密码', widget=forms.PasswordInput()) # 表中没有的数据想要在前端展示,在此定义即可 comfire_password = forms.CharField(label='密码确认', widget=forms.PasswordInput()) code = forms.CharField(label='验证码', widget=forms.TextInput(), max_length=6, min_length=6) education = form_model.ModelChoiceField(required=False, label='最高学历', queryset=models.global_downlist_value.objects.filter( type='education')) class Meta: model = models.UserInfo # fields = "__all__" 不能控制展示顺序 # 按照列表的顺序进行校验 fields = ['username', 'password', 'comfire_password', 'mail', 'education', 'mobile_phone', 'code'] # # 重新定义 init函数,目的是给每一个field(input标签)添加一个标签的class,便于前端美化页面 def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # 取出field 添加class属性 for name, field in self.fields.items(): # name ->字段名称;field ->imput标签 field.widget.attrs['class'] = 'form-control' field.widget.attrs['placeholder'] = '请输入{}'.format(field.label) # 自定义方法(局部钩子),密码必须包含字母和数字,函数名固定写法 “clean_接字段名" # 钩子会在其它字段校验都完成后才会进行,所以此时cleaned_data(存放剔除校验未通过的字段、值) def clean_password(self): password = self.cleaned_data['password'] if self.cleaned_data.get('password').isdigit() or self.cleaned_data.get('password').isalpha(): raise ValidationError("密码必须同时包含字母和数字") # password加密返回 return md5(password) def clean_code(self): conn = get_redis_connection() # redis中获取到的数据时二进制的 code = self.cleaned_data.get("code") print(f"code ---------------{code}") # stored_code = conn.get(self.cleaned_data.get("mobile_phone")).decode('utf-8') stored_code = code print(f"stored_code ---------------{stored_code}") # strip() 方法用于移除字符串头尾指定的字符(默认为空格或换行符)或字符序列。 if code.strip() == stored_code: return self.cleaned_data.get("code") else: raise ValidationError("验证码错误") def clean_comfire_password(self): comfire_password = self.cleaned_data.get('comfire_password') secret_comfire_password = md5(comfire_password) if self.cleaned_data.get('password') != secret_comfire_password: # 所以此处使用该方法将报错信息添加到comfire_password中,用于前端展示,展示位置 # ('#id_comfire_password').next() self.add_error('comfire_password', "两次密码不一致") raise ValidationError("两次密码不一致") # 全局钩子抛出的异常是在error.__all__中,不属于任何字段,所以error.0取不到 # ValidationError 执行默认会执行一次add_error,两者差异在于add_error被执行后后面的代码不会中断。 return self.cleaned_data # 自定义方法(全局钩子, 检验两个字段),检验两次密码一致; # 重写clean方法,需要带入self._validate_unique=True,否则会丢掉唯一性判断。唯一性判断会根据表字段是否唯一决定结果。 # def clean(self): # self._validate_unique = True # # 应为密码已经加密,所以comfire_password也需要加密后才能比较 # comfire_password = self.cleaned_data.get('comfire_password') # secret_comfire_password = md5(comfire_password) # if self.cleaned_data.get('password') != secret_comfire_password: # self.add_error('comfire_password', "两次密码不一致") # 所以此处使用该方法将报错信息添加到re_pwd中,用于前端展示, # raise ValidationError("两次密码不一致") # 全局钩子抛出的异常是在error.__all__中,不属于任何字段,所以error.0取不到 # # ValidationError 执行默认会执行一次add_error,两者差异在于add_error被执行后后面的代码不会中断。 # return self.cleaned_data -
与数据库无关内容展示到页面,继承forms.Form
# 手机号登录的form数据处理 # Bootstrp 将初始化返回input标签属性的初始化方法剥离出来 ,SmsLoginForm本身没有定义初始化函数,所以会先执行父类Bootstrp的初始化函数, class SmsLoginForm(Bootstrp, forms.Form): # 定义前端要展示的页面 mobile_phone = forms.CharField(label="手机号", validators=[RegexValidator(r'^(1[3456789])\d{9}', message='手机号格式错误')]) code = forms.CharField(label="验证码", widget=forms.TextInput(), min_length=6, max_length=6, error_messages={ 'min_length': '验证码长度不够6位', 'max_length': '验证码长度超过6位', } ) def clean_mobile_phone(self): print("进入 clean_mobile_phone") # 获取mobile_phone mobile_phone = self.cleaned_data.get('mobile_phone') # 检查手机号是否存在数据库 obj = models.UserInfo.objects.filter(mobile_phone=mobile_phone).first() print(f"obj ------------>{obj}") # print(f"phone obj {obj.mobile_phone}") if not obj: raise ValidationError("手机号未注册") # return obj 即将self.cleaned_data中mobile_phone字段的值由手机号变更为查询结果。这样view就可以根据查询结果去 # 设置session return obj # 测试结果,无论前面的局部钩子是否抛出异常,后面的局部钩子都会被执行 # 测试结果,如果局部钩子校验失败,对象的数据会从self.cleaned_data中被剔除, # 如果校验成功,返回值中,key值一定是字段的name属性值 def clean_code(self): print(f"进入 clean_code") # print(f"-------------self.cleaned_data: {self.cleaned_data}") code = self.cleaned_data.get('code') conn = get_redis_connection() if self.cleaned_data.get('mobile_phone'): # store_code = conn.get(self.cleaned_data.get('mobile_phone').mobile_phone).encoding('utf-8') store_code = code.strip() print(f'store_code -----> {store_code}') if code.strip() != store_code: raise ValidationError("验证码错误") return code
五、view拿到处理结果后,将内容返回给前端
-
返回数据
form = account.RegisterModelForm() ''' view函数通过form=RegisterModelForm()的方式获取展示字段。 view函数通过form=RegisterModelForm(request.POST)的方式将前端的参数传递给form。 form.field.name 字段名 username form.field.label 前端展示的文本框标题 ‘用户名’ form.cleaned_data form校验通过的字段信息 :{'orm.field.name':'clean_name()函数的返回值'} ''' -
前端使用ajax提交数据到后端,view返回HttpResponse给前端
-
前端使用submit按钮提交数据到后端,view返回JsonResponse
技术点
前端
//给按钮绑定事件
$('#id').click.(function fun-name(){
//事件
})
//ajax传递数据给后端
$.ajax({
url : "{% url 'urlname' %}",
type: "POST",
data : "传递数据",
success : function (res){
// res为后端响应的内容
if(res.status){
//响应status= true (后端返回{'status' : True})
}else{
//响应status= false (后端返回{'status' : False})
//错误信息存放res.error; $('#id_' + key) input标签中id=id_key;.next()下一个元素 ; text()写入内容
$.each(res.error, function (key, value) {
$('#id_' + key).next().text(value[0])
}
}
})
/*
* 倒计时函数
* */
function sendSmsRemind() {
// 定位按钮 点击发送验证码
var $btnSms = $('#getCodeBtn');
// 增加按钮属性,不可点击
$btnSms.prop('disabled', true);
var time = 60;
var obj = setInterval(function () {
// 按钮元素上显示内容修改
$btnSms.val(time + "后重新发送");
time = time - 1;
if (time < 1) {
clearInterval(obj)
$btnSms.val("点击发送验证码").prop('disabled', false);
}
}, 1000)
}
后端
form
from django.forms import ModelForm
from django.forms import Form
通过重写__init__方法传递request到lForm
# view.py
form = account.CheckPhoneAndTPL(request, data=request.GET)
# account.py
class CheckPhoneAndTPL(forms.Form):
def __init__(self, request=None, *args, **kwargs):
"""
重新定义init函数,接收view函数中的数据
"""
super().__init__(*args, **kwargs)
# 将视图函数中的request传递过来了
self.request = request
通过重写__init__方法增加返回的input标签属性
# 重新定义个类,让需要增加input属性的FORM类继承
class Bootstrp(object):
'''
function: 重新定义 init函数,目的是给每一个field添加一个标签的class,便于前端美化页面
'''
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# 取出field 添加class属性,fields是一个字典key = field.name value=inpiut标签的属性
for name, field in self.fields.items():
field.widget.attrs['class'] = 'form-control'
field.widget.attrs['placeholder'] = '请输入{}'.format(field.label)
ModelFrom的格式
class RegisterModelForm(Bootstrp, forms.ModelForm):
# 类属性写前端需要展示字段的长度、正则等校验
username = forms.CharField(label='用户名',
min_length=6,
max_length=32,
strip=True #是否移除用户输入空白
error_messages={
'min_length': web_error.username_min_error,
'max_length': web_error.username_max_error,
})
# validators=[RegexValidator(r'^(1[3456789])\d{9}', message='手机号格式错误')] 正则示例
mobile_phone = forms.CharField(label="手机号",
validators=[RegexValidator(r'^(1[3456789])\d{9}', message='手机号格式错误')])
password = forms.CharField(label='密码', widget=forms.PasswordInput())
# 单选Select widget=forms.widgets.Select()
#
education = form_model.ModelChoiceField(required=False, label='最高学历',
queryset=models.global_downlist_value.objects.filter(
type='education'))
# 表中不存在的字段,但是需要在前端页面展示的可以写在这里
comfire_password = forms.CharField(label='密码确认', widget=forms.PasswordInput())
code = forms.CharField(label='验证码', widget=forms.TextInput(), max_length=6, min_length=6)
# 固定写法
class Meta:
model = models.UserInfo
# fields = "__all__" 不能控制展示顺序
fields = ['username', 'password', 'comfire_password', 'mail', 'education', 'mobile_phone', 'code']
# 自定义方法(局部钩子),密码必须包含字母和数字,函数名固定写法 “clean_接字段名"
# 钩子会在其它字段校验都完成后才会进行,cleaned_data(存放已经完成校验的字段、值)
def clean_password(self):
pass
def clean_code(self):
pass
def clean_comfire_password(self):
pass
# 自定义方法(全局钩子, 检验两个字段),检验两次密码一致;
# 重写clean方法,需要带入self._validate_unique=True,否则会丢掉唯一性判断。唯一性判断会根据表字段是否唯一决定结果。
# def clean(self):
# self._validate_unique = True
# # 应为密码已经加密,所以comfire_password也需要加密后才能比较
# comfire_password = self.cleaned_data.get('comfire_password')
# secret_comfire_password = md5(comfire_password)
# if self.cleaned_data.get('password') != secret_comfire_password:
# self.add_error('comfire_password', "两次密码不一致") # 所以此处使用该方法将报错信息添加到re_pwd中,用于前端展示,
# raise ValidationError("两次密码不一致") # 全局钩子抛出的异常是在error.__all__中,不属于任何字段,所以error.0取不到
# # ValidationError 执行默认会执行一次add_error,两者差异在于add_error被执行后后面的代码不会中断。
# return self.cleaned_data
中间件
自定义用于判断request的用户是否登录
from django.utils.deprecation import MiddlewareMixin
from web import models
# 自定义的中间件需要去setting中注册
class AuthMiddleWare(MiddlewareMixin):
# 定义一个鉴权中间件,所有请求都会来判断其session中的user_id是否存在
def process_request(self, request):
# 取出user_id 这个动作是服务器给浏览器响应中要求浏览器返回userid吗?
# 建立连接后,浏览器会得到服务器给得session id,以后的请求中会携带该id访问
# 如果该id能够在服务器侧找到 user_id,则说明该用户已经登录。
user_id = request.session.get('user_id', 0)
user_obj = models.UserInfo.objects.filter(id=user_id).first()
# user_id 为空则给默认值0,根据这个id去数据库中是不会找到用户信息的,
# 此时request.auth = None,用户为登出状态
request.auth = user_obj
print(f"MiddlewareMixin ==============>>>>>{user_obj}")
ORM
- 下拉列表中的值如何返回给前端
# model.py
class global_downlist_value(models.Model):
attr = models.CharField(max_length=32)
type = models.CharField(max_length=16)
# 前端展示的是下拉列表,重定义str方法,将返回值从id改为atrr
def __str__(self):
return self.attr
# form
class RegisterModelForm(Bootstrp, forms.ModelForm):
# 前端页面显示的下拉列表 最高学历;
education = form_model.ModelChoiceField(required=False, label='最高学历',
queryset=models.global_downlist_value.objects.filter(type='education'))
本文介绍了一个基于Django框架的前后端未分离项目的注册登录流程,涵盖了前端页面、路由配置、中间件验证、视图处理及表单验证等方面,详细展示了从前端请求到后端处理的全过程。
1538

被折叠的 条评论
为什么被折叠?



