0%

Koa.js中的Cookies防篡改机制

  1. 概述
  2. Keygrip使用方法
  3. Koa.js中Cookies防篡改的方法

crypto-utils/keygrip

Cookie防篡改机制 | 掘金

基于Keygrip的签名和未签名cookies

概述

作为为数不多的写在Koa实例中的功能,Cookies防篡改机制通过接口app.keys暴露给开发者。该接口接收两类参数:

  • 密钥:['secret1', 'secret2', 'secret3']'secret',字符串或数组;
  • Keygrip实例:app.keys = new Keygrip(["SEKRIT2", "SEKRIT1"], 'sha256', 'hex')

在防篡改机制的保护下,存放在Cookies中的数据如果被人为修改,就会被检测出来,不再生效。

Cookies防篡改机制为:

  • 服务器提供一个签名生成算法sign
  • 对数据进行签名sign(key)得到密文保存为key.sig
  • keykey.sig同时存在Cookie中,客户端每一次请求都带着keykey.sig
  • 服务器收到请求后,校验keykey.sig,检验内容是否被篡改

Keygrip使用方法

安装:

1
npm i keygrip -S

初始化Keygrip对象:

1
const keys = new Keygrip(keylist, [hmacAlgorithm], [encoding])
  • keylist:字符串数组,必填,用于签名的密钥列表;
  • hmacAlgorithm:用于签名的算法,默认为sha1,该参数用于指定node.js自带的crypto模块的createHmac()方法的第一个参数,取决于平台上OpenSSL版本支持的可用算法。例如sha1, sha256, sha512, md5, …;
  • encoding:编码方式,默认为base64,可选:utf8, base64, hex, ascii, binary, …。

使用Keygrip计算hash值、验证hash、匹配列表中的密钥。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const Keygrip = require('keygrip');

// 获取Keygrip实例
const keyList = ['secret1', 'secret2', 'secret3'];
const keys = new Keygrip(keyList, 'sha1', 'base64');

// 计算hash值
const str = '123456';
const hash = keys.sign(str);
console.log(hash); // eIpfKfH_9jX0QEbzfuKOKMOr7qw

// 验证
const matched1 = keys.verify('123456', hash);
console.log(matched1); // true
const matched2 = keys.verify('123455', hash);
console.log(matched2); // false

// 匹配密钥顺序
const idx1 = keys.index('123456', hash);
console.log(idx1); // 0
const idx2 = keys.index('123455', hash);
console.log(idx2); // -1

// 在数组开始插入新的密钥
keyList.unshift('secret0');
console.log(keyList); // [ 'secret0', 'secret1', 'secret2', 'secret3' ]

const matched3 = keys.verify('123456', hash);
console.log(matched3); // true
const idx3 = keys.index('123456', hash);
console.log(idx3); // 1(当index的值大于0时,虽然可以正确匹配,但表明应该更新签名,使用最新的、也就是序号为0的那个密钥)

Keygrip的核心是三个函数:

  • sign(data):使用密钥列表中的第一个密钥对数据进行签名;
  • verify(data, hash):验证数据是否被篡改,计算data的hash值,与hash进行对比;
  • index(data, hash):验证hash是否是由data经列表中的密钥签名获得,如果不是,返回-1,如果是,返回密钥在列表中的序号。

Koa.js中Cookies防篡改的方法

Koa.js中使用pillarjs/cookies来管理Cookies,可以通过ctx.cookies访问到这个对象的实例,其中最重要的两个方法是get()set()

  • ctx.cookies.set(key, value, opts)

  • ctx.cookies.get(key, opts)

opts的完整参数可以查看仓库API或源码,这里我们只需要用到其最常用的signed,一个使用签名防止cookie篡改的例子如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Cookies 防篡改
*/
const Keygrip = require('keygrip');
app.keys = new Keygrip(['secret'], 'sha1', 'base64');
const data = {
account: 'test',
password: '123456'
}
app.use(async(ctx, next) => {
ctx.cookies.set('_user', JSON.stringify(data), { signed: true, maxAge: 86400000 });
ctx.body = 'ok';
});

这样,当访问服务器后,会在cookie中留下两个字段:_user_user.sig,分别存放的是数据和数据签名:

_user = {"account":"test","password":"123456"}

_user.sig = 8onPj6-OdNwq2N9ON52GtkEZBX0

要验证一个cookie是否被篡改,需要用到ctx.cookies.get()函数:

1
ctx.cookies.get('_user', { signed: true })

如果数据不存在或者被篡改,则该语句的返回值为null


做了个小测试,想看看cookie中保存的签名字段到底是什么的签名,做了如下测试:

1
2
3
4
5
6
7
8
9
const Keygrip = require('keygrip');
const keys = new Keygrip(['secret'], 'sha1', 'base64');
const data = {
account: 'test',
password: '123456'
}

const hash = keys.sign(JSON.stringify(data))
console.log(hash)

结果发现输出结果是:J-89cUmdlRoSwdmub_5gBhLHdaQ,这和保存在cookie中的8onPj6-OdNwq2N9ON52GtkEZBX0不一致啊!

后来到Cookies的仓库pillarjs/cookies翻了一下源码,找到两处语句:

1
2
3
4
5
6
if (opts && signed) {
if (!this.keys) throw new Error('.keys required for signed cookies');
cookie.value = this.keys.sign(cookie.toString())
cookie.name += ".sig"
pushCookie(headers, cookie)
}
1
2
3
Cookie.prototype.toString = function() {
return this.name + "=" + this.value
};

受此启发,将签名的字符串改为_user={"account":"test","password":"123456"},再进行签名:

1
2
3
4
5
6
7
8
9
const Keygrip = require('keygrip');
const keys = new Keygrip(['secret'], 'sha1', 'base64');
const data = {
account: 'test',
password: '123456'
}

const hash = keys.sign(`_user=${JSON.stringify(data)}`);
console.log(hash)

得到结果:8onPj6-OdNwq2N9ON52GtkEZBX0,与保存在cookie中的结果一至!