eggjs实现jwt的token鉴权

对于网页来说,用户鉴权及持久化登录是几个常规需求,之前使用php时采用的是session方案来解决,本次是学习egg.js框架,开始也是使用的session方案,但session方案对于跨域等环境支持困难,同时在服务器端大量存储用户session数据也会加重服务器负担(特别是像本人这样使用配置极低的小鸡的用户,内存、硬盘日常使用都很捉襟见肘)。基于以上原因,网上搜了下,最终采用了jwt实现token鉴权。

一、基于token的鉴权机制

基于token的鉴权机制是无状态的,它不会在服务端去保留用户的认证信息或者会话信息。用户会话信息保留在用户端,且一般服务器端不会考虑用户从哪里登录的。流程上是这样的:

  • 用户使用用户名密码来请求服务器
  • 服务器进行验证用户的信息
  • 服务器通过验证发送给用户一个token
  • 客户端存储token,并在每次请求时附送上这个token值
  • 服务端验证token值,并返回数据

这个token必须要在每次请求时传递给服务端(这个可以在鉴权中间件决定是否都需要,可以通过设置实现某些路由不需要鉴权也就不需要token),它一般在请求头(Request Headers)里,而客户端本地一般保存在localStorage里,以持久化存储。

二、jwt

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

JSON Web Token由三部分组成,它们之间用圆点(.)连接。这三部分分别是:

  • Header
  • Payload
  • Signature

一个典型的JWT是这个样子的:

xxxxx.yyyyy.zzzzz

1、Header :header典型的由两部分组成:token的类型(“JWT”)和算法名称(比如:HMAC SHA256或者RSA等等)。

例如:

{
    'alg': "HS256",
    'typ': "JWT"
}

然后,用Base64对这个JSON编码就得到JWT的第一部分

2、Payload:第二部分是payload,它包含声明(要求)。声明是关于实体(通常是用户)和其他数据的声明。声明有三种类型: registered, public 和 private。

Registered claims : 这里有一组预定义的声明,它们不是强制的,但是推荐。比如:iss (issuer), exp (expiration time), sub (subject), aud (audience)等。

Public claims : 可以随意定义。

Private claims : 用于在同意使用它们的各方之间共享信息,并且不是注册的或公开的声明。 下面是一个例子:

{
    "sub": '1234567890',
    "name": 'john',
    "admin":true
}

对payload进行Base64编码就得到JWT的第二部分

注意,不要在JWT的payload或header中放置敏感信息,除非它们是加密的。

3、Signature:签名部分

为了得到签名部分,你必须有编码过的header、编码过的payload、一个秘钥,签名算法是header中指定的那个,然对它们签名即可。

例如: HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。

三、egg.js配置jwt

1、安装和启用jwt插件

egg.js可以使用egg-jwt,一个jwt的egg实现。首先是安装:

npm i --save egg-jwt

在配置中启用jwt插件(路径是config/plugin.js),在插件配置中添加橙色内容(示例是使用eggjs脚手架自动生成的文件的格式):

// config/plugin.js
module.exports = {
  jwt: {
    enable: true,
    package: 'egg-jwt',
  },
// 其他内容
};

2、jwt配置

  • jwt密钥配置:在egg.js配置文件中配置jwt密钥(密钥自己设定,主要保密)
// config/config.default.js
 config.jwt = {
    secret: 'xxxxxxxxxxxxx',
  };

四、jwt鉴权中间件

这里是通过创建一个egg鉴权中间件来实现token鉴权。

1、鉴权中间件

根据egg.js官方教程方法创建1个jwt鉴权中间件 jwtauth: app/middleware/jwtauth.js,这样所有请求都会通过中间件进行鉴权,如果没通过就不会进行后续操作,示例如下:

//  app\middleware\jwtauth.js
'use strict';
// 全局jwt鉴权中间件
module.exports = (optinons, app) => {
  return async function(ctx, next) {
    // 当前路由
    const url = ctx.url;
    // 在配置中获取设置的路由验证白名单
    const routerAuthWhiteList = app.config.routerAuthWhiteList;
    if (routerAuthWhiteList.includes(url)) {
      await next();
    } else {
      // 获取请求携带的token(前端约定在authorization头携带了"Bearer "前缀)
      const token = ctx.headers.authorization ? ctx.headers.authorization.substring(7) : '';
      let decode;
      // 解析、验证token:
      try {
        decode = await app.jwt.verify(token, app.config.jwt.secret);
        ctx.state.userinfo = decode;   //这样可以在控制器里通过ctx.state.userinfo访问到token中保存的用户信息
        await next();
      } catch (error) {
        ctx.helper.fail(  //这是一个在helper中一个自定义的方法
          ctx,
          {
            code: 999,
            msg: '失败:token未通过鉴权验证',
            data: { error,token: ctx.headers.authorization,decode}
            
          }
        );
      }
    }
  };

该示例中,前端发送的Request的请求头的authorization头内携带了“Bearer ”+jwtToken。需要在验证时去掉"Bearer "前缀,然后使用jwt.verify函数进行鉴权验证。其中使用的helper.fail方法是自定义的helper方法,比如:

// app/extend/helper.js
'use strict';
module.exports = {
  success(ctx, msg, data) {
    ctx.body = {
      code: 200,
      msg,
      data,
    };
    ctx.status = 200;
  },
  fail(ctx, res) {
    ctx.body = {      
      code: res.code,
      msg: res.msg,
      data: res.data,
    };
  },
  // 数字不足5位补0;
  prefixzero(num, n = 5) {
    return (Array(n).join(0) + num).slice(-n);
  },

};

2、启用中间件

在配置文件中启用刚创建的中间件,在config/config.default.js中间添加如下内容,如果已经有其它中间件,则在数组中添加jwtauth插件,而不是新增一行,注意中间顺序:

// config/config.default.js
config.middleware = [ 'jwtauth' ];

3、鉴权白名单

在创建的中间件里,可以看到可设置白名单来实现部分路由无需进行鉴权,比如登录路由,由于尚未获得token,所以不需要进行鉴权:

// config/config.default.js
config.routerAuthWhiteList = [ '/user/login', '/re' ];

五、jwt鉴权运用

1、后端token生成

为了复用生成token过程,并且便于管理配置,这里创建了一个service来提供创建token的过程。注意这里设置了token的有效期为2天,但为了避免正常操作过程中token突然失效了导致操作没有有效保存,而且又要登录,所以通过一定方法尽量把token过期时间控制在凌晨2点左右,这个时间应该没人使用(根据实际情况来确定)。其中expiresIn就是在多长时间后失效,如果不加单位比如h的话,默认单位是毫秒。

// app/service/jwttoken.js
'use strict';
const Service = require('egg').Service;
class JwttokenService extends Service {
  async gettoken(data, opt = {}) {
    const hours = (new Date()).getHours();
    // token基础有效时间是2天=48h(实际为48h-72h),并尽量把token过期时间控制在凌晨2点左右:
    const expiresIn = (48 + 24 - hours + 2) + 'h';
    const secret = this.app.config.jwt.secret;
    const token = await this.app.jwt.sign(
      data,
      secret,
      {
        expiresIn,
        ...opt,
      }
    );
    return token;
  }
}
module.exports = JwttokenService;

然后在controller中可以通过使用该服务生成jwt的Token:

// app/controller/xxx.js 
const token = await ctx.service.jwttoken.gettoken(
   {
     // 自定义数据部分
    });
 ctx.helper.success(ctx, '登录成功', { token, });

2、前端使用

前端使用时就是在登录时应将获得的token进行持久化存储:

localStorage.setItem('token', res.data.data.token);

然后在其它请求中在请求头中添加authorization请求头,注意与后端格式约定好,比如是否携带"Bearer "前缀。为了减少重复代码,可以在实例化请求,并且将t添加token请求头写在请求拦截器中,比如如果是axios可以参考这篇文章的第三部分:前端axios+eggjs(后端)进行网络请求

--------------------------------

除非注明,否则均为清风揽月阁原创文章,转载应以链接形式标明本文链接

本文链接:https://www.iimm.ink/287.html

发表评论

滚动至顶部