[Python自学] day-22 (1) (Session、CSRF、中间件)
一、响应函数补充
三种返回响应的方式:
return HttpResponse() return render() return redirect()
HttpResponse:
除了能够返回字符串,还能返回bytes。
content = "Hello" return HttpResponse(content) return HttpResponse(bytes(content))
render:
返回渲染后的模板。
return render(request, 'mlogin.html', {"pwd_error": pwd_error_msg})
redirect:
跳转到url。
return redirect('/mgmt/host')
三种方式都可以设置cookie:
response = redirect(...) # 或HttpResponse、render response.set_cookie('key', 'value')
set_cookie是将cookie放在响应头中的。
二、Session介绍
由于Cookie存放在客户端浏览器,所以Cookie是不安全的,可以被人查看和修改,甚至伪造。
Cookie不适合存放敏感信息,例如密码等。
所以,我们可以将原本存放在Cookie中的数据,放置到服务器端,例如:
{ 'username':'Alex', 'id':'12973', 'favor':'xxxxxx' }
然后,服务器在保存这些数据之前,生成一个随机字符串。并将这个字符串使用set_cookie放置在客户端。并在服务器也保存一份,作为数据的key,例如:
session = { # 某用户的session 'h9j2j3987d2bksjf98': { 'username': 'Alex', 'id': '12973', 'is_login':True, 'favor': 'xxxxxx' }, # 另一个用户的session 'h978hnjdfi9100':{ 'username': 'Leo', 'id': '12989', 'is_login':True, 'favor': 'yyyyyy' } }
客户端浏览器保存的cookie为:
sessionid = 'h9j2j3987d2bksjf98'
当用户请求时,携带该sessionid。服务器拿到这个随机ID后,会到session中查到对应的数据(例如is_login),如果is_login为True,则说明用户登录过。
优点:同样完成了用户验证,而且数据存放在服务端,安全性更高。
缺点:增加了服务器开销。
三、Session使用
1.使用Session前创建数据库
python manage.py makemigrations
python manage.py migrate
2.查看session表
Django的Session默认是放在数据库的django_session表中的。
3.在视图函数中使用session
def login(request): # 如果是get请求,则检查session中is_login是否为True,如果有则免登录,没有则跳转到登录界面 if request.method == 'GET': if request.session.get('is_login'): return redirect('/index') else: return render(request, 'login.html') # 如果是post请求,则获取请求数据,并验证用户名密码。 if request.method == 'POST': user = request.POST.get('user') pwd = request.POST.get('pwd') # 如果验证通过,则写session,写入username和is_login,并跳转到业务页面index if user == USER_DICT.get('username') and pwd == USER_DICT.get('password'): request.session['username'] = user request.session['is_login'] = True return redirect('/index') else: # 没验证通过,则跳转回login页面 return redirect('/login')
4.django处理session原理
1)当我们使用request.session['is_login']=True设置session时,django会自动生成随机字符串(set_cookie以及自己保留一份存数据库)
2)存放session的键值对到数据库中(现在很多可能使用的是NOSQL数据库,例如redis之类,并且session要提供共享功能,方便网站分布式部署)
3)当浏览器带着sessionid请求时(如下图),视图函数中使用request.session['is_login']获取值时,django会使用sessionid,去数据库中查询对应条目的is_login(如下图)
很多网站不一定使用Django框架,所以在请求头中不一定能看到sessionid,但一般都有一个对应的ID来表示用户:
这是博客园存放的已登录用户的随机字符串,类似于sessionid。后台拿到这个id后,可以从数据库中获取用户信息。
四、session操作
Django为session提供了很多操作。
1.普通键值操作
# 类似字典操作session request.session['k1'] = 123 # 设置k1的值 k1 = request.session['k1'] # 取k1的值,如果k1不存在,则报错 k1 = request.session.get('k1', None) # 取k1的值,不存在则返回None del request.session['k1'] # 删除k1及对应的值
2.键值批量操作
request.session.keys() # 获取所有key列表 request.session.values() # 获取所有value request.session.items() # 获取所有键值对 request.session.iterkeys() # 返回所有key迭代器 request.session.itervalues() # 返回所有value迭代器 request.session.iteritems() # 返回所有item迭代器
3.session删除操作
# 清空数据库中过期的session request.session.clear_expired() # 获取用户对应随机字符串(cookie中的sessionid) sk = request.session.session_key # 判断随机字符串在数据库是否存在(一般用不到,因为在获取session某个值得时候底层也会进行这个操作) request.session.exists(sk) # 删除当前用户的所有Session数据 request.session.delete(sk) # 或 request.session.clear() # 一般在用户注销时使用
4.session过期时间
为session设置过期时间:
request.session.set_expiry(10) #单位是秒
当这样设置以后,所有session的过期时间都是10秒。如果不设置的话,Session的过期时间默认是两周。
设置浏览器关闭时过期:
request.session.set_expiry(0) #设置为0时,浏览器关闭过期
在这种情况下,浏览器的sessionid cookie没有设置过期时间,所以关闭浏览器,Cookie就消失了, 自然无法再利用session来认证。但是在后台数据库中还存在sessionid的记录。
五、session配置
1.Session默认配置:
先查看一下Django对Session的默认配置:
from django.conf import settings print(settings.SESSION_COOKIE_NAME) # 'sessionid' print(settings.SESSION_COOKIE_PATH) #'/' print(settings.SESSION_COOKIE_DOMAIN) # None print(settings.SESSION_COOKIE_SECURE) # False print(settings.SESSION_COOKIE_HTTPONLY) # True print(settings.SESSION_COOKIE_AGE) #1209600 print(settings.SESSION_EXPIRE_AT_BROWSER_CLOSE) # False print(settings.SESSION_SAVE_EVERY_REQUEST) # False
以上打印的值即为默认值。
要修改session的默认配置,只需在settings.py配置文件中修改:
SESSION_COOKIE_NAME = "sessionid" # session产生的随机字符串保存到浏览器cookie时的名字,默认就是sessionid SESSION_COOKIE_PATH = "/" # Session的cookie生效路径 SESSION_COOKIE_DOMAIN = None # Session的cookie生效域名 SESSION_COOKIE_SECURE = False # 是否Https传输cookie SESSION_COOKIE_HTTPONLY = True # 是否只支持http传输 SESSION_COOKIE_AGE = 1209600 # 默认两周失效 SESSION_EXPIRE_AT_BROWSER_CLOSE = False # 是否关闭浏览器使得Session过期 SESSION_SAVE_EVERY_REQUEST = False # 是否每次请求都保存Session,即失效时间根据每次请求向后推
以上配置直接写在setting中即可。
2.Session配置后端存储
数据库后端:(默认)
# settings.py
SESSION_ENGINE = 'django.contrib.sessions.backends.db' # 引擎(默认)
缓存后端:
SESSION_ENGINE = 'django.contrib.sessions.backends.cache' SESSION_CACHE_ALIAS = 'default' CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', 'LOCATION': [ '192.168.1.3:11211', '192.168.1.4:11211', ] }, }
文件后端:
SESSION_ENGINE = 'django.contrib.sessions.backends.file' # 引擎 #SESSION_FILE_PATH = None # 缓存文件路径,如果为None,则使用tempfile模块获取一个临时地址tempfile.gettempdir()SESSION_FILE_PATH = os.path.join(BASE_DIR,'cache') # 在工程目录下创建一个cache目录来存放session
缓存+数据库作为后端:(推荐)
数据库做持久化,缓存提高效率。前提是缓存和数据库都要配置好,参照前面分别的配置。
SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db'
加密cookie session:(不推荐)
将session的数据全部加密后放在cookie中,这种方式不能算一种后端存储技术,实际上都是cookie了。
SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies'
总结:以上几种Session配置只是使用的引擎不同,使用方法都是一样的。前面的默认配置就是通用配置。
Redis作为Session存储(推荐):
第一种配置方法:
首先安装django-redis-sessions:
pip install django-redis-sessions
# settings中配置 SESSION_ENGINE = 'redis_sessions.session' SESSION_REDIS = { 'host': '192.168.1.181', 'port': 6379, 'db': 2, # 哪个数据库 'password': '', 'prefix': 'session', # key的前缀 'socket_timeout': 10 }
可以看到在redis中,session存储为以下形式:
127.0.0.1:6379[2]> keys * 1) "session:fnifus1tqkbilhr1k549mcn5q9k5utdv"
形式为,prefix:session_id。
第二种配置方法:
首先安装django_redis:
pip install django_redis
# 在settings中配置 CACHES = { "default": { "BACKEND": "django_redis.cache.RedisCache", # 把这里缓存你的redis服务器ip和port "LOCATION": "redis://192.168.1.181:6379/3", "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", } } } # 我们定义一个cache(本地缓存来存储信息,cahe指定的是redis SESSION_ENGINE = "django.contrib.sessions.backends.cache" # 指定本地的session使用的本地缓存名称是'default' SESSION_CACHE_ALIAS = "default"
这种配置方式下,session存储为以下形式:
127.0.0.1:6379[3]> keys * 1) ":1:django.contrib.sessions.cachefj33bv40gk7gf2srklfi2fminmohnskb"
六、CSRF
在前面的章节,我们注释掉了CSRF中间件,因为发送post请求的时候出现403错误。
1.CSRF原理
CSRF是用来防止有人在其他非法站点向我们的某个页面发送POST请求。
CSRF启用的时候,我们以GET方式请求一个页面时,Django会生成随机字符串(分别放在render的参数中,以及cookie中,两个字符串是独立的),并传递到页面。
在我们提交表单时,必须携带该随机字符串,才能正常提交,否则报403forbid错误。
由于我们提交数据可以使用form表单,也可以使用Ajax,所以对应两个地方需要获取随机字符串。
在settings.py中启用CSRF中间件:
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', ]
2.Form表单提交时使用CSRF
<body> <form action="/login/" method="post"> {% csrf_token %} <input type="text" name="user"/> <input type="password" name="pwd"/> <input type="submit" value="提交"/> </form> </body>
我们使用了{%csrf_token%}后,在页面元素中可以看到:
Django为我们自动添加了一个隐藏的<input>标签,value就是csrf随机字符串。后台可以自动获取这个值并验证。
3.在Ajax提交Post请求时使用CSRF
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Login</title> </head> <body> <form action="/login/" method="post"> {% csrf_token %} <input type="text" name="user"/> <input type="password" name="pwd"/> <input type="submit" value="提交"/> <input id='ajax_btn' type="button" value="Ajax提交"/> </form> <script src="/static/jquery-1.12.4.js"></script> <script src="/static/jquery.cookie.js"></script> <script> $(function () { $('#ajax_btn').click(function(){ $.ajax({ url:'/login', type:'POST', data:{'user':'leokale','pwd':123}, headers:{'X-CSRFtoken':$.cookie('csrftoken')}, success:function(arg){ location.reload() } }) }) }) </script> </body> </html>
以上这种使用方法在有很多Ajax提交的时候显得不是很方便,因为要为每一个Ajax的请求添加 X-CSRFtoken字段,所以可以使用以下方式:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Login</title> </head> <body> <form action="/login/" method="post"> {% csrf_token %} <input type="text" name="user"/> <input type="password" name="pwd"/> <input type="submit" value="提交"/> <input id='ajax_btn' type="button" value="Ajax提交"/> <input id='ajax_btn2' type="button" value="Ajax提交2"/> </form> <script src="/static/jquery-1.12.4.js"></script> <script src="/static/jquery.cookie.js"></script> <script> $(function () { //在这里的配置,对所有该页面的Ajax都生效 $.ajaxSetup({ beforeSend: function(xhr,settings){ xhr.setRequestHeader('X-CSRFtoken',$.cookie('csrftoken')); } }); //在ajaxSetup中配置后,ajax中就不用再设置X-CSRFtoken了 $('#ajax_btn').click(function(){ $.ajax({ url:'/login', type:'POST', data:{'user':'leokale','pwd':123}, success:function(arg){ location.reload() } }) }); $('#ajax_btn2').click(function(){ $.ajax({ url:'/login', type:'POST', data:{'user':'leokale','pwd':123}, success:function(arg){ location.reload() } }) }); }) </script> </body> </html>
在ajaxSetup中可以进行ajax的全局配置,后面的所有ajax操作都不用单独添加csrf值了。
在ajaxSetup中,参数xhr表示xmlHttpRequest,settings参数表示从后面$.ajax函数参数中获得的字典:
所以我们应该在ajaxSetup中对请求方法进行过滤,GET|HEAD|OPTIONS|TRACE 请求不添加csrf字符串,其余请求类型才添加:
//使用正则判断是否是GET|HEAD|OPTIONS|TRACE function csrfSafeMethod(method){ return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); } //在这里的配置,对所有该页面的Ajax都生效 $.ajaxSetup({ beforeSend: function(xhr,settings){ // 如果不是GET|HEAD|OPTIONS|TRACE,就在请求头中添加x-csrftoken if(!csrfSafeMethod(settings.type)){ xhr.setRequestHeader('X-CSRFtoken',$.cookie('csrftoken')); } } });
4.两个CSRF token
特别注意,在ajax中为请求头添加xsrf随机字符串,这个字符串是从cookie中获得的。当我们第一次以GET请求login页面时,Django除了使用render传递了一个csrftoken,还在cookie中放置了一个csrftoken:
对比通过{%csrf_token%}拿到的字符串:
可以发现这两个字符串是不一样的,所以这两种方式的CSRF token是分别验证的。
七、按需使用CSRF
在settings.py中启用CSRF中间件,相当于所有的views.py视图方法或类中都要验证CSRF随机字符串。但是有时候这样是不合适的。
通过装饰器,按照需求来为视图函数添加CSRF验证:
from django.views.decorators.csrf import csrf_exempt, csrf_protect @csrf_protect def login(request): return HttpResponse("login page") @csrf_protect def index(request): return HttpResponse("login page") def host(request): return HttpResponse("login page")
当settings.py中的csrf配置为禁用时(对全部视图函数禁用csrf验证),上述代码表示login()和index()启用csrf验证。
from django.views.decorators.csrf import csrf_exempt, csrf_protect def login(request): return HttpResponse("login page") def index(request): return HttpResponse("login page") @csrf_exempt def host(request): return HttpResponse("login page")
当settings.py中的csrf配置为启用时(对全部视图函数启用csrf验证),上述代码表示host()禁用csrf验证。
八、Middle Ware中间件
1.Django的中间件是什么
当用户发一个请求时,请求到达Django框架后,这个请求要经过一系列的中间件才能到达视图函数。
我们可以在settings.py中查看经过了哪些中间件:
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', ]
以上列表中,每一条代表一个类,每个类都是一个中间件,我们的请求会从上到下一个一个经过,才能到达视图函数进行处理。
视图函数处理完毕后,使用HttpResponse、render、redirect返回响应时,又要从下到上经过所有的中间件,最终才能返回给浏览器。
2.中间件如何工作
如图,每个中间件都有两个方法,一个处理request,另一个处理response:
我们查看Django提供的中间件源码,可以看到以下代码:
class SessionMiddleware(MiddlewareMixin): def __init__(self, get_response=None): #...... def process_request(self, request): #...... def process_response(self, request, response): # ...... return response
中间件类继承于类MiddlewareMixin,同样我们看以下它的源码:
class MiddlewareMixin: def __init__(self, get_response=None): self.get_response = get_response super().__init__() def __call__(self, request): response = None if hasattr(self, 'process_request'): response = self.process_request(request) response = response or self.get_response(request) if hasattr(self, 'process_response'): response = self.process_response(request, response) return response
我们仿照SessionMiddleware,就可以实现自己的中间件。
3.实现自己的中间件
创建mymiddles文件夹,在其中创建mm1.py:
from django.utils.deprecation import MiddlewareMixin # 定义第一个中间件 class MyMiddle1(MiddlewareMixin): def process_request(self, request): print('This is MyMiddle1, process request, Done') def process_response(self, request, response): print('This is MyMiddle1, process response, Done') return response # 定义第二个中间件 class MyMiddle2(MiddlewareMixin): def process_request(self, request): print('This is MyMiddle2, process request, Done') def process_response(self, request, response): print('This is MyMiddle2, process response, Done') return response # 定义第三个中间件 class MyMiddle3(MiddlewareMixin): def process_request(self, request): print('This is MyMiddle3, process request, Done') def process_response(self, request, response): print('This is MyMiddle3, process response, Done') return response
我们定义了三个中间件。
修改settings.py配置文件,在中间件列表中加入我们自己的中间件:
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', 'mymiddles.mm1.MyMiddle1', 'mymiddles.mm1.MyMiddle2', 'mymiddles.mm1.MyMiddle3', ]
4.测试中间件
我们任意发一个请求,在后台得到以下打印信息:
This is MyMiddle1, process request, Done This is MyMiddle2, process request, Done This is MyMiddle3, process request, Done This is MyMiddle3, process response, Done This is MyMiddle2, process response, Done This is MyMiddle1, process response, Done [23/Dec/2019 21:42:44] "GET /login HTTP/1.1" 200 2115
我们可以看到,request按照流程图中所示,先后经过 MyMiddle1 ---> MyMiddle2 ---> MyMiddle3
response先后经过 MyMiddle3 ---> MyMiddle2 ---> MyMiddle1
5.中间件的作用
既然request和response都要经过中间件(特别是request),我们就可以在中间件中对request进行一些规则过滤、检查等。
当某个中间件验证不通过,则request不能到达下一层中间件,直接返回,例如:
# 定义第二个中间件 class MyMiddle2(MiddlewareMixin): def process_request(self, request): print('This is MyMiddle2, process request, Done') if request.headers.get('Host') == '127.0.0.1:8000': return HttpResponse("走开") def process_response(self, request, response): print('This is MyMiddle2, process response, Done') return response
我们在第二个中间件中添加一个过滤条件,当请求刚好满足这条时,直接返回HttpResponse。后台可以看到以下打印信息:
[23/Dec/2019 21:51:00] "GET /login HTTP/1.1" 200 6 This is MyMiddle1, process request, Done This is MyMiddle2, process request, Done This is MyMiddle2, process response, Done This is MyMiddle1, process response, Done
我们发现第二个中间件并没有将request交给后面的中间件。
总结:通过中间件,我们可以对请求和响应进行自定义的过滤。
6.中间件中的process_view方法
除了前面所描述的process_request和process_response方法,中间件还有process_view方法。如图:
当request进过了每一层中间件的process_request方法后,会经过urls.py路由系统,找到对应的视图函数。
然后折返到第一层中间件执行process_view方法:
from django.utils.deprecation import MiddlewareMixin # 定义第一个中间件 class MyMiddle1(MiddlewareMixin): def process_request(self, request): print('This is MyMiddle1, process request, Done') def process_response(self, request, response): print('This is MyMiddle1, process response, Done') return response # view_func就是对应的视图函数,view_func_args对应视图函数的*args参数,view_func_kwargs对应**kwargs参数 def process_view(self, request, view_func, view_func_args, view_func_kwargs): print('This is MyMiddle1, process view, Done') # 定义第二个中间件 class MyMiddle2(MiddlewareMixin): def process_request(self, request): print('This is MyMiddle2, process request, Done') def process_response(self, request, response): print('This is MyMiddle2, process response, Done') return response # view_func就是对应的视图函数,view_func_args对应视图函数的*args参数,view_func_kwargs对应**kwargs参数 def process_view(self, request, view_func, view_func_args, view_func_kwargs): print('This is MyMiddle2, process view, Done') # 定义第三个中间件 class MyMiddle3(MiddlewareMixin): def process_request(self, request): print('This is MyMiddle3, process request, Done') def process_response(self, request, response): print('This is MyMiddle3, process response, Done') return response # view_func就是对应的视图函数,view_func_args对应视图函数的*args参数,view_func_kwargs对应**kwargs参数 def process_view(self, request, view_func, view_func_args, view_func_kwargs): print('This is MyMiddle3, process view, Done')
测试结果打印如下:
This is MyMiddle1, process request, Done This is MyMiddle2, process request, Done This is MyMiddle3, process request, Done This is MyMiddle1, process view, Done This is MyMiddle2, process view, Done This is MyMiddle3, process view, Done 执行了login() This is MyMiddle3, process response, Done This is MyMiddle2, process response, Done This is MyMiddle1, process response, Done
可以看到,request先经过process_request,然后折返经过process_view,然后执行视图函数,再通过process_response返回数据。
7.中间件中的process_exception方法
中间件中还有一个叫process_exception的方法,这个方法主要用来处理视图函数中的异常。处理流程图:
我们在视图函数中添加一个错误:
def login(request): int('asdf') #......
然后在三个中间件中添加process_exception方法:
def process_exception(self,request,exception): print(exception) # 打印视图函数中出现的异常 return HttpResponse("出现异常")
当视图函数出现异常时,他会将异常交给离他最近的中间件(有process_exception的),例如MyMiddle3。
如果这个中间件对异常进行了处理(如上代码中return HttpResponse('出现异常')),则页面显示“出现异常”。
否则,会继续交给下一个中间件的process_exception方法(例如MyMiddle2),如果所有的中间件都没有处理这个异常,则页面报错。
8.中间件中的process_template_response方法
这个方法主要用来让用户自定义render方法。例如我们修改视图函数:
class Foo(object): def render(self): return HttpResponse('自定义render') # 视图函数login def login(request): return Foo()
我们自己定义了一个类Foo,具有成员方法render(必须这个名字)。然后让视图函数返回Foo的一个对象。
然后在中间件中实现process_template_response方法:
# 定义第一个中间件 class MyMiddle1(MiddlewareMixin): def process_request(self, request): print('This is MyMiddle1, process request, Done') def process_response(self, request, response): print('This is MyMiddle1, process response, Done') return response # view_func就是对应的视图函数,view_func_args对应视图函数的*args参数,view_func_kwargs对应**kwargs参数 def process_view(self, request, view_func, view_func_args, view_func_kwargs): print('This is MyMiddle1, process view, Done') def process_template_response(self,request,response): print('This is MyMiddle1, process template response, Done') return response
这样,process_template_response方法就会执行,我们将Foo对象直接返回,Django会将该对象里render函数的返回值作为数据返回给浏览器。我们在页面上可以看到:
所以,process_template_response方法,主要就是让我们自定义模板渲染方法。但是一般没怎么用。