一篇文章告诉你JWT的实现原理
传统的 session 流程
- 浏览器发起请求登陆
- 服务端验证身份,生成身份验证信息,存储在服务端,并且告诉浏览器写入 Cookie
- 浏览器发起请求获取用户资料,此时 Cookie 内容也跟随这发送到服务器
- 服务器发现 Cookie 中有身份信息,验明正身
- 服务器返回该用户的用户资料
JWT 流程
- 浏览器发起请求登陆
- 服务端验证身份,根据算法,将用户标识符打包生成 token, 并且返回给浏览器
- 浏览器发起请求获取用户资料,把刚刚拿到的 token 一起发送给服务器
- 服务器发现数据中有 token,验明正身
- 服务器返回该用户的用户资料
- session 存储在服务端占用服务器资源,而 JWT 存储在客户端
- session 存储在 Cookie 中,存在伪造跨站请求伪造攻击的风险
- session 只存在一台服务器上,那么下次请求就必须请求这台服务器,不利于分布式应用
- 存储在客户端的 JWT 比存储在服务端的 session 更具有扩展性
- …
JWT 工作原理
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpcCI6IjEyNy4wLjAuMSIsInV1aWQiOiJmZjEyMTJmNS1kOGQxLTQ0OTYtYmY0MS1kMmRkYTczZGUxOWEiLCJpYXQiOjE1Mjc1MjMwMTd9.1C01cpOf1N3M9YAYvQjfcLbDGHZ4iPVncCGIoG-lpO0jHOIA_ZHtSMDvK1nzArLpGK5syQSwExsZJz2FJsd2W2TUiHQYtzmQTU8OBXX6mfSZRlkts675W5_WhIiOEwz69GFSD0AKXZifCRgIpKLC0n273MRMr0wJnuBi9ScfJ7YjSiqCr7qyQ5iXeOdS3ObT3wdjjk-Wu9wbWM7R25TFb-7PEZY7r_e8jmczPCVcNbOYegedu73T4d30kRn2jKufTGntD5hR6T9AQsgAMwVR1edEFflWb772TmrHI7WZOAivsBCN9sr4YiyYMvE8lcz_mBsgsunugGiHA3DGxB2ORbjIC8NPm8FI25zGOh9JIM2r_jFFTIm9GiuKtC8Ck8N3-eWi9u1NgBxwLdgN5JyCORnIOlciQEsScg-3SdCTM5LH_j6CeqQNwJxT4-oENzqLSTDJbP-SOj9nnx8HnJ5wh3n64rAvtc89CeTk7PhWFjksHDifngN-cnaszl5lqoF1enz5i9FYYELSjM-G7jns2SyY1MQeLRjuEDriPZtFaGmTW-RLH3gJfQXtbdpEo0_nHBqXEohwoN_FLKo4BNrEwshpyA7vkBpCQC0QALKyC1_L1Q5qduR6dDcqRozAo2VqJXmAKN0rvhLuIEHZkicOZY0Ds4So4GCcleqvFcxm1HQ
Header
{
"alg": "RS256",
"typ": "JWT"
}
- alg: 声明加密的算法
- typ: 声明类型
const headerBuff = Buffer.from(
JSON.stringify({
alg: "RS256",
typ: "JWT"
})
);
const header = headerBuff.toString("base64");
console.log(header);
// eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9
Payload
- 标准声明
- 公共声明
- 私有声明
interface Stantar {
iss?: string; // JWT的签发者
sub?: string; // JWT所面向的用户
aud?: string; // 接收JWT的一方
exp?: number; // JWT的过期时间
nbf?: number; // 在xxx日期之间,该JWT都是可用的
iat?: number; // 该JWT签发的时间
jti?: number; //JWT的唯一身份标识
}
interface Public {
[key: string]: any;
}
interface Private {
[key: string]: any;
}
{
"ip": "127.0.0.1",
"uuid": "ff1212f5-d8d1-4496-bf41-d2dda73de19a",
"iat": 1527523017
}
const payloadBuffer = Buffer.from(
JSON.stringify({
ip: "127.0.0.1",
uuid: "ff1212f5-d8d1-4496-bf41-d2dda73de19a",
iat: 1527523017
})
);
const payload = payloadBuffer.toString("base64");
console.log(payload);
// eyJpcCI6IjEyNy4wLjAuMSIsInV1aWQiOiJmZjEyMTJmNS1kOGQxLTQ0OTYtYmY0MS1kMmRkYTczZGUxOWEiLCJpYXQiOjE1Mjc1MjMwMTd9
Signature
const crypto = require("crypto");
const sign = crypto.createSign("SHA256");
const secret = `私钥,太长我就不贴出来了`;
sign.write(header + "." + payload);
sign.end();
const signature = sign
.sign(secret, "base64")
// 在JWT库中,已经把这些字符过滤掉了
.replace(/=/g, "")
.replace(/\+/g, "-")
.replace(/\//g, "_");
console.log(signature);
它是如何做身份验证的?
Token 的过期时间怎么确定?
如何防止 Token 被串改?
- 客户端可以自行签发 token
- 黑客/中间人可以肆意篡改 token
安全性相关
- 缩短 token 有效时间
- 使用安全系数高的加密算法
- token 不要放在 Cookie 中,有 CSRF 风险
- 使用 HTTPS 加密协议
- 对标准字段 iss、sub、aud、nbf、exp 进行校验
- 使用成熟的开源库,不要手贱造轮子
- 特殊场景下可以把用户的 UA、IP 放进 payload 进行校验(不推荐)