Cookie
无状态的意思是每次请求都是独立的,它的执行情况和结果与前面的请求和之后的请求都无直接关系,它不会受前面的请求响应情况直接影响,也不会直接影响后面的请求响应情况。状态可以理解为客户端和服务器在某次会话中产生的数据,那无状态的就以为这些数据不会被保留。会话中产生的数据又是我们需要保存的,也就是说要“保持状态”。因此Cookie就是在这样一个场景下诞生。
cookie的工作原理是:由服务器产生内容,浏览器收到请求后保存在本地;当浏览器再次访问时,浏览器会自动带上Cookie,这样服务器就能通过Cookie的内容来判断当前的连接的状态了.
在Django中操作Cookie,再次用到之前的登录页面.然后再建立一个home页面.在默认的情况下,启动Django服务,login页面和登录页面都可以访问.
模板:
<div class="container">
<h1 class="text-center">请登陆</h1>
<form action="/login/" method="post" class="form-horizontal">
<div class="form-group">
<label for="username">用户名</label>
<input type="text" id="username" name="username">
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" id="password" name="password">
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">提交</button>
</div>
{% csrf_token %}
</form>
</div>
函数:
def login(request):
if request.method == "GET":
return render(request, "login.html")
else:
name = request.POST.get("username").strip()
pwd = request.POST.get("password").strip()
print(name, pwd)
if name == "lee0709" and pwd == "1234":
return redirect("/home/")
else:
return redirect("/login/")
函数被设计的目的是:登录成功之后,访问home页面,登录不成功,则还停留在登录页面.
但实际上可以直接访问home.想要达到目的,就需要保存一个状态用于判断,如果是登录状态,访问home页面可以正常显示,如果不是登录状态,访问home页面的话进行一些其他操作比如导向到登录页面.
Django内使用Cookie
Cookie其实就是一个以字符串形式存储的键值对,由服务端生成,在服务端的响应里可以控制浏览器在本地保存,浏览器拿到以后保存在本地,在发送请求的时候,将Cookie一起发送.
Cookie的用途有很多种,比如:
- 登录,七天免登陆等
- 记录用户的浏览器习惯
- 简单的请求投票限制
一个简单例子
由于cookie是通过响应设置的,所以就暂时不直接返回 Http响应,而是拿到响应对象,然后对响应对象做操作,修改视图函数如下:
def login(request):
if request.method == "GET":
return render(request, "login.html")
else:
name = request.POST.get("username").strip()
pwd = request.POST.get("password").strip()
print(name, pwd)
if name == "lee0709" and pwd == "1234":
# 用户和密码正确的情况下,控制响应返回一个cookie
# 先得到一个响应对象
rep = redirect("/home/")
rep.set_cookie("minkopig","jenny")
return rep
else:
return redirect("/login/")
在登录页面里填入正确的用户名和密码,然后会进到home页面这时候打开chrome的开发工具,可以看到cookie里多了一对键值就是set_cookie()方法的两个参数.
此时跳转到home页面,为了实现检测认证的功能,需要从客户端的请求中检测cookie:
def home(request):
# 从请求的cookie中找有没有键值对
value = request.COOKIES.get("is_login", None)
if value == "jenny":
return render(request, "home.html")
else:
return redirect("/login/")
直接访问home则会跳转到登录页面,在登录了之后,就可以反复直接访问home页面了.
如果关闭浏览器,则cookie就失效了,必须再次登录才可以使用.
设置Cookie
刚才的设置,实际上设置了明文的cookie,容易被伪造.实际都是设置经过加密算法和加盐后的值:
set_signed_cookie(key, value, salt='', **kwargs)
set_signed_cookie
其内部是先用value和盐一起计算出加密后,再调用set_cookie
方法来设置,其中的**kwargs实际上是传递给set_cookie
方法的.
set_cookie(key, value='', max_age=None, expires=None, path='/',
domain=None, secure=False, httponly=False):
其中:
- max_age是过期时间,可以是一个字符串或者datetime对象,也可以设置成数值,单位是秒
- expires=None也是超时时间,给IE浏览器使用.Django里只要设置max_age就可以,会自动设置expires
- path='/' 表示cookie的路径,"/"表示根路径,根路径的cookie可以被任何url的页面访问
- domain=None表示cookie生效的域名
- secure=False,HTTPS的时候才能设置为True,否则无效果.
- httponly=False 只能http协议传输,无法被JavaScript获取
获取Cookie
例子中的获取Cookie是直接通过request.COOKIES
来获得,这是获得不加密的cookie的方法,如果需要获取加密的cookie,就用下边的语句:
request.get_signed_cookie(key, default=RAISE_ERROR, salt='', max_age=None)
其中的key是键名,default这里是获取不到值默认返回的值,默认是出错,可以手工指定一个返回值.salt是加盐的字符串,必须和加密时候的盐一致才能取回正确结果.max_age是后台过期时间.
删除Cookie
由于Cookie的设置是服务端的指令.客户端只负责更新cookie和发送请求的时候将cookie一起发送.所以服务端也可以指定浏览器从cookie中删除指定的键值对.比如可以写一个退出登录的函数:
def logout(request):
rep = redirect("/login/")
rep.delete_cookie("user") # 删除用户浏览器上之前设置的usercookie值
return rep
这个函数首先生成一个重定向到登录页面的响应,然后对这个响应指示浏览器从cookie里删除user键及对应的值.浏览器接到响应就会删除cookie然后重定向到login页面,此时cookie已经删除,再去访问home页面也无法访问了.
装饰器实现登录验证
考虑到想给这些视图函数动态的添加功能,验证cookie的键值对是否存在及值相等,如果结果正确,则继续执行视图函数,如果不正确,就返回到登录页面.这就想到了装饰器,只需要在装饰器内部将验证放到执行原来函数之前就可以了.
使用cookie装饰器简单编写如下:
# cookie验证装饰器
def check_cookie(func):
def wrapper(request, *args, **kwargs):
value = request.COOKIES.get("minkopig", None)
if value == "jenny":
ret = func(request, *args, **kwargs)
return ret
else:
return redirect("/login/")
return wrapper
# 给home函数加上装饰器
@check_cookie
def home(request):
return render(request, "home.html")
Cookie由于保存在本地,安全性差很多,而且只有4069字节的限制,在保存一些用户的复杂信息的时候容量不够,所以一般只用作保存不私密的普通状态信息比如浏览记录等,对于需要验证身份的登录信息在生产环境中主要使用Session.
Session
由于Cookie存储在浏览器上,如果仅靠cookie存放所有用户数据,安全性太差.Session是对cookie的改进,即只用cookie来进行识别用户,用户的所有数据保存在服务端.
先来将刚才的登录函数和页面修改为使用session登录.
def login(request):
if request.method == "GET":
return render(request, "login.html")
else:
name = request.POST.get("username").strip()
pwd = request.POST.get("password").strip()
print(name, pwd)
if name == "lee0709" and pwd == "1234":
# session全部通过request控制
# 给session data内添加一个键值对
request.session["minkopig"] = "jenny"
return redirect("/home/")
else:
return redirect("/login/")
def home(request):
# 获取session键对应的值
if request.method == "GET":
value = request.session.get("minkopig", None)
if value == "jenny":
return render(request, "home.html")
else:
return redirect("/login/")
else:
quit_tag = request.POST.get('quit', None)
if quit_tag:
# 点击退出登录按钮的时候清除cookie和session data
request.session.flush()
return redirect("/login/")
else:
redirect("/home/")
然后在home中增加一个退出按钮
<div class="container">
<h1 class="text-center">这是主页</h1>
</div>
<form action="" method="post">
{% csrf_token %}
<button class="submit btn btn-primary" name="quit" value="1">退出登录</button>
</form>
装饰器版本的验证登录如下:
def check_session(func):
def wrapper(request, *args, **kwargs):
value = request.session.get("minkopig", None)
if value and value == "jenny":
ret = func(request, *args, **kwargs)
return ret
else:
return redirect("/login/")
return wrapper
@check_session
def home(request):
if request.method == "GET":
return render(request, "home.html")
else:
value = request.POST.get("quit", None)
if value:
request.session.flush()
return redirect("/login/")
else:
return redirect("/home/")
session的操作其实与cookie很类似,不同点就是session全部都是针对request请求调用.session属性和方法.而cookie在发给客户端时是对响应结果做操作,取得cookie的时候对request做操作.
看一下Django中session的所有操作:
方法 |
解释 |
获取session数据 |
request.session['k1'] |
用键获取 |
request.session.get('k1',None) |
用get方法获取,推荐此方法 |
设置session数据 |
request.session['k1'] |
直接用键设置值 |
request.session.setdefault('k1',123) |
如果存在这不设置,如果不存在则按照值设置 |
删除数据 |
request.session.delete() |
采用该方法删除,只删除session数据,不删除cookie |
del request.session['k1'] |
仅删除session |
request.session.flush() |
仅删除session |
键和值操作 |
request.session.keys() |
取出所有的key |
request.session.values() |
取出所有的值 |
request.session.items() |
取出所有的键值 |
request.session.iterkeys() |
迭代器版本的键 |
request.session.itervalues() |
迭代器版本的值 |
request.session.iteritems() |
迭代器版本的键值 |
session key操作 |
request.session.session_key |
取session key |
request.session.exists("session_key") |
检查session key是否存在 |
有效时间操作 |
request.session.clear_expired() |
将所有Session失效日期小于当前日期的数据删除 |
request.session.set_expiry(value) |
设置过期时间,如果是整数表示秒数,设置为0表示关闭浏览器就失效.设置为None则会依据全局变量.还接受datetime对象 |
Session的保存与设置
Session的默认保存位置是在数据库中,按照默认的数据库引擎操作.像刚才的例子中,就在数据库中的django_session表内生成了数据.
在Django中Session是通过一个中间件管理的。如果要在应用程序中使用Session,需要在settings.py中的MIDDLEWARE_CLASSES变量中加入’django.contrib.sessions.middleware.SessionMiddleware’。
此外,在settings.py中,还能够具体对session进行设置。
设置内容 |
解释 |
SESSION_ENGINE = 'django.contrib.sessions.backends.db' |
设置数据库引擎,默认就用settings.py里的DATABASES部分 |
SESSION_ENGINE = 'django.contrib.sessions.backends.cache' |
设置引擎为缓存,即保存在内存中,也可以设置为其他的内存缓存数据库如Redis |
SESSION_ENGINE = 'django.contrib.sessions.backends.file' |
保存到文件,同时还需要设置 SESSION_FILE_PATH = None ,如果为none,需要使用 tempfile.gettempdir() 获得一个临时文件目录
|
SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db' |
设置为缓存加数据库,缓存内的数据会定期写入数据库 |
SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies' |
将session数据以加密的cookie形式发送给浏览器,保存在客户端.通过响应修改和取数,不保存在服务端. |
Settings.py内其他session设置 |
SESSION_COOKIE_NAME = "sessionid" |
设置保存在客户端的cookie的键名,不设置就是默认值sessionid. |
SESSION_COOKIE_PATH = "/" |
Session的cookie保存的路径(默认) |
SESSION_COOKIE_DOMAIN = None |
Session的cookie保存的域名(默认) |
SESSION_COOKIE_PATH = "/" |
Session的cookie保存的路径(默认) |
SESSION_COOKIE_SECURE = False |
是否Https传输cookie(默认) |
SESSION_COOKIE_HTTPONLY = True |
是否Session的cookie只支持http传输(默认) |
SESSION_COOKIE_AGE = 1209600 |
Session的cookie失效日期(2周)(默认) |
SESSION_EXPIRE_AT_BROWSER_CLOSE = False |
是否关闭浏览器使得Session过期(默认) |
SESSION_SAVE_EVERY_REQUEST = False |
是否每次请求都保存Session,默认修改之后才保存.推荐改成True |
这里用到了装饰器,就再看一下装饰器在Django视图函数和视图类中的应用.
FBV和CBV的装饰器
FBV的装饰器,与python中装饰一般函数的装饰器没有区别.正常写和使用即可.但是在装饰CBV的时候,有一些变化.首先把FBV的session相关视图函数login和home修改成CBV版本.
class Login(views.View):
def get(self, request):
return render(request, "login.html")
def post(self, request):
name = request.POST.get("username").strip()
pwd = request.POST.get("password").strip()
print(name, pwd)
if name == "lee0709" and pwd == "1234":
request.session["minkopig"] = "jenny"
return redirect("/home/")
else:
return redirect("/login/")
class Home(views.View):
def get(self, request):
return render(request, "home.html")
def post(self, request):
value = request.POST.get("quit", None)
if value:
request.session.flush()
return redirect("/login/")
else:
return redirect("/home/")
这时候Home的get方法是没有验证功能的,想要在get方法上增加验证功能,就不能使用刚才编写的check_session
装饰器,而必须采用其他方法:
方法一: 导入方法装饰器,装饰需要的方法
导入Django模块内的方法装饰器,将原来的FBV装饰器函数做为参数传入.其内部结构很简单,由于绑定的方法的第一个参数永远是self,直接用FBV装饰器取到的参数有误,这个方法装饰器就舍去了第一个参数,将其后的参数对应到原来的FBV装饰器中.
from django.utils.decorators import method_decorator
# 加在Home的get方法上
@method_decorator(check_session)
def get(self, request):
return render(request, "home.html")
这样Home函数的get方法就实现了刚才的功能,即只有登录后带着session才能够访问home页面.
方法二: 用方法装饰器装饰dispatch方法实现类视图内所有方法都带有验证功能
经过分析发现,其实点击退出登录也需要验证session,这样才知道清除哪个记录,如果在没有登录的情况下去点按钮,实际上不应该有操作.也就是post方法也需要验证,这时候就没有必要在get和post方法上都用装饰器,而是重写继承的dispatch方法(是一个默认的方法,会在get和post方法执行前执行,用于分发请求给不同的方法执行.),然后将装饰器加在dispatch方法上即可.
class Home(views.View):
@method_decorator(check_session)
def dispatch(self, request, *args, **kwargs):
return super(Home, self).dispatch(request, *args, **kwargs)
方法三: 方法装饰器带上类的方法名称作为参数,直接装饰类
将方法名称作为name关键字参数传入方法装饰器,get方法和post方法都需要登录校验的话就写两个装饰器,有几个方法就需要些几个装饰器.
@method_decorator(check_session, name="post")
@method_decorator(check_session, name="get")
class Home(views.View):
def dispatch(self, request, *args, **kwargs):
return super(Home, self).dispatch(request, *args, **kwargs)
def get(self, request):
return render(request, "home.html")
def post(self, request):
value = request.POST.get("quit", None)
if value:
request.session.flush()
return redirect("/login/")
else:
return redirect("/home/")
CSRF装饰器
在现在的项目中,CSRF已经都打开了,在模板内也增加了CSRF token,但如果想要为某个方法特别指定是否验证CSRF(比如做了一些无状态的单页面应用),可以引入csrf_exempt
和csrf_protect
装饰器.
csrf_exempt
装饰的函数或方法不会验证CSRF(即使全局CSRF中间件启用).csrf_protect
装饰的函数则会验证CSRF(即使全局CSRF中间件未启用).
csrf_exempt
和csrf_protect
只能够作用在dispatch方法上.
如果让login函数不验证CSRF,则可以按照如下装饰:
from django.views.decorators.csrf import csrf_exempt, csrf_protect
from django.utils.decorators import method_decorator
class Login(views.View):
@method_decorator(csrf_exempt)
def dispatch(self, request, *args, **kwargs):
return super(Login, self).dispatch(request, *args, **kwargs)
def get(self, request):
return render(request, "login.html")
def post(self, request):
name = request.POST.get("username").strip()
pwd = request.POST.get("password").strip()
print(name, pwd)
if name == "lee0709" and pwd == "1234":
request.session["minkopig"] = "jenny"
return redirect("/home/")
else:
return redirect("/login/")
此时在模板中将CSRF token 去掉,依然可以登录成功.