文档
安全
JWT

JWT

JWT是什么

JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。

什么时候你应该用JWT

  • Authorization (授权) : 这是使用JWT的最常见场景。一旦用户登录,后续每个请求都将包含JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是现在广泛使用的JWT的一个特性,因为它的开销很小,并且可以轻松地跨域使用。

  • Information Exchange (信息交换) : 对于安全的在各方之间传输信息而言,JSON Web Tokens无疑是一种很好的方式。因为JWT可以被签名,例如,用公钥/私钥对,你可以确定发送人就是它们所说的那个人。另外,由于签名是使用头和有效负载计算的,您还可以验证内容没有被篡改。

使用

  1. 安装依赖
yarn add yarn add @yunflyjs/yunfly-plugin-fast-jwt
  1. config/config.plugin.ts 中声明插件
config/config.plugin.ts
/**
 * yunfly 插件
 * 数组顺序就是插件的加载顺序
 */
const plugins: {[key:string]: string}[] = [
  {
    name: 'jwt',
    package: '@yunflyjs/yunfly-plugin-fast-jwt'
  }
];
// 
export default plugins;
  1. config/config.default.ts 中启用插件 config.jwt
src/config/config.default.ts
config.jwt = {
  enable: true,
  expiredPassThrough: false,
  secret:'YUNFLYJS_JWT_TOKEN_DEMO',
  expire: '1h',
  token: { 
    type: 'cookie', 
    key: 'Authorization', 
    httpOnly: true, 
    domain:'127.0.0.1', 
    path:'/',
    // 20分钟后过期
    expires: () => { return new Date(Date.now() + 60 * 1000 * 20) }, 
  },
  // token: { type:'header', key: 'Authorization' },
  rsSign: {
    enable: false,
    interval: 15,
  },
  unless: ['/favicon.ico'],
  global: true,
  passThrough: false,
}
  • jwt.config 还可以是一个函数
src/config/config.default.ts
type JWTConfig = JWTOptions | ((ctx: Context) => JWTOptions)
 
config.jwt = (ctx: Context) => {
  return {
    nable: true,
    expiredPassThrough: false,
    secret:'YUNFLYJS_JWT_TOKEN_DEMO',
    expire: '1h',
    ......
  }
}

配置说明

参数类型默认值必填说明
enablebooleantrue是否启用 JWT 校验
secretstringYUNFLYJS_JWT_TOKEN_DEMOJWT 盐值
expiredPassThroughbooleanfalseJWT过期时是自动重签还是向外抛出过期错误。自动重签会一直不过期,抛出过期错误需要自定义错误处理逻辑
expirestring | number3h过期时间 Eg: 60, "2 days", "10h", "7d". ("120" is equal to "120ms").
tokenJWTTokenOptions{ type:'header', key: 'Authorization' }token配置项 (重要参数,请关注下面说明。)
rsSignRsSignOptions{ enable: false, interval: 15 }自动续签,interval单位为分,默认为15分钟。若开启,当判断过期时间是否快超过interval分钟, 如果快超过就重新生成JWT秘钥。若expire小于等于interval则不处理
globalbooleantrue是否全局启用 JWT 校验 (重要参数,请关注下面说明。)
unlessstring[]['/favicon.ico']{ gobbal:true }时,此参数可用,需要排除 JWT 验证的接口
passThroughbooleanfalse{ gobbal:true }时,此参数可用,校验不通过时是否继续执行

token 参数说明

token 表示 JWT 参数传递方式,目前有两种方式进行传递

  • 1、 通过 header 头进行校验
  • 2、 通过 cookie 进行校验
参数类型必填说明
type`headercookie`
keystringJWT 校验的 key 值
httpOnlybooleantype:cookie 时有效
domainbooleantype:cookie 时有效
pathstring[]type:cookie 时有效
maxAgenumbertype:cookie 时有效, 一个数字, 表示从 Date.now() 得到的毫秒数
expiresDate | ()=>Datetype:cookie 时有效, 一个 Date 对象, 表示 cookie 的到期日期 (默认情况下在会话结束时过期) 或者[函数,返回一个Data对象]
securebooleantype:cookie 时有效, 一个布尔值, 表示 cookie 是否仅通过 HTTPS 发送 (HTTP 下默认为 false, HTTPS 下默认为 true)
sameSiteboolean | stringtype:cookie 时有效, 一个布尔值或字符串, 表示该 cookie 是否为 "相同站点" cookie (默认为 false). 可以设置为 'strict', 'lax', 'none', 或 true (映射为 'strict')
signedbooleantype:cookie 时有效, 一个布尔值, 表示是否要对 cookie 进行签名 (默认为 false). 如果为 true, 则还会发送另一个后缀为 .sig 的同名 cookie, 使用一个 27-byte url-safe base64 SHA1 值来表示针对第一个 Keygrip 键的 cookie-name=cookie-value 的哈希值. 此签名密钥用于检测下次接收 cookie 时的篡改
overwritebooleantype:cookie 时有效, 一个布尔值, 表示是否覆盖以前设置的同名的 cookie (默认是 false). 如果是 true, 在同一个请求中设置相同名称的所有 Cookie(无论路径或域)是否在设置此Cookie 时从 Set-Cookie 消息头中过滤掉

global 参数说明

golbal 是一个比较重要的参数,参数类型为 boolean

  • global = trueglobal值为true时,则表示当前node服务所有controller接口都进行jwt校验global参数通常跟 unless一起配合使用。unless参数表示需要排除jwt校验的controller接口。

  • global = falseglobal值为false时, 则表示不进行全局使用,此时我们就需要手动进行引入使用。

  • 手动使用

src/controller/JWTController.ts
import { Post, UseBefore } from "@yunflyjs/yunfly";
import { JWTMiddleware, JWTPassThroughMiddleware } from '@yunflyjs/yunfly/build/middleware/JWTMiddleware'
 
@JsonController('/user')
@UseBefore(JWTMiddleware)  //controller手动使用jwt
export default class JWTController {
  @Inject()  private userService: UserService;
  // 
  @Post('/get-user-info')
  @UseBefore(JWTMiddleware) //具体方法手动使用jwt
  async getUserMsg()Promise<any> {
    return {}
  }
}

passThrough 参数说明

  • passThrough = false

当 JWT 校验不通过时,直接抛出错误,并阻止后面的程序运行。

  • 等同于手动方式的 JWTMiddleware
src/controller/JWTController.ts
import { Post, UseBefore } from "@yunflyjs/yunfly";
import { JWTMiddleware } from '@yunflyjs/yunfly/build/middleware/JWTMiddleware'
@JsonController('/user')
@UseBefore(JWTMiddleware)  //controller手动使用jwt
export default class JWTController {
  // 
  @Post('/get-user-info')
  @UseBefore(JWTMiddleware) //具体方法手动使用jwt
  async getUserMsg()Promise<any> {
    return {}
  }
}
  • passThrough = true

当 JWT 校验不通过时,继续执行后面的逻辑, 不阻塞流程。

  • 等同于手动方式的 JWTPassThroughMiddleware
src/controller/JWTController.ts
import { Post, UseBefore } from "@yunflyjs/yunfly";
import { JWTPassThroughMiddleware } from '@yunflyjs/yunfly/build/middleware/JWTMiddleware'
@JsonController('/user')
@UseBefore(JWTPassThroughMiddleware)  //controller手动使用jwt
export default class JWTController {
  // 
  @Post('/get-user-info')
  @UseBefore(JWTPassThroughMiddleware) //具体方法手动使用jwt
  async getUserMsg()Promise<any> {
    return {}
  }
}

JWT暴露接口说明

  • JWT暴露出如下的一些方法 generateToken,verifyToken,decodeToken,injectToken,JWTMiddleware,JWTPassThroughMiddleware
方法名说明
generateToken根据自定义参数生成jwt秘钥, 例如用户登录时生成秘钥操作
verifyToken校验jwt秘钥是否有效,自带过期重签功能
decodeToken解码jwt秘钥,不校验秘钥是否过期,此方法一般不用(慎用)
injectToken生成的jwt秘钥如何处理,可以设置到header头或者cookie中,具体以config.jwt.token 配置项为准
JWTMiddleware(config.jwt.global=false) 时使用,手动使用JWT中间件方式,当JWT校验失败是直接抛出错误信息,并中断执行
JWTPassThroughMiddleware(config.jwt.global=true) 时使用,手动使用JWT中间件方式,当JWT校验失败时继续执行
  • 使用
src/controller/JWTController.ts
import { JsonController, Get } from "@yunflyjs/yunfly";
import { generateToken, verifyToken } from '@yunflyjs/yunfly/build/middleware/JWTMiddleware'
//  
@JsonController('/jwt')
export default class JWTController {
  @Get('/gen-token')
  async genToken(
    @Ctx() ctx:Context,
  ): Promise<AnyOptions> {
    const token = generateToken({
      ctx,
      data : {
        name:'zane',
        age: 25,
        hobby:['游泳','爬山']
      },
    })
    return { token }
  }
  //
  @Get('/verify-token')
  async verifyToken(
    @Ctx() ctx: Context,
  ): Promise<AnyOptions> {
    return await verifyToken({ ctx })
  }
}
// verify-token 返回值
// {
//   name:'zane',
//   age: 25,
//   hobby:['游泳','爬山']
// }

通过 CurrentUser 获得 JWT 数据

src/controller/JWTController.ts
import { JsonController, Get, CurrentUser, UseBefore } from "@yunflyjs/yunfly";
//  
@JsonController('/jwt')
export default class JWTController {
  // 
  @Get('/get-jwt-data')
  async getJWTdata(
    @CurrentUser() user: AnyOptions,
  ): Promise<AnyOptions> {
    const { name, age, hobby } = user || {}
    return { name, age, hobby }
  }
}
// 解码返回值
// {
//   name:'zane',
//   age: 25,
//   hobby:['游泳','爬山']
// }

JWT重签机制 expiredPassThrough

  • 值为 true, 当 JWT 过期时,会自动重签,也就是说 JWT 不会过期,JWT 仅用来做参数校验。
  • 值为 false, 当 JWT 过期时,会向外抛出 401 错误,此时 JWT 可用来做 登录态校验 + 参数校验。

自动重签规则

为了保证服务的正常,当jwt过期时,会拿着过期的密钥进行数据解码,不会阻断当前的request请求(对客户端无影响),但JWT内部会进行过期日志打印和token重签。

  • 使用新的token
  • JWT 当前支持 header头cookie 两种方式
  • 当我们的处理方式为cookie类型时,过期会自动重签并设置新的cookie信息(无需做额外处理)。
  • 当我们的处理方式为header类型时,过期重签需要进行手动处理。

当 JWT 需要签名或者重签时,请求的Header头会返回一个特殊的重签头标识: Set-${key}, 默认标识为Set-Authorization(此处标识取决于你config.jwt.token.key字段),因此我们需要在客户端统一拦截接口的Header返回头标识,当发现有Set-${key}返回头标识时,取值并覆盖客户端过期的token即可

  • 下面以axios为例

全局拦截

// ---------------------全局拦截---------------------
// 新增一个全局拦截器
axios.interceptors.response.use(function (response) {
  // 如果返回token为真时,重新替换本地token
  const authorization = response.headers['set-authorization'];
  if(authorization) {
    sessionStorage.setItem('token',authorization);
  }
  return response;
}, function (error) {
  return Promise.reject(error);
});
 

单接口拦截

// ----------------------单个接口处理---------------------
axios({
  method: 'post',
  url: 'http://xxx.com/api/xxx',
  data: {},
  headers: {'Authorization': 'xxxxxx'},
}).then((response)=>{
  // 如果返回token为真时,重新替换本地token
  const authorization = response.headers['set-authorization'];
  if(authorization) {
    sessionStorage.setItem('token',authorization);
  }
})
  • 附加: 如果BFF使用cors则需要配置允许的header头字段 exposeHeaders(允许返回的header头信息);
config.security = { 
    cors: {
        // 运行返回头
        exposeHeaders: ['Set-Authorization'],
    },
}