|
| 1 | +--- |
| 2 | +title: jwt |
| 3 | +author: 高红翔 |
| 4 | +date: 2024-10-08 11:27:55 |
| 5 | +categories: |
| 6 | +tags: |
| 7 | +--- |
| 8 | + |
| 9 | +## 1.JWT |
| 10 | + |
| 11 | +- JWT(json web token)是为了在网络应用环境间传递声明而执行的一种基于 JSON 的开放标准。 |
| 12 | +- JWT 的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。比如用在用户登录上。 |
| 13 | +- 因为数字签名的存在,这些信息是可信的,JWT 可以使用 HMAC 算法或者是 RSA 的公私秘钥对进行签名 |
| 14 | + |
| 15 | +## 2.主要应用场景 |
| 16 | + |
| 17 | +- 身份认证在这种场景下,一旦用户完成了登陆,在接下来的每个请求中包含 JWT,可以用来验证用户身份以及对路由,服务和资源的访问权限进行验证。 |
| 18 | +- 信息交换在通信的双方之间使用 JWT 对数据进行编码是一种非常安全的方式,由于它的信息是经过签名的,可以确保发送者发送的信息是没有经过伪造的 |
| 19 | + |
| 20 | +## 3.JWT 的结构 |
| 21 | + |
| 22 | +JWT 包含了使用`.`分隔的三部分 |
| 23 | + |
| 24 | +- Header 头部 |
| 25 | +- Payload 负载 |
| 26 | +- Signature 签名 |
| 27 | + |
| 28 | +### 3.1 Header |
| 29 | + |
| 30 | +在 header 中通常包含了两部分:token 类型和采用的加密算法。 |
| 31 | + |
| 32 | +```json |
| 33 | +{ "alg": "HS256", "typ": "JWT" } |
| 34 | +``` |
| 35 | + |
| 36 | +接下来对这部分内容使用`Base64Url`编码组成了`JWT`结构的第一部分。 |
| 37 | + |
| 38 | +### 3.2 Payload |
| 39 | + |
| 40 | +负载就是存放有效信息的地方。这个名字像是指货车上承载的货物,这些有效信息包含三个部分 |
| 41 | + |
| 42 | +- 标准中注册的声明 |
| 43 | +- 公共的声明 |
| 44 | +- 私有的声明 |
| 45 | + |
| 46 | +```js |
| 47 | +{ "name": "zhangsan"} |
| 48 | +``` |
| 49 | + |
| 50 | +上述的负载需要经过`Base64Url`编码后作为 JWT 结构的第二部分 |
| 51 | + |
| 52 | +### 3.3 Signature |
| 53 | + |
| 54 | +- 创建签名需要使用编码后的 header 和 payload 以及一个秘钥 |
| 55 | + |
| 56 | +- 使用 header 中指定签名算法进行签名 |
| 57 | + |
| 58 | +- 例如如果希望使用 HMAC SHA256 算法,那么签名应该使用下列方式创建 |
| 59 | + |
| 60 | + ```js |
| 61 | + HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret) |
| 62 | + ``` |
| 63 | + |
| 64 | +- 签名用于验证消息的发送者以及消息是没有经过篡改的 |
| 65 | + |
| 66 | +- 完整的 JWT 完整的 JWT 格式的输出是以. 分隔的三段 Base64 编码 |
| 67 | + |
| 68 | +- 密钥 secret 是保存在服务端的,服务端会根据这个密钥进行生成 token 和验证,所以需要保护好。 |
| 69 | + |
| 70 | +## 4.如何使用 JWT |
| 71 | + |
| 72 | +1. 当用户使用它的认证信息登陆系统之后,会返回给用户一个 JWT |
| 73 | + |
| 74 | +2. 用户只需要本地保存该 token(通常使用 local storage,也可以使用 cookie)即可 |
| 75 | + |
| 76 | +3. 当用户希望访问一个受保护的路由或者资源的时候,通常应该在 Authorization 头部使用 Bearer 模式添加 JWT,其内容看起来是下面这样 |
| 77 | + |
| 78 | + ```js |
| 79 | + Authorization: Bearer <token> |
| 80 | + ``` |
| 81 | + |
| 82 | +4. 因为用户的状态在服务端的内存中是不存储的,所以这是一种无状态的认证机制 |
| 83 | + |
| 84 | +5. 服务端的保护路由将会检查请求头 Authorization 中的 JWT 信息,如果合法,则允许用户的行为。 |
| 85 | + |
| 86 | +6. 由于 JWT 是自包含的,因此减少了需要查询数据库的需要 |
| 87 | + |
| 88 | +7. JWT 的这些特性使得我们可以完全依赖其无状态的特性提供数据 API 服务,甚至是创建一个下载流服务。 |
| 89 | + |
| 90 | +8. 因为 JWT 并不使用 Cookie 的,所以你可以使用任何域名提供你的 API 服务而不需要担心跨域资源共享问题(CORS) |
| 91 | + |
| 92 | + |
| 93 | + |
| 94 | +## 5. JWT 实战 |
| 95 | + |
| 96 | +### 5.1 server.js |
| 97 | + |
| 98 | +```js |
| 99 | +const Koa = require('koa'); |
| 100 | +const app = new Koa(); |
| 101 | +const Router = require('koa-router'); |
| 102 | +const router = new Router(); |
| 103 | +const bodyParser = require('koa-bodyparser'); |
| 104 | +const jwt = require('./jwt-simple'); |
| 105 | +const secretKey = 'jwt-secret'; |
| 106 | +app.use(bodyParser()); |
| 107 | +router.get('/login', async (ctx) => { |
| 108 | + ctx.body = ` |
| 109 | + <form action="/login" method="post"> |
| 110 | + <input type="text" name="username" /> |
| 111 | + <input type="submit" value="提交" /> |
| 112 | + </form> |
| 113 | + `; |
| 114 | +}); |
| 115 | +const expirationTime = 60 * 60 * 24; // 过期时间为1天(单位为秒) |
| 116 | +const expirationDate = Math.floor(Date.now() / 1000) + expirationTime; // 计算过期时间戳 |
| 117 | +router.post('/login', async ctx => { |
| 118 | + const { username } = ctx.request.body; |
| 119 | + const token = jwt.encode({ username,exp: expirationDate }, secretKey); |
| 120 | + ctx.body = { token }; |
| 121 | +}); |
| 122 | +router.get('/user', async ctx => { |
| 123 | + const authorizationHeader = ctx.request.headers.authorization; |
| 124 | + if (authorizationHeader && authorizationHeader.startsWith('Bearer ')) { |
| 125 | + const token = authorizationHeader.substring(7); |
| 126 | + try { |
| 127 | + const decoded = jwt.decode(token, secretKey); |
| 128 | + ctx.body = decoded.username; |
| 129 | + } catch (error) { |
| 130 | + ctx.status = 401; |
| 131 | + ctx.body = 'Invalid token'; |
| 132 | + } |
| 133 | + } else { |
| 134 | + ctx.status = 401; |
| 135 | + ctx.body = 'Missing token'; |
| 136 | + } |
| 137 | +}); |
| 138 | +app.use(router.routes()); |
| 139 | +app.listen(3000, () => { |
| 140 | + console.log('Server is running at http://localhost:3000'); |
| 141 | +}); |
| 142 | +curl -H "Authorization: Bearer token" http://localhost:3000/user |
| 143 | +``` |
| 144 | + |
| 145 | +### 5.2 jwt-simple.js |
| 146 | + |
| 147 | +jwt-simple.js |
| 148 | + |
| 149 | +```js |
| 150 | +const crypto = require("crypto") |
| 151 | + |
| 152 | +/** |
| 153 | + * 编码JWT令牌 |
| 154 | + * @param {Object} payload 负载数据 |
| 155 | + * @param {string} key 密钥 |
| 156 | + * @returns {string} 编码后的JWT令牌 |
| 157 | + */ |
| 158 | +function encode(payload, key) { |
| 159 | + let header = { type: "JWT", alg: "sha256" } // 声明类型和算法 |
| 160 | + var segments = [] // 声明一个数组 |
| 161 | + segments.push(base64urlEncode(JSON.stringify(header))) // 对header进行base64编码 |
| 162 | + segments.push(base64urlEncode(JSON.stringify(payload))) // 对负载进行base64编码 |
| 163 | + segments.push(sign(segments.join("."), key)) // 加入签名 |
| 164 | + return segments.join(".") |
| 165 | +} |
| 166 | + |
| 167 | +/** |
| 168 | + * 生成签名 |
| 169 | + * @param {string} input 输入数据 |
| 170 | + * @param {string} key 密钥 |
| 171 | + * @returns {string} 签名 |
| 172 | + */ |
| 173 | +function sign(input, key) { |
| 174 | + return crypto.createHmac("sha256", key).update(input).digest("base64") |
| 175 | +} |
| 176 | + |
| 177 | +/** |
| 178 | + * 解码JWT令牌 |
| 179 | + * @param {string} token JWT令牌 |
| 180 | + * @param {string} key 密钥 |
| 181 | + * @returns {Object} 解码后的负载数据 |
| 182 | + * @throws {Error} 如果验证失败或令牌过期,则抛出错误 |
| 183 | + */ |
| 184 | +function decode(token, key) { |
| 185 | + var segments = token.split(".") |
| 186 | + var headerSeg = segments[0] |
| 187 | + var payloadSeg = segments[1] |
| 188 | + var signatureSeg = segments[2] |
| 189 | + var payload = JSON.parse(base64urlDecode(payloadSeg)) |
| 190 | + if (signatureSeg != sign([headerSeg, payloadSeg].join("."), key)) { |
| 191 | + throw new Error("verify failed") |
| 192 | + } |
| 193 | + if (payload.exp && Date.now() > payload.exp * 1000) { |
| 194 | + throw new Error("Token expired") |
| 195 | + } |
| 196 | + return payload |
| 197 | +} |
| 198 | + |
| 199 | +/** |
| 200 | + * Base64 URL编码 |
| 201 | + * @param {string} str 输入字符串 |
| 202 | + * @returns {string} 编码后的字符串 |
| 203 | + */ |
| 204 | +function base64urlEncode(str) { |
| 205 | + return Buffer.from(str).toString("base64") |
| 206 | +} |
| 207 | + |
| 208 | +/** |
| 209 | + * Base64 URL解码 |
| 210 | + * @param {string} str 编码的字符串 |
| 211 | + * @returns {string} 解码后的字符串 |
| 212 | + */ |
| 213 | +function base64urlDecode(str) { |
| 214 | + return Buffer.from(str, "base64").toString() |
| 215 | +} |
| 216 | + |
| 217 | +module.exports = { |
| 218 | + encode, |
| 219 | + decode, |
| 220 | +} |
| 221 | +``` |
0 commit comments