django(cookie与session、中间件、auth模块)
""" 发展史 1.网站都没有保存用户功能的需求,所有用户访问返回的结果都是一样的 eg:新闻、博客、文章 2.出现了一些需要保存用户信息的网站 eg:淘宝、支付宝、京东 解决每次访问网站都需要重复的输入用户名和密码的问题 当用户第一次登录成功之后,将用户的用户名密码返回给用户浏览器,让浏览器保存在本地,之后访问网站的时候浏览器就自动将保存在浏览器上的用户名和密码发送给服务端,服务端获取之后自动验证(早期的这种方式具有非常大的安全隐患) 优化: 当用户登录成功之后,服务端产生一个随机字符串(在服务端保存数据,用kv键值对的形式) 交由客户端浏览器保存 随机字符串1:用户1相关信息 随机字符串2:用户2相关信息 随机字符串3:用户3相关信息 之后访问服务端的时候,都带着该随机字符串,服务端去数据库中比对是否有对应的随机 字符串从而获取到对应的用户信息 但拿到了截获的随机字符串,还是可以冒充当前用户,还是安全隐患 需要知道的是web领域没有绝对的安全也没有绝对的不安全 """ cookie 服务端保存在客户端浏览器上的信息都可以称之为cookie 它的表现形式一般都是k:v键值对(可以有多个) session 数据是保存在服务端的并且它的表现形式一般也是k:v键值对(可以有多个) 需要基于cookie才能工作 token(补充了解) 解决session在服务器上存储的数据大 服务端不在保存数据 登录成功之后,将一段用户信息进行加密处理(加密算法只有公司开发知道) 将加密之后的结果拼接到信息后面,整体返回给浏览器保存 浏览器下次访问的时候带着该信息,服务端自动切去前面一段信息再次使用自己的加密算法跟浏览器尾部的密文进行比对 总结: 1.cookie就是保存在客户端浏览器上的信息 2.session就是保存在服务端上的信息 3.session是基于cookie工作的(其实大部分的保存用户状态的操作都是需要使用到cookie) 4.在web领域没有绝对的安全 基本上防御措施都需要程序员自己写代码完善,并且只能完善没法杜绝
# cookie:是存在于浏览器的键值对,向服务器端发送请求,携带它过去(不安全) # session:存在于服务端的键值对(内存,文件,mysql,redis) ''' 缺陷:如果用户量很大,存储需要耗费服务器资源 ''' # token现在应用非常广泛,契合了前后端分离 # JWT:json web token
虽然cookie是服务端告诉客户端浏览器需要保存内容
但是客户端可以选择拒绝保存,如果禁止了,那么需要记录用户状态的网站登录功能都无法使用了
(浏览器上的cookie关掉后,淘宝和京东就登录不了)
# 视图函数的返回值 return HttpResponse() return render() return redirect() obj1=HttpResponse() # 操作cookie return obj1 obj2=render() # 操作cookie return obj2 obj3=redirect() # 操作cookie return obj3 # 如果你想要操作cookie,就不得不用obj对象
'''
设置cookie
obj.set_cookie(key,value)
获取cookie
request.COOKIES.get(key)
在设置cookie的时候可以添加一个超时时间
obj.set_cookie('username', 'lq123',max_age=4,expires=3)
# 登录后的页面,在4s后,需要重新登录
max_age
expires
两者都是设置超时时间的,并且都是以秒为单位
需要注意的是,针对IE浏览器需要使用expires
'''
''' 完成一个真正的登录功能 校验用户是否登录的装饰器 用户如果在没有登录的情况下想访问一个需要登录的页面 那么先跳转到登录页面,当用户输入正确的用户名和密码之后 应该跳转到用户之前想要访问的页面去,而不是直接写死 '''
# 登录装饰器 views.py def login_auth(func): def inner(request, *args, **kwargs): print(request.path_info) print(request.get_full_path()) # 能够获取到用户上一次想要访问的url target_url = request.get_full_path() # 获取到cookie值,说明已登录成 if request.COOKIES.get('username'): return func(request, *args, **kwargs) else: # 没有获取到cookie,重定向到登录页面,需登录 return redirect('/login/?next={}'.format(target_url)) # GET属性的属性next return inner def login(request): if request.method == 'POST': username = request.POST.get('username') password = request.POST.get('password') if username == 'lq' and password == '123': # 获取用户上一次想要访问的url target_url = request.GET.get('next') # 这个结果可能是None if target_url: obj = redirect(target_url) else: # 保存用户登录状态 obj = redirect('/home/') # 让浏览器记录cookie数据 obj.set_cookie('username', 'lq123',max_age=4,expires=3) ''' 浏览器不只帮你存 二期后面每次访问你的时候还会带它过来 ''' # 跳转到一个需要用户登录之后才能看到的页面 return obj return render(request, 'login.html') # 注销功能,删除cookie功能 @login_auth def logout(request): obj=redirect('/login/') obj.delete_cookie('username') return obj @login_auth def home(request): # 获取cookie信息,判断你有没有 # if request.COOKIES.get('username') == 'lq123': # return HttpResponse('我是home页面,只有登录的用户才能进') # 没有登录应该跳转到登录页面 return HttpResponse('我是home页面,只有登录的用户才能进')
''' session数据是保存在服务器上的(存数据库,表、文件、缓存、其他),给客户端返回的是一个随机字符串 sessionid:随机字符串 1.默认情况下操作session的时候需要django默认的一张django_session表 数据库迁移命令 django会自己创建很多表 django_session就是其中的一张 django默认session的过期时间是14天 但是可以修改 设置session request.session['key']=value 获取session request.session.get('key') ''' ''' 设置过期时间 request.session.set_expiry() 括号内可以放四种类型的参数 1.整数 多少秒 2.日期对象 到指定日期就失效 3.0 一旦当前浏览器窗口关闭立刻失效 4.不写 失效时间就取决于djano内部全局session默认的失效时间 清除session request.session.delete() # 只删服务端的,客户端的不删 request.session.flush() # 浏览器和服务端都清空(推荐使用) django_session表中的数据条数是取决于浏览器的 同一个计算机上(IP地址)同一个浏览器只会有一条数据生效 (当session过期的时候可能会出现多条数据对应一个浏览器,但是该现象不会持续很久,内部会自动识别过期的数据清除,也可以通过代码清除) 主要是为了节省服务端数据库资源 '''
def ab_session(request): request.session['hobby'] = 'read' ''' 内部发生哪些事 1.django内部会自动帮你生成一个随机字符串 2.django内部自动将随机字符串和对应的数据库存储到django_session表中 2.1先在内存中产生操作数据的缓存 2.2在响应结果django中间件的时候才真正的操作数据库 3.将产生的随机字符串返回给客户端浏览器 ''' return HttpResponse('我是session') def get_session(request): print(request.session.get('hobby')) ''' 内部发生了哪些事 1.自动从浏览器请求中获取sessionid对应的随机字符串 2.拿着该随机字符串去django_session表中查找对应的数据 3.1 如果比对上了,则将对应的数据取出并以字典的形式封装到request.session中 3.2 如果比对不上,则request.session.get()返回的是None ''' return HttpResponse('我是get_session') ''' 基于session实现用户登录 如果多个视图函数都需要使用到一些数据的话,也可以考虑将该数据存储到django_session表中,方便后续的使用 比如:登录验证码 '''
# views.py
from django.views import View from django.utils.decorators import method_decorator ''' CBV中django不建议直接给类的方法加装饰器 无论该装饰器能正常给你,都不建议直接加 ''' @method_decorator(login_auth, name='get') # 方式二(可以添加多个针对不同的方式加不同的装饰器) @method_decorator(login_auth, name='post') class MyLogin(View): @method_decorator(login_auth) # 方式三:直接作用域当前类里面的所有的方法 def dispatch(self, request, *args, **kwargs): # CBV源码中的一个方法 pass # @method_decorator(login_auth) # 方式一:指名道姓 def get(self, request): return HttpResponse('GET请求') def post(self, request): return HttpResponse('POST请求')
5 更多详细参考
https://www.cnblogs.com/liuqingzheng/articles/9509779.html
二 中间件
1 简介
''' 首先django自带七个中间件,每个中间件都有各自对应的功能 并且django还支持程序员自定义中间件 在用django开发项目的时候,只要是涉及到全局相关的功能都可以使用中间件 方便完成 * 全局用户身份校验 * 全局用户权限校验(补充) * 全局访问频率校验 django中间件是django的门户 1.请求来的时候需要先经过中间件才能到达真正的django后端 2.响应走的时候最后也需要经过中间件才能发送出去 ''' # 七个中间件 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', ] ''' 自定义中间件的五个方法 1.必须掌握 process_request process_response 2.了解即可 process_view process_template_response process_exception '''
''' 1.在项目名或应用名下创建一个任意名称的文件夹 2.在该文件夹内创建一个任意名称的py文件 3.在该py文件内需要书写类(这个类必须继承MiddlewareMixin) 然后在这个类里面就可以自定义五个方法了 (这五个方法并不是全部都需要书写,用几个写几个) 4.需要将类的路径以字符串的形式注册到配置文件中才能生效 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', '自己写的中间件的路径1', '自己写的中间件的路径2', '自己写的中间件的路径3', ] ''' ''' 1.必须掌握 process_request(self,request) 1)请求来的时候需要经过每一个中间件里面的process_request方法 结果的顺序是按照配置文件中注册的中间件从上往下的顺序依次执行 2)如果中间件里面没有定义该方法,那么直接跳过执行下一个中间件 3)如果该方法返回了HttpResponse对象,那么请求不再继续往后执行,而是直接原路返回(校验失败不允许访问) process_request方法就是用来做全局相关的所有限制功能 process_response(self,request,response) 1)响应走的时候需要经过每一个中间件里面的process_response方法 该方法有两个额外的参数request,response 2) 该方法必须返回一个HttpResponse对象 (1)默认返回的就是形参response (2)也可以自己返回自己的 3)顺序是按照配置文件中注册了的中间件从下往上依次经过 如果你没有定义的话,直接跳过执行下一个 研究如果在第一个process_request方法就已经返回了HttpResponse对象,那么响应走的时候是经过所有的中间件里面的 porocess_response还是有其他情况 是其他情况 就是会直 接走同级别的process_response返回 flask框架也有一个中间件但是它的规律 只要返回数据了就必须经过所有中间件里面的类似于process_reponse方法 2.了解即可 process_view(self,view_name,*args,**kwargs) 路由匹配成功之后执行视图函数之前,会自动执行中间件里面的该方法 顺序是按照配置文件中注册的中间件从上往下的顺序依次执行 process_template_response(self,request,exception) 返回的HttpResponse对象有render属性的时候才会触发 顺序是按照配置文件中注册了得中间件从下往上依次经过 process_exception(self,request,response) 当前视图函数中出现异常的情况下触发 顺序是按照配置文件中注册了的中间件从下往上依次执行 '''
钓鱼网站(银行转账的例子)
内部本质
在钓鱼网站的页面,针对对方转账,只给用户提供一个没有name属性的普通Input框
然后我们内部隐藏一个已经写好name和value的input框
如何规避上述问题
csrf跨站请求伪造校验
网站在给用户返回一个具有提交数据功能页面的时候会给这个页面加一个唯一标识
当这个页面朝后端发送post请求的时候,我的后端会先校验唯一标识,如果唯一标识不对,直接拒绝(403 forbbiden),如果成功则正常执行
# form表单如何符合校验 <form action="" method="post"> {% csrf_token %} <p>username: <input type="text" name="username"></p> <p>target_user: <input type="text" name="target_user"></p> <p>money: <input type="text" name="money"></p> <input type="submit"> </form> # 如何符合ajax校验 <button id="d1">ajax请求</button>
<script src="/static/js/mysetup.js"></script>
<script> $('#d1').click(function () { $.ajax({ url:'', type:'post', // 第一种,利用标签查找获取页面上的随机字符串 {#data:{'username':'lq','csrfmiddlewaretoken':$('[name=csrfmiddlewaretoken]').val()},#} // 第二种,利用模版语法提供的快捷书写 {#data:{'username':'lq','csrfmiddlewaretoken':'{{ csrf_token }}'},#} // 第三种,通用方式,导入外部js文件,mysetup.js data:{'username':'lq'}, success:function () { } }) }) </script>
mysetup.js
function getCookie(name) { var cookieValue = null; if (document.cookie && document.cookie !== '') { var cookies = document.cookie.split(';'); for (var i = 0; i < cookies.length; i++) { var cookie = jQuery.trim(cookies[i]); // Does this cookie string begin with the name we want? if (cookie.substring(0, name.length + 1) === (name + '=')) { cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); break; } } } return cookieValue; } var csrftoken = getCookie('csrftoken'); function csrfSafeMethod(method) { // these HTTP methods do not require CSRF protection return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); } $.ajaxSetup({ beforeSend: function (xhr, settings) { if (!csrfSafeMethod(settings.type) && !this.crossDomain) { xhr.setRequestHeader("X-CSRFToken", csrftoken); } } });
''' 需求: 网站整体都不校验csrf,就单单几个视图函数需要校验 网站整体都校验csrf,就单单几个视图函数不校验 '''
# views.py from django.views.decorators.csrf import csrf_protect, csrf_exempt from django.utils.decorators import method_decorator ''' csrf_protect 需要校验 csrf_exempt 忽视校验 在CBV中,针对csrf_protect符合我们之前所学的装饰器的三种玩法,针对csrf_exempt只能给dispatch方法加才有效 ''' @csrf_protect def transfer(request): if request.method == 'POST': username = request.POST.get('username') target_user = request.POST.get('target_user') money = request.POST.get('money') print('{}给{}转了{}多少元'.format(username, target_user, money)) return render(request, 'transfer.html')
import importlib res='myfiles.b' ret=importlib.import_module(res) # from myfiles import b # 该方法最小只能到py文件名 print(ret,type(ret)) # <module 'notify1.wechar' from 'E:\\Python学习\\python全栈学习\\day70\\notify1\\wechar.py'> <class 'module'>
需求:发送一条信息,同时让qq、微信、邮箱发送该条信息
包notif1
# __init__.py import setting import importlib def send_all(content): for path_str in setting.NOTIFY_LIST: module_path,class_name=path_str.rsplit('.',maxsplit=1) # 按.从右进行分割,取一个 print(module_path) print(class_name) # 1.利用字符串导入模块 module=importlib.import_module(module_path) print(module,type(module)) # 2.利用反射获取类名 cls=getattr(module,class_name) # Emile,QQ,Wechar print(cls) # 3.生成类的对象 obj=cls() # 4.利用鸭子类型直接调用send方法 obj.send(content)
setting.py
NOTIFY_LIST=[ 'notify1.wechar.Wechar', 'notify1.qq.QQ', 'notify1.emile.Emile' ]
start.py
import notify1 notify1.send_all('快下课了')
from django.utils.deprecation import MiddlewareMixin # 中间件源码 import json class JsonMiddle(MiddlewareMixin): def process_request(self, request): try: request.data = json.loads(request.body) except Exception as e: request.data = request.POST
views.py
def index(request): if request.method == 'POST': # QueryDict 通过看源码,也是字典,是继承的字典,比dict更强大,更改了父类方法,字典的值不能更改 print(request.POST) # <QueryDict: {'username': ['lq'], 'password': ['123']}> print(request.body) # b'username=lq&password=123' print(request.data) # <QueryDict: {'username': ['lq'], 'password': ['123']}> # ajax提交的是json格式数据 if request.is_ajax(): print(request.POST) print(request.body) # b'{"username":"lq","password":"123"}' print(request.data) # {'username': 'lq', 'password': '123'} dict字典 return render(request, 'index.html')
index.html
<body> <form method="post"> <p>用户名:<input type="text" class="form-control" name="username" id="name"></p> <p>密码:<input type="password" class="form-control" name="password" id="password"></p> <input type="submit" class="btn btn-success" value="form提交"> <!--form表单和ajax一起,type属性设置为submit,或者用button按钮,会两次提交,记住:input框的type属性设为button属性--> <input type="button" class="btn btn-info" id="submit" value="ajax提交"> </form> <script> $(function () { $('#submit').click(function () { $.ajax({ url: '', type: 'post', contentType: 'application/json', data: JSON.stringify({'username': $('#name').val(), 'password': $('#password').val()}), success: function (args) { alert(args) } }) }) }) </script>
8 更多详细参考
https://www.cnblogs.com/liuqingzheng/articles/9509739.html
三 auth模块
1 简介
''' 其实我们在创建好一个django项目之后直接执行数据库迁移命令会自动生成很多表 django_session auth_user django在启动之后就可以直接访问admin路由,需要输入用户名和密码,数据参考的就是auth_user表,并且还必须是管理员用户才能进入 创建超级用户(管理员) python3 manage.py createsuperuser 依赖于auth_user表完成用户相关的所有功能 '''
2 如何使用
from django.shortcuts import render, HttpResponse, redirect from django.contrib import auth ''' 用auth模块就用全套,不要单用 ''' # Create your views here. def login(request): if request.method == 'POST': username = request.POST.get('username') password = request.POST.get('password') # 去用户表中校验数据 # 1.表如何获取 # 2.密码如何比对 user_obj = auth.authenticate(request, username=username, password=password) print(user_obj) # 用户对象 lq 数据不符合则返回None # print(user_obj.username) # print(user_obj.password) if user_obj: # 保存用户状态 auth.login(request, user_obj) # 类似于request.session[key]=user_obj # 执行了该方法,你就可以在任何地方通过request.user获取到当前登录的用户对象 return redirect('/home/') ''' 1.自动查找auth_user标签 2.自动给密码加密再比对 该方法注意事项 括号内必须同时传入有用户名和密码 不能只传用户名(一步就帮你筛选出用户对象) ''' return render(request, 'login.html') from django.contrib.auth.decorators import login_required # @login_required(login_url='/login/') # 局部配置:用户没有登录跳转到login_user后面指定的网址 @login_required # 优先级 局部 > 全局 def home(request): # 用户登录之后才能看home print(request.user) # 用户对象 如果没有登录,AnonymousUser匿名用户 # 判断用户是否登录 print(request.user.is_authenticated()) # 自动去django_session里面查找对应的用户对象给你封装到request.user中 return HttpResponse('好吧') ''' 1.如果局部和全局都有吗,该听谁的? 局部 > 全局 2.局部和全局那个好呢? 全局的好处在于无需重复写代码,但是跳转的页面却很单一 局部的好处在于不同的视图函数在用户没有登录的情况下可以跳转到不同的页面 ''' @login_required() def set_password(request): if request.method == 'POST': old_password = request.POST.get('old_password') new_password = request.POST.get('new_password') confirm_password = request.POST.get('confirm_password') # 先校验两次密码对不对 if new_password == confirm_password: # 校验老密码对不对 is_right = request.user.check_password(old_password) # 自己加密比对密码 if is_right: # 修改密码 request.user.set_password(new_password) # 仅仅是在修改对象的属性 request.user.save() # 这一步才是操作数据库 return redirect('/login/') return render(request, 'set_password.html', locals()) @login_required def logout(request): auth.logout(request) # 类似于request.session.flush() return redirect('/login/') from django.contrib.auth.models import User from app01.models import UserInfo def register(request): if request.method == 'POST': username = request.POST.get('username') password = request.POST.get('password') # 操作auth_user表写入数据 # User.objects.create(username=username,password=password) # 写入数据 不能用create,密码加密处理 # 创建普通用户 # User.objects.create_user(username=username,password=password) # UserInfo.objects.create_user(username=username,password=password,phone='1311122121') # 创建超级用户(了解):使用代码创建超级用户,邮箱是必填的,而用命令创建则可以不填 UserInfo.objects.create_superuser(username=username,email='lq88@163.com',password=password) return render(request, 'register.html')
总结:
# 方法总结 1.比对用户名和密码是否正确 # 括号内必须同时传入有用户名和密码 user_obj = auth.authenticate(request, username=username, password=password) # print(user_obj) # 用户对象 lq 数据不符合则返回None # print(user_obj.username) # print(user_obj.password) 密文 2.保存用户状态 auth.login(request, user_obj) # 类似于request.session[key]=user_obj # 执行了该方法,你就可以在任何地方通过request.user获取到当前登录的用户对象 3.判断当前用户是否登录 print(request.user.is_authenticated()) 4.获取当前登录用户 request.user 5.校验用户是否登录装饰器 from django.contrib.auth.decorators import login_required 1)局部配置 @login_required(login_url='/login/') 2)全局配置,在settings.py文件中配置 LOGIN_URL='/login/' ''' 1.如果局部和全局都有吗,该听谁的? 局部 > 全局 2.局部和全局那个好呢? 全局的好处在于无需重复写代码,但是跳转的页面却很单一 局部的好处在于不同的视图函数在用户没有登录的情况下可以跳转到不同的页面 ''' 6.比对原密码 request.user.check_password(old_password) 7.修改密码 request.user.set_password(new_password) # 仅仅是在修改对象的属性 request.user.save() # 这一步才是操作数据库 8.注销 @login_required def logout(request): auth.logout(request) # 类似于request.session.flush() return redirect('/login/') 9.注册 from django.contrib.auth.models import User def register(request): if request.method == 'POST': username = request.POST.get('username') password = request.POST.get('password') # 操作auth_user表写入数据 # User.objects.create(username=username,password=password) # 写入数据 不能用create,密码加密处理 # 创建普通用户 # User.objects.create_user(username=username,password=password) # 创建超级用户(了解):使用代码创建超级用户,邮箱是必填的,而用命令创建则可以不填 User.objects.create_superuser(username=username,email='lq88@163.com',password=password) return render(request, 'register.html')
from django.db import models from django.contrib.auth.models import User, AbstractUser # Create your models here. # 第一种:一对一 不推荐 # class UserDetail(models.Model): # phone=models.BigIntegerField() # user=models.OneToOneField(to='User') # 第二种:面向对象的继承 models.py
class UserInfo(AbstractUser): ''' 如果继承了AbstractUser 那么在执行数据库迁移命令的时候吗auth_user表就不会再创建出来了 而UserInfo表中会出现auth_user所有的字段外加自己扩展的字段 这么做的好处在于你能够直接点击你自己的表更加快速的完成操作及扩展 前提: 1.在继承之前没有执行过数据库迁移命令 auth_user没有被创建,如果当前库已经创建二楼那么就重新换一个库 2.继承的类里面不要覆盖AbstractUser里面的字段名 表里面有的字段都不要动,只扩展额外字段即可 3.需要在配置文件中告诉django要用UserInfo替代auth_user AUTH_USER_MODEL='app01.UserInfo' '应用名.表名' ''' phone=models.BigIntegerField() ''' 如果自己写表代替二楼auth_user那么 auth模块的功能还是照常使用,参考的表页由原来的auth_user变成了UserInfo
# 需要到admin.py文件中设置
from . import models
# Register your models here.
admin.site.register(models.UserInfo)
bbs作业用户表就用上述方式 '''