# 无状态协议
由于 HTTP 协议的无状态性(客户端的每次 HTTP 请求都是独立的,服务器不会主动保留 HTTP 请求的状态)。
# 一、Cookie
# 1、Cookie 认知
Cookie 是某些网站为了辨别用户身份而存储在用户本地终端上的数据。
提示
Cookie 通常是由后端设置,用来区分 HTTP 请求的身份信息
但由于浏览器机制的原因,Cookie 并不安全:
缺陷
由于浏览器也提供了读写 Cookie 的 API,所以 Cookie 很容易被伪造(安全性有待考量)。
// 1、设置Cookie
document.cookie = 'username=lencamo'
// 2、读取Cookie
javascript: alert(document.cookie)
// 【当然还可以通过 开发者工具的Application、浏览器地址前的图标查看】
# 2、Cookie 分类
根据 Cookie 在客户端存储的位置,我们可以将 Cookie 分为内存 Cookie 和硬盘 Cookie。
<button>存储Cookie</button>
<script>
const btnEl = document.querySelector('button')
btnEl.onclick = () => {
// 内存 Cookie(浏览器关闭时销毁)
document.cookie = 'name=lencamo;'
// 硬盘 Cookie(指定时间内自动销毁)
document.cookie = 'age=22;max-age=10;'
}
</script>
# 3、Cookie 组成
Cookie 是存储在浏览器中的不超过 4kb 的字符串。由 内容(键值对) + 其他可选属性组成(有效期、安全性、使用范围)
通过浏览器上的 Application 中的 Cookie 中可以看到其展示的 Cookie 属性
- name 和 value 属性
document.cookie = 'name=lencamo;'
- Domain 属性
指定哪些主机(ip 地址)可以接受 Cookie
// - 默认值为origin,不包含子域名
//
// - 手动设置Domain时,会包含子域名(login.taobao.com、market.m.taobao.com)
document.cookie = 'name=lencamo;domain=taobao.com'
- Path 属性
指定主机下的哪些路径可以接受 Cookie
// - 默认值为 /
// - 一般不用设置
- Expires/Max-age 属性
设置过期时间
document.cookie = 'age=22;max-age=10;'
# 4、Cookie 认证
提示
浏览器在发送网络请求时,会自动为该请求携带上 Cookie
当然,前提是我们通过特定的请求(如:/login 等),触发了服务器的 Cookie 设置
const koa = require('koa')
const { bodyParser } = require('@koa/bodyparser')
const Router = require('@koa/router')
const app = new koa()
app.use(bodyParser())
const userRouter = new Router({ prefix: '/user' })
userRouter.get('/login', (ctx, next) => {
ctx.cookies.set('slogan', 'ren-sir666', {
maxAge: 10 * 1000 // 10秒
})
ctx.body = '登录成功!!!'
})
userRouter.get('/cartList', (ctx, next) => {
const value = ctx.cookies.get('slogan')
if (value === 'ren-sir666') {
ctx.body = {
data: []
}
} else {
ctx.body = {
message: '没有访问权限,请先登录~~'
}
}
})
app.use(userRouter.routes())
app.use(userRouter.allowedMethods())
app.listen(3000)
# 5、Session 认证 🎈
由于 Cookie 很容易被伪造,我们可以采用 Cookie + SessionId 的方式进行安全升级。
代码示例
const koa = require('koa')
const { bodyParser } = require('@koa/bodyparser')
const Router = require('@koa/router')
const koaSession = require('koa-session')
const app = new koa()
// ---------------------
// session中间件
app.keys = ['your-secret-key'] // 密钥数组
const session = koaSession(
{
key: 'sessionId', // Cookie名称
maxAge: 10 * 1000, // 10秒
signed: true // 加密(需要密钥数组,默认会自动多一个sessionId.sig的Cookie)
},
app
)
app.use(session)
// ---------------------
app.use(bodyParser())
const userRouter = new Router({ prefix: '/user' })
userRouter.get('/login', (ctx, next) => {
// ctx.cookies.set('slogan', 'ren-sir666', {
// maxAge: 10 * 1000 // 10秒
// })
ctx.session.slogan = 'ren-sir666'
ctx.body = '登录成功!!!'
})
userRouter.get('/cartList', (ctx, next) => {
// const value = ctx.cookies.get('slogan')
const value = ctx.session.slogan
if (value === 'ren-sir666') {
ctx.body = {
data: []
}
} else {
ctx.body = {
message: '没有访问权限,请先登录~~'
}
}
})
app.use(userRouter.routes())
app.use(userRouter.allowedMethods())
app.listen(3000)
express 中:express-session (opens new window)
代码示例
const expressSession = require('express-session')
// ---------------------
// session中间件
const session = expressSession(
name:'sessionId',
secret: 'your-secret-key',
cookie: {
maxAge:1000*60*60 //过期时间
}
)
app.use(session)
// ---------------------
# 二、token 令牌
# 为什么?
随着互联网的发展,Cookie + SessionId 的身份认证方式的缺点开始暴露了出来:
- Cookie 附加在 HTTP 上,不仅有大小限制(4KB),还会增加网站流量
- 经过使用了 SessionId 进行加密,但 Cookie 本身仍然还是明文传递,安全隐患一直存在
跨站请求伪造
登录 a 网站成功后会自动携带 cookie,在没有退出登录的情况下关闭网站,此时 cookie 仍然有效;若 b 网站此时挂载了一个携带 cookie 的 a 网站的链接,那 😢……
再者,对于浏览器外的
比如:微信小程序
还有,伴随着
难题:如何让在 用户系统(kao-session)中生成的 SessionId,在其他在其他系统中进行认证?
解决方案

那有什么其他好的方案吗?
# 1、token 认知
在现今 前后端分离开发 模式盛行的时代,通过前后端间的 token 令牌交互,可以非常方便的解决上述问题。
方案如下:
我们可以通过 非对称加密(私钥和公钥) 的方式,只让 用户系统 可以通过私钥颁发 token 令牌,而 其他系统 则通过 公钥来验证 token 令牌即可。
token 可以翻译为令牌,是用户系统为用户颁发的一个令牌,该令牌可以作为用户访问一些接口和资源的凭证。
# 2、JWT 认证
既然可以通过生成 token 来解决 身份认证问题,那怎么实现这个方案呢?token 又如何生成如何组成呢? —— JSON Web Token (opens new window)
JWT 的 Token 机制
# 3、token 生成 🎉
我们可以通过官网查看 token 生成情况:
https://jwt.io/
代码实现
- 编写一个 base64url 编码函数
function base64url(source) {
// Encode in classical base64
encodedSource = CryptoJS.enc.Base64.stringify(source)
// Remove padding equal characters
encodedSource = encodedSource.replace(/=+$/, '')
// Replace characters according to base64url specifications
encodedSource = encodedSource.replace(/\+/g, '-')
encodedSource = encodedSource.replace(/\//g, '_')
return encodedSource
}
- 使用 CryptoJS (opens new window) 库 进行 base64 编码
var header = {
alg: 'HS256',
typ: 'JWT'
}
var stringifiedHeader = CryptoJS.enc.Utf8.parse(JSON.stringify(header))
var encodedHeader = base64url(stringifiedHeader)
var data = {
iss: 'techmaster',
exp: 1426420800,
'https://www.techmaster.vn/jwt_claims/is_admin': true,
user: 'paduvi',
awesome: true
}
var stringifiedData = CryptoJS.enc.Utf8.parse(JSON.stringify(data))
var encodedData = base64url(stringifiedData)
var token = encodedHeader + '.' + encodedData
var secret = 'My very confidential secret!'
var signature = CryptoJS.HmacSHA256(token, secret)
signature = base64url(signature)
var signedToken = token + '.' + signature

# 4、JWT 字符串
JWT 字符串通常由三部分组成(每个部分都是经过 base64 编码的字符串):
- header(头部:头部的信息)
const header = {
alg: 'HS256', // 采用的加密算法(默认为对称的 HMAC SHA256 算法)
typ: 'JWT' // 固定值 JWT
}
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
- payload(负载:携带的数据)
const payload = {
id: 1, // 用户ID
name: 'admin', // 用户名
iat: Math.floor(Date.now() / 1000), // 令牌的生成时间(当前时间) ---> 默认携带
exp: Math.floor(Date.now() / 1000) + 3600 // 令牌的过期时间(1小时后)
}
eyJpZCI6MSwibmFtZSI6ImFkbWluIiwiaWF0IjoxNjk3NjM5Mjg5LCJleHAiOjE2OTc2NDI5MDR9
- signature(签名:)
设置一个'your-secret-key',然后通过算法将 Header + Payload 组合在一起
# 在对称加密算法中,secretKey非常重要,不能被泄露
HS256((base64Url(header) + . + base64Url(payload)), secretKey)
UCMjxjD-QcHut9jXHd5PZ1xnP6IbOsIxgFx2TCsw2CE
综上,完整的 JWT 字符串为:
// 颁发
const token =
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJpZCI6MSwibmFtZSI6ImFkbWluIiwiaWF0IjoxNjk3NjM5Mjg5LCJleHAiOjE2OTc2NDI5MDR9.UCMjxjD -
QcHut9jXHd5PZ1xnP6IbOsIxgFx2TCsw2CE
// 认证
// Authorization: Bearer <token>
# 5、开发应用 🎈
关于 token 的生成,在 Node.js 环境中我们通常会使用 jsonwebtoken (opens new window) 库
使用对称加密算法(HS256)
const koa = require('koa')
const { bodyParser } = require('@koa/bodyparser')
const Router = require('@koa/router')
const jwt = require('jsonwebtoken')
const app = new koa()
app.use(bodyParser())
const userRouter = new Router({ prefix: '/user' })
// ---------------------
const secretkey = 'your-secret-key'
// ---------------------
userRouter.get('/login', (ctx, next) => {
const payload = { id: 1, name: 'admin' }
const token = jwt.sign(payload, secretkey, {
expiresIn: 20 // 20 表示'20s';'20'表示'20ms';其他简写表示:'2h'、'2d'
})
ctx.body = {
code: 0,
token,
message: '登录成功!!!'
}
})
userRouter.get('/cartList', (ctx, next) => {
const authorization = ctx.header.authorization
try {
// jwt验证异常检测
const token = authorization.replace('Bearer ', '')
const payload = jwt.verify(token, secretkey)
console.log(payload) // { id: 1, name: 'admin', iat: 1697686450, exp: 1697686470 }
ctx.body = {
code: 0,
data: []
}
} catch (error) {
ctx.body = {
code: -1010,
message: 'token过期或者无效的token,请重新登录~~'
}
}
})
app.use(userRouter.routes())
app.use(userRouter.allowedMethods())
app.listen(3000)
由于默认采用的是 HS256 这个对称算法,所以一旦某个系统被攻破,那黑客就可以使用该系统进行令牌颁发,进而简单攻陷其他系统。
为了提升安全性,我们可以指定一种非对称加密算法,这样就可以只让某个系统(用户系统)来进行 token 令牌 的颁发,而其他系统只进行 token 令牌的验证即可。
这样,我们只需要重点维护 用户系统 的安全性即可,大大减少工作量。
OpenSSL 工具生成密钥对
我们可以直接在 git bash 中使用 openssl:
# 1、生成rsa私钥
openssl genrsa -out app_private_key.pem 2048
# 2、生成rsa公钥
openssl rsa -in app_private_key.pem -pubout -out app_public_key.pem
使用非对称加密算法(RS256)
const koa = require('koa')
const { bodyParser } = require('@koa/bodyparser')
const Router = require('@koa/router')
const jwt = require('jsonwebtoken')
const { readFileSync } = require('node:fs')
const app = new koa()
app.use(bodyParser())
const userRouter = new Router({ prefix: '/user' })
// ---------------------
const private_key_buffter = readFileSync('./key/app_private_key.pem')
const public_key_buffter = readFileSync('./key/app_public_key.pem')
// ---------------------
userRouter.get('/login', (ctx, next) => {
const payload = { id: 1, name: 'admin' }
const token = jwt.sign(payload, private_key_buffter, {
expiresIn: 20, // 20 表示'20s';'20'表示'20ms';其他简写表示:'2h'、'2d'
algorithm: 'RS256'
})
ctx.body = {
code: 0,
token,
message: '登录成功!!!'
}
})
userRouter.get('/cartList', (ctx, next) => {
const authorization = ctx.header.authorization
try {
// jwt验证异常检测
const token = authorization.replace('Bearer ', '')
const payload = jwt.verify(token, public_key_buffter, {
algorithms: ['RS256']
})
console.log(payload) // { id: 1, name: 'admin', iat: 1697686450, exp: 1697686470 }
ctx.body = {
code: 0,
data: []
}
} catch (error) {
ctx.body = {
code: -1010,
message: 'token过期或者无效的token,请重新登录~~'
}
}
})
app.use(userRouter.routes())
app.use(userRouter.allowedMethods())
app.listen(3000)
# 接口测试工具:
编写接口时,我们往往会使用一些工具进行接口测试,频繁的对 token 进行复制粘贴是非常不方便的。我们可以使用这些工具自带的一些功能简化操作:
- Apifox 中
我们可以为 login 接口添加 后置操作 将返回的 token 存储在 全局变量中,在后期需要的地方使用 该变量即可。
提示
在 apifox 中,我们也可以通过全局(根目录)、分组(分组设置)、接口(文档编辑页)的 Auth 设置同时为多个接口设置 token。

进一步的,如果 token 过期,我们还是需要手动重新 发起 login 请求,如果不想手动执行该操作,我们可以:
- postman 中
