关于Cookie

我们知道一个页面关闭之后,关于这个页面的前端代码在内存里的东西就都没有了。如果想要进行前端数据的持久化,目前主要有Cookie、LocalStorage和SessionStorage这几种方式。这些方式本质上都是将数据通过浏览器存储在了磁盘上(所以随着浏览器的发展,以后可能还会出现更多的数据持久化的方式呢)。那为什么普遍都选用Cookie来实现页面自动登录的功能呢?

简单地说,因为Cookie有这样一个机制:它会自动出现在前端与后端发送的每一个请求中(以header的形式),即使你没有设置任何Cookie,请求中也会包含一个空的Cookie header。因此只要将用户的认证信息放在Cookie当中,就表示之后的每个请求自带了身份验证的功能,服务器端可以通过检查每一条请求的Cookie信息来确认该请求是否“合法”。这里要注意的是,Cookie是和域名绑定的,不同域名的Cookie是相互隔离的,即设置了http://www.google.com的Cookie后,只有该页面下的请求会自动夹带这个设定的Cookie。

另外,Cookie在设置时一般会添加一个过期的时间,即当一个Cookie过期了,浏览器会自动删除该Cookie的内容,然后后端发现过来的请求的Cookie为空,就redirect到登陆页面了。用户重新登陆后,后端会同时更新Cookie的信息。设置过期时间的目的主要还是为了安全,因为Cookie里面一般存储的是根据用户的用户名和密码生成的一段秘钥,用户每次登陆就会去重新生成一段秘钥,这就好比你经常更换密码一样,坏人想破解你的秘钥,可能破解到一半秘钥就变了,你说坏人气不气。(Cookie过期的时候服务器端一般不需要做任何操作?这里如果服务器不去管理Cookie过期的话可能会有安全的隐患,即一个用户很长时间不登陆,虽然Cookie过期了,但后端服务器还是认之前这个过期的Cookie信息的,也就给人足够的时间来破解Cookie的信息并伪装成该用户登陆了)

以上,也就解释了为什么有些网站很久才需要重新登陆一次,而有些网站没过多久就又要输用户名密码了。

这里有额外的一个问题,上面这个场景下,Cookie能不能被LocalStorage替代?

答案是肯定的,因为它们都能将数据持久化嘛,只不过我们需要在前端代码中手动添加一个header来存放LocalStoage里的信息,且每次发送请求都夹带这个header,并且每次还要检查是否过期,过期了则删除该header里的内容。

查看/设置Cookie

随便打开一个需要登录的网页,在浏览器的dev console里面输入如下命令就能看的当前页面的Cookie啦(注意,如果Cookie设置了HttpOnly属性,JS代码是没权限查看到Cookie的,也就是说document.cookie会永远都是空):

document.cookie

因此前端查看Cookie信息就是查看document.cookie变量,设置Cookie也是设置这个变量。

实现自动登录

如果路由是由前端决定的,那么只需要在每次向后端发送请求的时候以及切换子页面时检查下document.cookie变量是否包含所需的内容即可,如果不包含则跳转到登陆页面。

这里以react-router为例,并且使用react-router-redux来将路由信息也放入Redux的状态机中,每次登陆子页面时来检查Cookie是否存在:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import {Router, Route, browserHistory, IndexRoute} from 'react-router'
import About from './components/About'
import Login from './components/Login'
import Home from './components/Home'
import './index.css';
import {Provider} from 'react-redux'
import {syncHistoryWithStore} from 'react-router-redux'
import store from './store'


function requireAuth(nextState, replaceState) {
    // Assume the cookie only saves auth info
    if (!document.cookie)
        replaceState('/login');
}

// Create an enhanced history that syncs navigation events with the store
const history = syncHistoryWithStore(browserHistory, store, {
    selectLocationState: state => ({locationBeforeTransitions: state.get('routing').get('locationBeforeTransitions')})
});

ReactDOM.render(
    (<Provider store={store}>
        <Router history={history}>
            <Route path="/login" component={Login}>
            </Route>
            <Route path="/" component={App}>
                <IndexRoute component={Home} onEnter={requireAuth}/>
                <Route path="about" component={About} onEnter={requireAuth}/>
            </Route>
        </Router>
    </Provider>), document.getElementById('root')
);

当然,上面的实现有一个前提,那就是Cookie没有设置为HttpOnly。如果Cookie设置了HttpOnly该怎么实现呢?一个思路是利用LocalStorage存储一个变量表示Cookie过期的时间节点,这个时间节点在登陆成功时触发更新(更新到多久以后要和后端一致,后端也可以提供一个api来同步过期时间),每次需要向后端发送请求时检查这个时间是否已过期,如果过期则转换到登陆页面,这样就避免了JS代码直接操作Cookie产生的安全隐患。

等等,真的需要这么复杂吗?还有一种简单有效的方法是,将含有敏感信息的Cookie设为HttpOnly的同时,添加一个无关痛痒的Cookie,它不设为HttpOnly且过期时间和上面的Cookie一致,毕竟前端代码只是想知道Cookie过期了没,并不关心Cookie的内容嘛。

查看/设置Cookie

Flask已经封装好了Cookie相关的方法,直接调用即可:

from flask import Flask, request, Response
import time

app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'hello world'

@app.route('/login')
def login():
    res = Response('set cookies')
    res.set_cookie(key='name', value='foo', expires=time.time() + 60)
    return res

@app.route('/show')
def show():
    return request.cookies.__str__()

实现自动登录

如果路由是由后端决定的,那么自动登录功能就很简单啦。所谓自动登录,其实就等同于Cookie没过期时不需要重新登陆。所以,只需要在每个路由下面判断下Cookie是否过期,如果过期了则跳转到登陆页面即可。

同样用上面例子,这次如果Cookie过期了(或没设置)则自动跳转到/login页面:

from functools import wraps
from flask import Flask, request, Response, redirect
import time

app = Flask(__name__)

def check_cookie(func):
    @wraps(func)
    def wrapper():
        if not request.cookies:
            return redirect('/login')
        else:
            func()
    return wrapper

@app.route('/')
@check_cookie
def hello_world():
    return 'hello world'

@app.route('/login')
def login():
    res = Response('add cookies')
    res.set_cookie(key='name', value='foo', expires=time.time() + 60)
    return res

@app.route('/show')
@check_cookie
def show():
    return request.cookies.__str__()

Reference

  1. 详说 Cookie, LocalStorage 与 SessionStorage
  2. Set cookie and get cookie with JavaScript
  3. 浅入浅出Flask框架:Cookie