# 无状态协议

  由于 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 的方式进行安全升级。

koa 中: koa-session (opens new window)

代码示例
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 网站的链接,那 😢……

  再者,对于浏览器外的客户端(如:iOS、Android),Cookie 和 Session 是不会自动携带在 HTTP 请求上 和 自动保存在客户端的,必须要我们手动进行保存和携带。

比如:微信小程序

  还有,伴随着服务器端的发展(高并发需求),在进行分布式系统和服务器集群中,各个系统间如何正确的解析/共享 Session 也是一个问题。

难题:如何让在 用户系统(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
}
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 编码的字符串):

参考 🎈:Khái niệm về JSON Web Token (opens new window)

  • 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 中

快速入门 (opens new window)

  我们可以为 login 接口添加 后置操作 将返回的 token 存储在 全局变量中,在后期需要的地方使用 该变量即可。

提示

  在 apifox 中,我们也可以通过全局(根目录)、分组(分组设置)、接口(文档编辑页)的 Auth 设置同时为多个接口设置 token。

官方介绍:Apifox 发请求时如何自动获取 Cookie 和 token (opens new window)

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

官方介绍:登录态(Auth)如何处理 (opens new window)

  • postman 中
更新于 : 8/7/2024, 2:16:31 PM