Skip to content

Cookie鉴权

提示

HTTP 请求都是无状态的,但是我们的 Web 应用通常都需要知道发起请求的人是谁。为了解决这个问题,HTTP 协议设计了一个特殊的请求头:Cookie。服务端可以通过响应头(set-cookie)将少量数据响应给客户端,浏览器会遵循协议将数据保存,并在下次请求同一个服务的时候带上(浏览器也会遵循协议,只在访问符合 Cookie 指定规则的网站时带上对应的 Cookie 来保证安全性)。

一. 原生nodejs使用cookie

1 一些原生nodejs设置cookie的API

1.1 通过服务端设置cookie

js
response.setHeader('Set-Cookie', [`name=MrLi; httpOnly=true; max-age=10`, 'age=12']);

注意

  • 需要将要设置的cookie放入数组中,再统一执行Set-Cookie,若多次执行这句命令,后面的回覆盖前面的。
  • Cookie里面的每段内容是通过分号+空格来进行分割的例如: image.png
  • 若是我们要对某一个cookie键值对设置他参数,在数组内的对应cookie键值对中通过分号+空格进行分割,比如设置只读,设置可访问的域等等参数

1.2 服务端读取请求头里面的cookie

js
let cookies = request.headers.cookie
// 会得到请求投中cookie字段的值,是一个string,如上图内的cookie值类似。

1.3 利用nodejs提供的queryString模块,解析cookie值为对象

js
const querystring = require('querystring');
let cookiesObj = querystring.parse(req.headers.cookie, '; ', '=');
// {name:"MrLi",age:"12"}

2 服务端设置cookie时候的其他常用参数

首先,来看一下cookie里面有哪些可以设置的参数,我们来看一下百度的cookie:

image.png

  • name:cookie的名字
  • value:name对应的值
  • domain:域名设置(默认就是当前域名)这里的域名指的是父域和子域,不能跨域设置,限制哪些域下可以访问这个cookie,若设置为父级域名,则父、子域名都可以访问,若设置的是子级域名,则只有子域名及子域名的子域名才能访问,.baidu.comwww.baidu.com的父级域名
  • path:cookie在哪些路径下可以写入(很少用,默认为/),同express中间件原理一致,/表示是由以/开头的路径都可以写入
  • exipres或者max-age:类似于http缓存的过期时间,过期会自动删除cookie,默认是浏览器一关就过期,就是图片里面写的Session
    • exipres是绝对世间,截止到末年某月某日过期,例如 10秒后过期的绝对时间exipres=new Date(Date.now() + 10*1000).toGMTString()
    • max-age是相对世间,多少秒后过期,(单位秒)[常用]
  • httpOnly:是否客户端可以操纵cookie,若设置为true,则在客户端无法通过代码操纵cookie,但是可以在浏览器控制台手动修改它。

其他参数不常用,不一一列举了

3 原生Nodejs代码示例

js
const http = require('http');
const querystring = require('querystring');
http.createServer((req, res) => {
    // 通过服务端去写入cookie
    if (req.url == '/read') {
        // 读取cookie
        res.end(JSON.stringify(querystring.parse(req.headers.cookie, '; ', '=')));
    } else if (req.url == '/write') {
        // 设置cookie
        // 这里设置了两个cookie,name和 age,其中name为客户端只读,有效期10秒
        res.setHeader('Set-Cookie', [`name=MrLi; httpOnly=true; max-age=10`, 'age=18']);
        res.end('cookie write success')
    } else {
        res.end('Not Found')
    }
}).listen(3000);

效果

image.png

cookie只读效果(name是只读的,无法通过客户端代码访问到,也无法通过代码修改) image.png

二. 提高cookie安全性

由于cookie在客户端是可以随意修改的, 即使设置了httpOnly,也可以手动修改,这很不安全,有一种解决方法,可以提高cookie被更改的风险

当给浏览器设置cookie时,可以增加签名,根据数据内容创建一个唯一的签名,利用的是MD5,MD5的也叫加盐算法,是根据内容和秘钥算出一个签名(且不能反解),若是相同的秘钥和内容签名的结果是相同的。每次服务端拿到cookie的时候,就对比这两个签名是否一直,若不一致就说明已经被更改了。

这也是Express和koa提高cookie安全性的方法

js
const crypto = require('crypto');
const secret = 'fdkjgmvmlsdjvmv;cmx'; // 秘钥
const sign = (value)=>{
    let str = crypto.createHmac('sha256',secret).update(value).digest('base64');
    return str.replace(/\/|\=|\+/,'') // 需要将base64里面的/ = +这些符号替换掉,否则传输时候不识别
}

具体用法见下文

base64URL详解:image.png

通过上述的增加签名后,cookie会稍微变得安全一些,但是cookie始终是明文的,不能在里面放一些敏感信息,比如用户名密码等

三. 封装getCookie 和setCookie方法,很方便的操作cookie

js
const http = require('http');
const querystring = require('querystring');
const crypto = require('crypto');
const secret = 'erwefdcvdbvcxddfdfdf'; // 秘钥

const sign = (value)=>{
    let str = crypto.createHmac('sha256',secret).update(value).digest('base64');
    return str.replace(/\/|\=|\+/,'')
}

http.createServer((req, res) => {
    req.getCookie = function (key,options = {}) {
        let cookieObj = querystring.parse(req.headers.cookie, '; ', '=');
        if(options.signed){
            let [value,s] = (cookieObj[key]||'').split('.');
            let newSign = sign(value);
            if(newSign === s){
                return value; // 签名一致 说明这次的内容是没有被改过的
            }else{
                return undefined; // 签名被篡改了 不能使用了
            }
        }
        return cookieObj[key];
    }
    let arr = [];
    res.setCookie = function (key,value,options = {}) {
        let opts = [];
        if(options.domain){
            opts.push(`domain=${options.domain}`);
        }
        if(options.path){
            opts.push(`path=${options.path}`)
        }
        if(options.maxAge){
            opts.push(`max-age=${options.maxAge}`)
        }
        if(options.httpOnly){
            opts.push(`httpOnly=${options.httpOnly}`)
        }
        if(options.signed){
            value = value + '.' + sign(value); // 若设置signed为true时候,就给cookie的值加上签名,用点相连
        }
        arr.push(`${key}=${value}; ${opts.join('; ')}`);
        res.setHeader('Set-Cookie',arr); // 最终的设置Cookie
    }
    // 通过服务端去写入cookie
    if (req.url == '/read') {
        // 读取cookie
        res.end(req.getCookie('name',{signed:true}) || 'empty');
    } else if (req.url == '/write') {
        // 设置cookie

        // 当给浏览器设置cookie时 可以增加签名 根据数据内容创建一个唯一的签名  MD5
        // 加盐算法 根据内容 和秘钥算出一个签名 (不能反解)  相同的秘钥签名的结果是相同
        res.setCookie('name','MrLi',{ // 1.可以对设置一个字段 MrLi.sign
            httpOnly:true,
            maxAge:200,
            signed:true
        });
        res.setCookie('age','100');
        res.end('write Ok')
    } else {
        res.end('Not Found')
    }
}).listen(3000);