JSON WEB TOKEN (jwt) 是目前最流行的跨域身份验证解决方案。
解决问题:session 不支持分布式框架,无法支持横向扩展,只能通过数据库来保存会话数据实现共享,如果持久层失效就会出现认证失败。
优点:服务器不保存任何会话数据,即服务器变为无状态,使其更容易扩展。
header 典型的由两部分组成:token 的类型(“JWT”)和算法名称(比如:HMAC SHA256或者RSA等等)。
{
'typ': "JWT",
'alg': "HS256"
}
将头部进行 base64 加密(该加密是可以对称解密的),得到 JWT 的第一部分
它包含声明(要求),声明是关于实体(通常是用户)和其他数据的声明。
声明有三种类型: registered(标准中注册的声明), public(公共的声明)和 private(私有的声明)。
Registered claims
: 一组预定义的声明,它们不是强制的,但是推荐。Public claims
: 可以随意定义。Private claims
: 用于在同意使用它们的各方之间共享信息,并且不是注册的或公开的声明。jwt 规定了 7 个官方字段(标准中注册的声明 (建议但不强制使用))
iss (issuer) jwt 签发人
exp (expiration time) jwt 过期时间
sub (subject) 主题 / jwt 所面向的用户
aud (audience) 受众 / 接收 jwt 的一方
nbf (Not Before) jwt 生效时间
iat (Issued At) jwt 签发时间
jti (JWT ID) 编号 / jwt 唯一身份标识
例子:
{
"sub": '1234567890',
"name": 'kaimo',
"admin": true
}
对 payload 进行 Base64 编码就得到 JWT 的第二部分
对前两个部分的签名,防止数据篡改。
需要编码过的 header、编码过的 payload 连接组成的字符串,然后通过 header 中指定的签名算法进行加盐 secret 组合加密,然后就构成了 jwt 的第三部分。
例子:
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
Base64 有三个字符:+
,/
,=
,在 URL 里面有特殊含义,所以要被替换掉:
=
被省略+
替换成 -
/
替换成 _
这个就是 Base64URL 算法。
实现个登录跟检测是否登录功能
npm i jwt-simple koa-bodyparser
这里使用简单点的 jwt-simple
,强一点的可以使用 jwtwebtoken
const Koa = require("koa");
const Router = require("@koa/router");
const bodyparser = require("koa-bodyparser");
const jwt = require("jwt-simple");
const app = new Koa();
let router = new Router();
app.use(bodyparser());
app.use(router.routes());
router.post("/login", async (ctx, next) => {
let { username, password } = ctx.request.body;
if (username === "admin" && password === "123456") {
let token = jwt.encode(username, "kaimo");
ctx.body = {
err: 0,
username,
token
};
}
});
router.get("/validate", async (ctx, next) => {
let authorization = ctx.headers.authorization;
try {
let r = jwt.decode(authorization, "kaimo");
ctx.body = {
err: 0,
username: r
};
} catch (error) {
ctx.body = {
err: 1,
message: error
};
}
});
app.listen(3000);
console.log("Server running at http://127.0.0.1:3000/");
控制台可以访问:
curl -v -X POST --data "username=admin&password=123456" http://localhost:3000/login
我这里直接使用的 postman
http://localhost:3000/login
访问 http://localhost:3000/validate
这个 base64urlUnescape 方法反解 base64 的方法,可以参考 jwt-simple
里的实现,是规定的解法。
下面实现自己的 jwt
const Koa = require("koa");
const Router = require("@koa/router");
const bodyparser = require("koa-bodyparser");
const crypto = require("crypto");
const app = new Koa();
let router = new Router();
app.use(bodyparser());
app.use(router.routes());
let kaimoJwt = {
toBase64Url(base64) {
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
},
toBase64(content) {
return Buffer.from(JSON.stringify(content)).toString("base64");
},
sign(content, secret) {
return this.toBase64Url(crypto.createHmac("sha256", secret).update(content).digest("base64"));
},
encode(payload, secret) {
console.log("进入 kaimo-jwt--encode--->", payload, secret);
let header = this.toBase64Url(
this.toBase64({
typ: "JWT",
alg: "HS256"
})
);
let content = this.toBase64Url(this.toBase64(payload));
let sign = this.sign([header, content].join("."), secret);
return [header, content, sign].join(".");
},
base64urlUnescape(str) {
str += new Array(5 - (str.length % 4)).join("=");
return str.replace(/\-/g, "+").replace(/_/g, "/");
},
decode(token, secret) {
console.log("进入 kaimo-jwt--decode--->", token, secret);
let [header, content, sign] = token.split(".");
console.log("header---->", header);
console.log("content---->", content);
console.log("sign---->", sign);
let newSign = this.sign([header, content].join("."), secret);
console.log("newSign---->", newSign);
if (sign === newSign) {
// 将 base64 转化成字符串
return JSON.parse(Buffer.from(this.base64urlUnescape(content), "base64").toString());
} else {
throw new Error("token 被篡改");
}
}
};
router.post("/login", async (ctx, next) => {
let { username, password } = ctx.request.body;
if (username === "admin" && password === "123456") {
let token = kaimoJwt.encode(username, "kaimo");
ctx.body = {
err: 0,
username,
token
};
}
});
router.get("/validate", async (ctx, next) => {
let authorization = ctx.headers.authorization;
try {
let r = kaimoJwt.decode(authorization, "kaimo");
ctx.body = {
err: 0,
username: r
};
} catch (error) {
ctx.body = {
err: 1,
message: error
};
}
});
app.listen(3000);
// eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.ImFkbWluIg.prJEZUHxS0PtLnjZ0RwhUyQ-4HkANXZVAlIHU3ZNh7k
console.log("Server running at http://127.0.0.1:3000/");
效果也是一样的