什么是 jwt
JSON WEB TOKEN (jwt) 是目前最流行的跨域身份验证解决方案。
解决问题:session 不支持分布式框架,无法支持横向扩展,只能通过数据库来保存会话数据实现共享,如果持久层失效就会出现认证失败。
优点:服务器不保存任何会话数据,即服务器变为无状态,使其更容易扩展。
jwt 组成
Header 头部
header 典型的由两部分组成:token 的类型(“JWT”)和算法名称(比如:HMAC SHA256或者RSA等等)。
{ 'typ': "JWT", 'alg': "HS256" }
将头部进行 base64 加密(该加密是可以对称解密的),得到 JWT 的第一部分
Payload 负载、负荷
它包含声明(要求),声明是关于实体(通常是用户)和其他数据的声明。
声明有三种类型: 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 的第二部分
Signature
对前两个部分的签名,防止数据篡改。
需要编码过的 header、编码过的 payload 连接组成的字符串,然后通过 header 中指定的签名算法进行加盐 secret 组合加密,然后就构成了 jwt 的第三部分。
例子:
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
Base64URL 算法
Base64 有三个字符:+
,/
,=
,在 URL 里面有特殊含义,所以要被替换掉:
=
被省略+
替换成-
/
替换成_
这个就是 Base64URL 算法。
jwt-simple 的使用
实现个登录跟检测是否登录功能
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
实现自己的 jwt
这个 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/");
效果也是一样的