如何实现 JWT
鉴权机制
1. 什么是 JWT
JWT
是 JSON Web Token
的缩写,本质就是一个字符串书写规范,如下图,作用是用来在用户和服务器之间传递安全可靠的信息。
eyJpZCI6IjEyMzQ1Njc4OTAiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjIsImV4cCI6MTUxNjIzOTAyMn0=.
4D0aszyNAMMKlf8m6l2Q9yyOn4ed1EGXoXgRi3wwX8w=
在目前前后端分离的开发过程中,使用 token
鉴权机制用于身份验证是最常见的方案,流程如下
- 服务器当验证用户账号和密码正确的时候,给用户颁发一个令牌,作为后续用户请求的凭证
- 后续访问会根据这个令牌判断用户是否有权限进行访问
Token
,分成了三部分,头部(header
)、载荷(payload
)和签名(signature
),并一 .
进行拼接。其中头部和荷载都是以 JSON
格式存放数据,只是进行了 base64
编码,签名是对前两部分的签名,用于验证数据的完整性。
头部( header
)
每个 JWT
都会带有头部信息,这里主要声明使用的算法。声明算法的字段名为 alg
,同时还有一个 typ
字段,表示这个令牌的类型,默认 JWT
即可。以下示例中算法为 HS256
,表示使用 HMAC-SHA256
算法进行签名。
{
"alg": "HS256",
"typ": "JWT"
}
因为 JWT
是字符串,所以我们还需要对以上内容进行 base64
编码,得到如下结果
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
载荷( payload
)
载荷即消息体,这里会存放实际的内容,也就是 Token
的数据声明,例如用户的 id
和 name
,默认情况下也会携带令牌的签发时间 iat
,以及过期时间 exp
,这些都是可选的,但是建议都加上,以提高安全性。如下:
{
"id": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1516239022
}
同时进行 base64
编码,得到如下结果
eyJpZCI6IjEyMzQ1Njc4OTAiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjIsImV4cCI6MTUxNjIzOTAyMn0=
签名( signature
)
签名是对头部和载荷内容进行签名,一般情况,设置一个 secretKey
,对前两个的结果进行 HMACSHA256
算法进行签名,使用 crypto-js
库进行签名,得到如下结果
const secretKey = "9731dbbc-cc11-4432-8e8c-902ab57332d2";
const signature = hmacSHA256(base64Url(header) + base64Url(payload), secretKey);
// signature: 4D0aszyNAMMKlf8m6l2Q9yyOn4ed1EGXoXgRi3wwX8w=
最后,将以上三部分用 .
进行拼接,得到最终的 JWT
字符串
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJpZCI6IjEyMzQ1Njc4OTAiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjIsImV4cCI6MTUxNjIzOTAyMn0=.
4D0aszyNAMMKlf8m6l2Q9yyOn4ed1EGXoXgRi3wwX8w=
一旦前面两部分数据被篡改,只要服务器加密用的密钥没有泄露,得到的签名肯定和之前的签名不一致
2. 如何实现 JWT
Token
的会用分成了两部分:
- 生成
Token
: 登录成功的时候,颁发Token
给用户 - 验证
Token
: 用户访问需要验证的接口的时候,需要验证Token
是否有效
生成 Token
借助第三方库 jsonwebtoken
,通过 jsonwebtoken
的 sign
方法生成一个 Token
,代码如下:
- 第一个参数指的是
Payload
,即JWT
的第二部分,存放有效信息的地方,一般存放用户的id
和name
等信息 - 第二个参数指的是
secretKey
,即JWT
的第三部分,用于对前两部分进行签名 - 第三个参数指的是
options
,即JWT
的第一部分,用于指定生成Token
的一些选项,例如algorithm
指定签名算法,expiresIn
指定过期时间,issuer
指定签发人等
const crypto = require("crypto"),
jwt = require("jsonwebtoken");
// TODO:使用数据库
// 这里应该是用数据库存储,这里只是演示用
let userList = [];
class UserController {
// 用户登录
static async login(ctx) {
const data = ctx.request.body;
if (!data.name || !data.password) {
return (ctx.body = {
code: "000002",
message: "参数不合法",
});
}
const result = userList.find(
(item) =>
item.name === data.name &&
item.password ===
crypto.createHash("md5").update(data.password).digest("hex")
);
if (result) {
// 生成token
const token = jwt.sign(
{
name: result.name,
},
"test_token", // secret
{ expiresIn: 60 * 60 } // 过期时间:60 * 60 s
);
return (ctx.body = {
code: "0",
message: "登录成功",
data: {
token,
},
});
} else {
return (ctx.body = {
code: "000002",
message: "用户名或密码错误",
});
}
}
}
module.exports = UserController;
在前端接收到 token
之后,一般情况会通过 loaclStorage
进行缓存,然后将 token
放到 HTTP
请求头 Authorization
中,关于 Authorization
的设置,前面要加上 Bearer
,表示 JWT
的类型,最后得到的 Authorization
的值为 Bearer ${token}
axios.interceptors.request.use((config) => {
const token = localStorage.getItem("token");
config.headers.common["Authorization"] = "Bearer " + token; // 留意这里的 Authorization
return config;
});
验证 Token
使用 koa-jwt
中间件进行验证,方式比较简单
// 注意:放在路由前面
app.use(
koajwt({
secret: "test_token",
}).unless({
// 配置白名单
path: [/\/api\/register/, /\/api\/login/],
})
);
secret
必需和sign
方法中的secret
一致- 可以通过
unless
配置白名单,也就是不需要验证的接口 - 校验的中间件需要放在需要校验的路有前面,无法对前面的路由进行校验
获取 token
用户的信息方法如下:
router.get("/api/userInfo", async (ctx, next) => {
const authorization = ctx.header.authorization; // 获取jwt
const token = authorization.replace("Beraer ", "");
const result = jwt.verify(token, "test_token");
ctx.body = result;
});
注意:上述的 HMA256
加密算法为单秘钥的形式,一旦泄漏后非常的危险
在分布式系统中,每个子系统都要获取到秘钥,那么这个子系统根据该秘钥可以发布和验证令牌,但有些服务器只需要验证令牌
这时候可以采用非对称加密,利用私钥签发令牌,公钥验证令牌,这样就可以保证私钥不会泄漏,而公钥可以随意发布,加密算法可以选择 RS256
3. JWT
的优缺点
优点
json
具有通用性,可以在不同的编程语言中使用- 组成简单,字节占用小,便于传输
- 服务端无需保存会花信息,很容易进行水平扩展
- 一处生成,多出使用,可以在分布式系统中,解决单点登录问题
- 可防护
CSRF
攻击
缺点
payload
部分仅仅是进行简单编码,所以只能用于存储逻辑必需的非敏感信息- 需要保护好加密密钥,一旦泄露后果不堪设想
- 为避免
token
被劫持,最好使用https
协议