在网站应用中,由于 HTTP 协议是无状态的,识别不同用户的主要方法是通过 Cookie,即用户在认证后发送它让浏览器存储,之后每一次请求中再附带这个请求头信息发回服务器,这样服务器便知道是哪位用户发出的请求。
具体实现方法有哪些呢?下面我将为大家介绍几种常见的通过 Cookie 来识别不同用户的方法。
第一类:信息存用户侧
第一类的方法是将用户的部分信息存在浏览器的 Cookie 中(如用户 ID)。那么首要问题就是要防止被人为修改,因此实现方法有两种。
方法一:Base64 数据 + HMAC
首先,我们需要理解 HMAC。
HMAC,即使用哈希算法的消息认证码,简单来说就是使用密钥(准确来说是盐 salt)给数据上一个签名。
由于哈希算法是公开的,所以需要加上一段独一无二的密钥进行签名,如果没有这个密钥,几乎不可能篡改哈希值。
你也可以使用一套简易方法来自己实现消息认证码,比如如下的伪代码:
// 密钥
key = "你的密钥(盐)"// 消息
msg = "一些数据"// 使用 SHA256 算法进行签名
sha256(msg + key)// 输出省略
这样把密钥附加到消息后面,就实现了简单的消息认证码签名。但是还是建议使用现有的 HMAC
算法进行签名。
目前推荐哈希算法使用 SHA-2
系列,具体的有 SHA-224 SHA-256 SHA-384 SHA-512
,也许你听说过 MD5
,但 MD5
算法已被证明存在碰撞不再安全,SHA-1
也正在被逐渐淘汰,所以直接使用 SHA-2
即可。
接下来,应当使用 Base64
对数据本体进行编码,这么做并不是确保数据安全,因为 Base64
仅仅是一种公开编码规范,并没有加密,主要是确保数据中不会出现违反 HTTP
规范的字符导致出错。
整体流程如下:
- 用户登录,服务器已验证用户身份。
- 服务器将用户信息进行
Base64
编码。 - 服务器将 Base64 编码后的用户信息使用密钥(盐)进行
HMAC
签名。 - 将这两个信息与响应同时发送给用户,使用
Set-Cookie
响应头让浏览器自动设置Cookie
。 - 用户下次请求时,浏览器将会自动将这两个信息发送回来,服务器再次使用相同算法计算用户数据哈希值,与用户发回的签名进行对比,如果一致则说明数据未被篡改,这样就验证了用户的身份。
光看文字可能不好理解,接下来使用伪代码进行解释:
userData = {"uid": 1,// 建议始终写入这个过期时间数据,防止 Cookie 有效期被篡改导致无限登录"expireAt": "2021-05-20 00:00:00"
}// 密钥,密钥生成请使用密码学上的随机数生成器,不要人为指定,建议不少于 16 个字节
secretKey = "这是你的密钥,不要泄露"// 对用户数据进行 Base64 编码
base64UserData = base64.encode(userData)// 对编码后数据进行签名并转换为十六进制字符串
sign = HmacSHA256(data=base64UserData, key=secretKey).toHex()// 可以使用逗号连接用户数据和签名,也可以另外使用一个 Cookie 值来存储签名,建议放一起方便管理
cookieValue = base64UserData + "," + sign// 设置响应中的 Set-Cookie 头
// 这里设置为一天后过期,请始终设置过期时间,并与前面用户数据中的 expireAt 一致
// 注意!为了防止 CSRF 攻击,始终设置 HttpOnly,具体请参照:
// https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Cookies#%E9%99%90%E5%88%B6%E8%AE%BF%E9%97%AE_cookie
response.setHeader("Set-Cookie", `session=${cookieValue}; Max-Age=86400; Path=/; HttpOnly`)// 写入响应数据,这个随意,主要是设置 Set-Cookie 头
response.send("登录成功!")
顺利的话,浏览器就会接受这个响应头并设置 Cookie。
响应大概长这样(省略其他响应头):
HTTP/1.1 200 OK
Set-Cookie: session=编码后的数据,数据签名; Max-Age=86400; Path=/; HttpOnly
当服务器收到用户后续请求时,处理如下:
- 从
Cookie
头中提取你设置的键对应的值,这里假设键为session
。 - 分离数据和签名,如果给签名另外分配了 Cookie 则另外取出来。
- 对数据进行与发送响应阶段一样的签名计算。
- 将计算后的签名与收到的签名进行对比,一致则说明数据未被篡改。
- 检查是否过期,根据上述数据中的
expireAt
键值确定。如果过期,即使数据未被篡改也应视为不可信。
收到的请求大概长这样:
POST /send-comment HTTP/1.1
Cookie: session=编码后的数据,数据签名; ...其他 cookie,也许没有
伪代码如下:
// 取出 cookie
cookie = request.getCookie('session')// 分离签名和数据
data, sign = cookie.split(',')// 计算数据签名
sign1 = HmacSHA256(data=data, key="你的密钥")// 对比签名是否一致
if (sign !== sign1) {// 验签失败response.send("cookie 遭到篡改")return
}// 验签成功,Base64 解码数据
decodedData = base64.decode(data)// 验证是否过期
if (decodedData["expireAt"] < now()) {// 数据过期,不可信response.send("cookie 过期,请重新登录")return
}// 完全验证成功,已获取到用户身份,接下来继续写代码逻辑
uid = decodedData["uid"]
response.send("欢迎!用户 UID 为 " + String(uid))
优点:
- 信息存用户侧,无需服务端存储,方便 。
- Base64 编码速度快。
缺点:
- 由于数据并不是加密,所以不能存储敏感信息(如验证码答案)。
- 不能存储大量信息,因为 Cookie 的长度在不同浏览器有不同程度的限制。
- 密钥泄露就完犊子。
方法二:对称加密数据
对称加密,即使用同一密钥对数据进行加密和解密,最常用的对称加密算法叫做 AES。
AES 又分为 AES-128
、AES-256
、AES-512
。区别是它们分别使用密钥长度为 16,32,64 字节,AES- 后面跟着的数字单位为 bit,1 字节=8 bit,这样就好理解些。
感觉有点复杂,本文也不是讲加密算法的,所以如果你不是很明白 AES,建议使用现成的库来进行加密解密。比如 JS 有 crypto-js,这个库你只需要提供密钥(长度不限),剩下的他会帮你自动解决。
你只需要知道以下几点即可:
- AES 使用同一密钥进行加密解密,也就是说这个密钥绝对不能泄露。
- 如果你能选择加密模式,禁止使用 ECB 模式(不安全)。
- 部分加密模式加密后的数据若遭到篡改,将会导致解密失败(乱码或者报错),一定程度上相当于方法一中的 HMAC。如果你还是不放心,可以使用 CCM 或 GCM 模式(带签名的 AES 加密)。
接下来的代码与方法一重复部分不再详细讲解。
这个方法实现起来比方法一要简单一些,而且能存储敏感信息。伪代码如下:
// 用户数据
userData = {"uid": 1,"expireAt": "2021-05-20 00:00:00"
}// 密钥
key = "你的密钥"// 对数据加密
encryptedData = AES.encrypt(userData, key)// 设置响应头及发送响应省略
接收到了后续用户请求,伪代码如下:
// 获取 Cookie
cookie = request.getCookie("session")key = "你的密钥"// 尝试解密// 要注意的是,有的加密模式解密失败时并不会报错,而返回的是乱码,这个请根据实际情况确定
decryptedData = AES.decrypt(cookie, key)// 如果你的数据是 JSON 数据,那么只要尝试解析 JSON 数据失败了,就认定是解密失败try {jsonData = JSON.parse(decryptedData)
} catch(err) {// 解密失败response.send("数据解密失败")return
}// 数据解密成功,未遭到篡改,继续代码逻辑
优点:
- 数据经过了加密,可以存储敏感数据。
- 比方法一要简单一些。
缺点:
- 根据不同加密模式,可能会影响性能
- 同样不能存储大量数据。
- 密钥泄露就完犊子。
第二类:信息存服务器侧
这个就非常简单了。方法如下:
- 生成随机 ID(可使用 UUID 标准,只要是无法预测的随机 ID 即可)。
- 在数据库以该 ID 为键,然后记录用户信息。
- 给用户设置该 ID 为 Cookie。
在用户下次请求时,便会附上这个 ID,根据这个 ID 去数据库查就行了。
这个方法虽然简单,但是一定要确保 ID 是随机,如果能预测就没任何含义了。
优点:
- 生成的 ID 长度固定,不受限于 Cookie 长度限制。
- 数据存服务器,用户无论如何也看不到,而且当用户未请求时也可以获取或修改这个信息。
缺点:
- 还是数据存服务器,服务端得另外部署相关服务(如 Redis, MySQL, MongoDB 等数据库)。
总结
以上所有 Cookie 解决方案,只有一个目的,就是为了防止用户篡改数据,而不是为了防止数据泄露。想防止数据被第三方截获,请使用 HTTPS。
不同方法有不同的优缺点,还需根据实际情况决定。
https://www.passkou.com/