Python-100-Days/Day41-55/45.Cookie和Session.md

13 KiB
Raw Blame History

Cookie和Session

实现用户跟踪

如今,一个网站如果不通过某种方式记住你是谁以及你之前在网站的活动情况,失去的就是网站的可用性和便利性,继而很有可能导致网站用户的流式,所以记住一个用户(更专业的说法叫用户跟踪对绝大多数Web应用来说都是必需的功能。

在服务器端我们想记住一个用户最简单的办法就是创建一个对象通过这个对象就可以把用户相关的信息都保存起来这个对象就是我们常说的session用户会话对象。那么问题来了HTTP本身是一个无连接每次请求和响应的过程中服务器一旦完成对客户端请求的响应之后就断开连接、无状态客户端再次发起对服务器的请求时服务器无法得知这个客户端之前的任何信息的协议即便服务器通过session对象保留了用户数据还得通过某种方式来确定当前的请求与之前保存过的哪一个session是有关联的。相信很多人都能想到我们可以给每个session对象分配一个全局唯一的标识符来识别session对象我们姑且称之为sessionid每次客户端发起请求时只要携带上这个sessionid就有办法找到与之对应的session对象从而实现在两次请求之间记住该用户的信息也就是我们之前说的用户跟踪。

要让客户端记住并在每次请求时带上sessionid又有以下几种做法

  1. URL重写。所谓URL重写就是在URL中携带sessionid例如http://www.example.com/index.html?sessionid=123456服务器通过获取sessionid参数的值来取到与之对应的session对象。

  2. 隐藏域(隐式表单域)。在提交表单的时候,可以通过在表单中设置隐藏域向服务器发送额外的数据。例如:<input type="hidden" name="sessionid" value="123456">

  3. 本地存储。现在的浏览器都支持多种本地存储方案包括cookie、localStorage、sessionStorage、IndexedDB等。在这些方案中cookie是历史最为悠久也是被诟病得最多的一种方案也是我们接下来首先为大家讲解的一种方案。简单的说cookie是一种以键值对方式保存在浏览器临时文件中的数据每次请求时请求头中会携带本站点的cookie到服务器那么只要将sessionid写入cookie下次请求时服务器只要读取请求头中的cookie就能够获得这个sessionid如下图所示。

    在HTML5时代要除了cookie还可以使用新的本地存储API来保存数据就是刚才提到的localStorage、sessionStorage、IndexedDB等技术如下图所示。

Django框架对session的支持

在创建Django项目时默认的配置文件settings.py文件中已经激活了一个名为SessionMiddleware的中间件(关于中间件的知识我们在下一个章节做详细的讲解,这里只需要知道它的存在即可),因为这个中间件的存在,我们可以直接通过请求对象的session属性来操作会话对象。session属性是一个像字典一样可以读写数据的容器对象,因此我们可以使用“键值对”的方式来保留用户数据。与此同时,SessionMiddleware中间件还封装了对cookie的操作在cookie中保存了sessionid就如同我们之前描述的那样。

在默认情况下Django将session的数据序列化后保存在关系型数据库中在Django 1.6以后的版本中默认的序列化数据的方式是JSON序列化而在此之前一直使用Pickle序列化。JSON序列化和Pickle序列化的差别在于前者将对象序列化为字符串字符形式而后者将对象序列化为字节串二进制形式因为安全方面的原因JSON序列化成为了目前Django框架默认序列化数据的方式这就要求在我们保存在session中的数据必须是能够JSON序列化的否则就会引发异常。还有一点需要说明的是使用关系型数据库保存session中的数据在大多数时候并不是最好的选择因为数据库可能会承受巨大的压力而成为系统性能的瓶颈在后面的章节中我们会告诉大家如何将session的数据保存到缓存服务中。

我们继续完善之前的投票应用,前一个章节中我们实现了用户的登录和注册,下面我们首先完善登录时对验证码的检查。

def get_captcha(request):
    """验证码"""
    captcha_text = random_captcha_text()
    request.session['captcha'] = captcha_text
    image_data = Captcha.instance().generate(captcha_text)
    return HttpResponse(image_data, content_type='image/png')

注意上面代码中的第4行我们将随机生成的验证码字符串保存到session中稍后用户登录时我们要将保存在session中的验证码字符串和用户输入的验证码字符串进行比对如果用户输入了正确的验证码才能够执行后续的登录流程代码如下所示。

def login(request: HttpRequest):
    """登录"""
    hint = ''
    if request.method == 'POST':
        form = LoginForm(request.POST)
        if form.is_valid():
            # 对验证码的正确性进行验证
            captcha_from_user = form.cleaned_data['captcha']
            captcha_from_sess = request.session.get('captcha', '')
            if captcha_from_sess.lower() != captcha_from_user.lower():
                hint = '请输入正确的验证码'
            else:
                username = form.cleaned_data['username']
                password = form.cleaned_data['password']
                user = User.objects.filter(username=username, password=password).first()
                if user:
                    # 登录成功后将用户编号和用户名保存在session中
                    request.session['userid'] = user.no
                    request.session['username'] = user.username
                    return redirect('/')
                else:
                    hint = '用户名或密码错误'
        else:
            hint = '请输入有效的登录信息'
    return render(request, 'login.html', {'hint': hint})

上面的代码中我们设定了登录成功后会在session中保存用户的编号userid)和用户名(username页面会重定向到首页。接下来我们可以稍微对首页的代码进行调整在页面的右上角显示出登录用户的用户名。我们将这段代码单独写成了一个名为header.html的HTML文件首页中可以通过在<body>标签中添加{% include 'header.html' %}来包含这个页面,代码如下所示。

<div class="user">
    {% if request.session.userid %}
    <span>{{ request.session.username }}</span>
    <a href="/logout">注销</a>
    {% else %}
    <a href="/login">登录</a>&nbsp;&nbsp;
    {% endif %}
    <a href="/register">注册</a>
</div>

如果用户没有登录页面会显示登录和注册的超链接而用户登录成功后页面上会显示用户名和注销的链接注销链接对应的视图函数如下所示URL的映射与之前讲过的类似不再赘述。

def logout(request):
    """注销"""
    request.session.flush()
    return redirect('/')

上面的代码通过session对象flush方法来销毁session一方面清除了服务器上session对象保存的用户数据一方面将保存在浏览器cookie中的sessionid删除掉稍后我们会对如何读写cookie的操作加以说明。

我们可以通过项目使用的数据库中名为django_session 的表来找到所有的session该表的结构如下所示

session_key session_data expire_date
c9g2gt5cxo0k2evykgpejhic5ae7bfpl MmI4YzViYjJhOGMyMDJkY2M5Yzg3... 2019-05-25 23:16:13.898522

其中第1列就是浏览器cookie中保存的sessionid第2列是经过BASE64编码后的session中的数据如果使用Python的base64对其进行解码,解码的过程和结果如下所示。

>>> import base64
>>> base64.b64decode('MmI4YzViYjJhOGMyMDJkY2M5Yzg3ZWIyZGViZmUzYmYxNzdlNDdmZjp7ImNhcHRjaGEiOiJzS3d0Iiwibm8iOjEsInVzZXJuYW1lIjoiamFja2ZydWVkIn0=')    
'2b8c5bb2a8c202dcc9c87eb2debfe3bf177e47ff:{"captcha":"sKwt","no":1,"username":"jackfrued"}'

第3列是session的过期时间session过期后浏览器保存的cookie中的sessionid就会失效但是数据库中的这条对应的记录仍然会存在如果想清除过期的数据可以使用下面的命令。

python manage.py clearsessions

Django框架默认的session过期时间为两周1209600秒如果想修改这个时间可以在项目的配置文件中添加如下所示的代码。

# 配置会话的超时时间为1天86400秒
SESSION_COOKIE_AGE = 86400

有很多对安全性要求较高的应用都必须在关闭浏览器窗口时让会话过期不再保留用户的任何信息如果希望在关闭浏览器窗口时就让会话过期cookie中的sessionid失效可以加入如下所示的配置。

# 设置为True在关闭浏览器窗口时session就过期
SESSION_EXPIRE_AT_BROWSER_CLOSE = True

如果不希望将session的数据保存在数据库中可以将其放入缓存中对应的配置如下所示缓存的配置和使用我们在后面讲解。

# 配置将会话对象放到缓存中存储
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
# 配置使用哪一组缓存来保存会话
SESSION_CACHE_ALIAS = 'default'

如果要修改session数据默认的序列化方式可以将默认的JSONSerializer修改为PickleSerializer

SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'

在视图函数中读写cookie

Django封装的HttpRequestHttpResponse对象分别提供了读写cookie的操作。

HttpRequest封装的属性和方法

  1. COOKIES属性 - 该属性包含了HTTP请求携带的所有cookie。
  2. get_signed_cookie方法 - 获取带签名的cookie如果签名验证失败会产生BadSignature异常。

HttpResponse封装的方法

  1. set_cookie方法 - 该方法可以设置一组键值对并将其最终将写入浏览器。
  2. set_signed_cookie方法 - 跟上面的方法作用相似但是会对cookie进行签名来达到防篡改的作用。因为如果篡改了cookie中的数据在不知道密钥的情况下是无法生成有效的签名这样服务器在读取cookie时会发现数据与签名不一致从而产生BadSignature异常。需要说明的是这里所说的密钥就是我们在Django项目配置文件中指定的SECRET_KEY,而盐是程序中设定的一个字符串,你愿意设定为什么都可以,只要是一个有效的字符串。

上面提到的方法如果不清楚它们的具体用法可以自己查阅一下Django的官方文档,没有什么资料比官方文档能够更清楚的告诉你这些方法到底如何使用。

刚才我们说过了,激活SessionMiddleware之后,每个HttpRequest对象都会绑定一个session属性它是一个类似字典的对象除了保存用户数据之外还提供了检测浏览器是否支持cookie的方法包括

  1. set_test_cookie方法 - 设置用于测试的cookie。
  2. test_cookie_worked方法 - 检测测试cookie是否工作。
  3. delete_test_cookie方法 - 删除用于测试的cookie。
  4. set_expiry方法 - 设置会话的过期时间。
  5. get_expire_age/get_expire_date方法 - 获取会话的过期时间。
  6. clear_expired方法 - 清理过期的会话。

下面是在执行登录之前检查浏览器是否支持cookie的代码。

def login(request):
    if request.method == 'POST':
        if request.session.test_cookie_worked():
            request.session.delete_test_cookie()
            # Add your code to perform login process here
        else:
            return HttpResponse("Please enable cookies and try again.")
    request.session.set_test_cookie()
    return render_to_response('login.html')

Cookie的替代品

之前我们说过了cookie的名声一直都不怎么好当然我们在实际开发中是不会在cookie中保存用户的敏感信息如用户的密码、信用卡的账号等而且保存在cookie中的数据一般也会做好编码和签名的工作。即便如此HTML5中还是给出了用于替代cookie的技术方案其中使用得最为广泛的就是localStorage和sessionStorage相信从名字上你就能听出二者的差别存储在localStorage的数据可以长期保留;而存储在sessionStorage的数据会在浏览器关闭时会被清除 。关于这些cookie替代品的用法建议大家查阅MDN来进行了解。