Django 12 Django进阶-Session 和 Cookie

Django 12 Django进阶-Session 和 Cookie

Cookie 无状态的意思是每次请求都是独立的,它的执行情况和结果与前面的请求和之后的请求都无直接关系,它不会受前面的请求响应情况直接影响,也不会直接影响后面的请求响应情况。状态可以理解为客户端和服务器在某次会话中产生的数据,那无状态的就以为这些数据不会被保留。会话中产生的数据又是我们需要保存的,也

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_exemptcsrf_protect装饰器. csrf_exempt装饰的函数或方法不会验证CSRF(即使全局CSRF中间件启用).csrf_protect装饰的函数则会验证CSRF(即使全局CSRF中间件未启用). csrf_exemptcsrf_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 去掉,依然可以登录成功.
LICENSED UNDER CC BY-NC-SA 4.0
Comment