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)