Skip to content

JWT(JSON Web Token)

目前最流行的跨域身份验证解决方案。JWT 解决了 session 不支持分布式架构,无法支持横向扩展的问题,服务器不保存任何会话数据,只需要保存密钥即可。

JWT 组成

  • header

    固定的内容,alg 表示算法,typ 表示类型。

    javascript
    {
      alg: 'HS256', 
      typ: 'JWT' 
    }
  • payload

    • iss 签发人

    • exp 过期时间

    • sub 主题

    • aud 受众

    • nbf 生效时间

    • iat 签发时间

    • jti 编号

  • Signature

使用方式

  • 请求头,Bearer 表示 token 类型,是一个约定,可以省略。

    Authorization: Bearer <token>

  • url

    http://www.foo.com?token=bar

    有些场合 token 可能会放到 url 中,base64 中 + / = 这三个符号在 url 中有特殊含义,+ 替换成 -,/ 替换成 _,结尾的 = 被省略,这就是 base64URL 编码,所以解决办法就是对 payload 进行编码时使用 base64URL,而不是使用 base64,如果你使用 base64,需要替换所有+ -> -,/ -> _,移除结尾的=,解码时先把-替换回+,_替换回/,根据需要补全结尾的=,使长度为4的倍数,再用标准base64解码

  • 如果是 POST 请求也可以放到请求体中。

流程

  • 如果登录成功,基于 JWT 的算法创建一个 Token 并返回给客户端,客户端通过 localStorage 存储。

  • 之后的请求头中把 Token 带上,服务器根据 header 和 payload 重新签名判断是否被 修改,如果没有被修改再解析 token 得到 payload,和 payload 中的 expirationTime 对比 判断是否过期,过期让用户重新登录或者再给一个新的 token。

实现

js
const { createHmac, timingSafeEqual } = require('crypto')

const toBase64URL = s => Buffer.from(JSON.stringify(s)).toString('base64url')

const sign = (base64URL, secret) =>
  createHmac('sha256', secret).update(base64URL).digest('base64url')

const jwtEncoder = (payload, secret, header = { typ: 'JWT', alg: 'HS256' }) => {
  const base64URL = `${toBase64URL(header)}.${toBase64URL(payload)}`
  const signature = sign(base64URL, secret)

  return `${base64URL}.${signature}`
}

const jwtDecoder = (token, secret) => {
  const segments = token.split('.')

  if (segments.length !== 3) {
    return null
  }

  const [header, payload, signature] = segments
  const _signature = sign(`${header}.${payload}`, secret)
  const signBuffer = Buffer.from(signature)
  const _signBuffer = Buffer.from(_signature)

  if (
    signBuffer.length === _signBuffer.length &&
    // 避免 timing attack
    timingSafeEqual(signBuffer, _signBuffer)
  ) {
    const _payload = JSON.parse(Buffer.from(payload, 'base64url').toString())

    return _payload.exp > Date.now() ? _payload : null
  }

  return null
}

module.exports = {
  jwtEncoder,
  jwtDecoder
}
js
// A month
const time = 1000 * 60 * 60 * 24 * 30
const secret = 'yourSecret'

const token = jwtEncoder(
  { foo: 'foo', bar: 'bar', exp: Date.now() + time },
  secret
)
console.log(token)

const res = jwtDecoder(token, secret)
console.log(res)

双 token

双 Token 机制是一种将身份验证与权限管理分离的现代鉴权方案,它通过使用两个不同功能的令牌来提升安全性和用户体验。该机制通常包含两个令牌:1.访问令牌‌:用于日常请求API,有效期较短(如15分钟至几小时)。2.刷新令牌‌:用于在访问令牌过期后获取新的访问令牌,有效期较长(如7天至30天)。用户登录成功后,服务端同时生成并返回访问令牌和刷新令牌。用户发送 API 请求时:客户端在请求头中携带访问令牌访问。当访问令牌过期时,客户端使用刷新令牌向服务端请求新的访问令牌(和可选的新的刷新令牌),用户无需重新登录。用户退出登录时时,客户端清除本地访问令牌,同时服务端将对应的刷新令牌从 redis 中删除,如果访问令牌没过期,将访问令牌加入 redis 中的访问令牌黑名单中,并设置和访问令牌相同的剩余过期时间

优点

由于刷新令牌存储在 Redis 中,泄露风险极低,访问令牌中只需要存储用户标识,不再需要存储用户的基本信息,且访问令牌有效期短,泄露后,攻击窗口也很有限。