ThinkJS
首发于ThinkJS
ThinkJS JWT 鉴权实践

ThinkJS JWT 鉴权实践

编者注:我相信鉴权应该是大部分 Web 服务必备的基础功能之一。实现权限验证的方式有很多种,其中 JSON Web Token(即JWT)这种使用 Token 验证的方式受到了越来越多开发者的喜爱。其相对于传统的验证方式来说会更为安全一点,而且相对而言由于加密串中就包含了权限信息,所以不需要额外的数据库查询。今天我们请来了 ThinkJS 的开发人员卢士杰同学给我们实战讲解下载 ThinkJS 中如何使用 JWT 权限验证服务。


JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。它提供基于JSON 格式的 Token 来做安全认证。

JWT 组成


JWT 由三部分组成,分别是 header(头部),payload(载荷),signature(签证) 这三部分以小数点连接起来。

本例中使用名为jwt-token的cookie来存储JWT例如:

jwt-token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoibHVzaGlqaWUiLCJpYXQiOjE1MzI1OTUyNTUsImV4cCI6MTUzMjU5NTI3MH0.WZ9_poToN9llFFUfkswcpTljRDjF4JfZcmqYS0JcKO8;

其中:

\begin{array}{c|l}\text{部分} & \text{值} \\ \hline header & eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\\ payload & eyJuYW1lIjoibHVzaGlqaWUiLCJpYXQiOjE1MzI1OTUyNTUsImV4cCI6MTUzMjU5NTI3MH0\\ signature & WZ9_poToN9llFFUfkswcpTljRDjF4JfZcmqYS0JcKO8 \\ \end{array}\\

header

header 是对类型和哈希算法进行base64Encode之后得到的。对于比例中的header进行base64Decode可以得到:

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

payload

payload 是对我们需要传输的信息进行base64Encode之后得到的。对于本例中的payload进行base64Decode可以得到:

{
  "name":"lushijie”,
  "iat":1532595255, // JWT 发布的时间
  "exp”:1532595270 // JWT 过期的时间,15秒后过期
}

本例中的iat, exp 是 koa-jwt 中的默认字段,除此之外 JWT 标准中注册的非强制使用的声明还有 jti,iss等,有兴趣的小伙伴可以查看更多的相关标准。 由于 payload 可以在客户端解码获得,所以不建议在 payload 中存放敏感信息,例如用户的密码。

signature

signature 包含了 header,payload 和 密钥,计算公式如下:

const encodedString = base64Encode(header) + "." + base64Encode(payload);
let signature = HMACSHA256(encodedString, '密钥');

这里密钥是保存在服务端的,客户端是不知道的。

JWT 验证

对于验证一个 JWT 是否有效也是比较简单的,服务端根据前面介绍的计算方法计算出 signature,和要校验的JWT中的 signature 部分进行对比就可以了,如果 signature 部分相等则是一个有效的 JWT。

JWT 在 ThinkJS 中的实践

下面我们在 ThinkJS 中实现使用 JWT 实现只有在登录后才能访问一个接口。 ThinkJS 兼容 koa2 的所有middleware,那就找个现成的 jwt 插件吧,这里我们使用 koa-jwt 插件。koa-jwt 代码没有几行,大家可以稍微读一下,简单易懂,接下来我们开始使用它~ 首先我们要在 ThinkJS 中配置 koa-jwt:

公共配置

/src/config/config.js:

module.exports = {
    // ...
    jwt: {
      secret: 'lushijie-password',
      cookie: 'jwt-token',
      expire: 30 // 秒
    },
  }

因为这三个参数在不同的位置会用到,为了统一管理我们提取到了公共的 config 中。

中间件配置

/src/config/middleware.js:

const jwt = require('koa-jwt');
const isDev = think.env === 'development';

module.exports = [
  // ...
  {
    handle: jwt,
    // match(ctx) {
      // return !/^\/index\/login/.test(ctx.path);
    // },
    options: {
      cookie: think.config('jwt')['cookie'],
      secret: think.config('jwt')['secret'],
      passthrough: true
    }
  },

  // payload 这里配置因为本例中 jwt 并没有用到 request 解析后的参数
];

起初我想通过配置 match 参数来决定某个 URL 是否需要登录认证,后来发现这样需要配置好多的正则,比较麻烦; 其次 koa-jwt 没有提供无权访问自定义错误的钩子,所以放弃了 match 的方案。

这里采用了 koa-jwt 提供的配置 passthrough: true,这个参数让我们不管权限验证通过与否都可以继续执行后面的中间件,只是在当前的 ctx 上设置了 payload。

我们错误处理需要在 logic 层进行,而不应该在 controller 层,否则会出现以下问题:如果 logic 层有有参数校验不通过同时无权访问,会先报参数校验不通过信息,然后再报无权访问,这显然是不符合要求的。

扩展 think.Controller

这里我们对 think.Controller 做了扩展,这里没有对 think.Logic 上进行扩展是因为 think.Logic 继承自 think.Controller。

/src/extend/controller.js:

const jsonwebtoken = require('jsonwebtoken');
module.exports = {
  authFail() {
    return this.fail('JWT 验证失败');
  },

  checkAuth(target, name, descriptor) {
    const action = descriptor.value;
    descriptor.value = function() {
      console.log(this.ctx.state.user);
      const userName = this.ctx.state.user && this.ctx.state.user.name;
      if (!userName) {
        return this.authFail();
      }
      this.updateAuth(userName);
      return action.apply(this, arguments);
    }
    return descriptor;
  },

  updateAuth(userName) {
    const userInfo = {
      name: userName
    };
    const {secret, cookie, expire} = this.config('jwt');
    const token = jsonwebtoken.sign(userInfo, secret, {expiresIn: expire});
    this.cookie(cookie, token);
    return token;
  }
}

其中 authFail 是 JWT 验证失败的操作;updateAuth 是更新 JWT,此处使用 jsonwebtoken 生成 JWT 并种 cookie;checkAuth 使用了 decorator 方式实现,当然你也可以使用你喜欢的方式。

此处使用 cookie 的方式记录生成的JWT, 当初也可以采用别的方式储存,koa-jwt 提供了 getToken 让我们能够自由的获取 JWT, 此处不再详述。

controller 业务逻辑

/src/controller/jwt1.js

const userList = {
  lushijie: '123123',
  xiaoming: '456456'
};

module.exports = class extends think.Controller {
  async userAction() {
    const userInfo = this.ctx.state.user;
    if (userInfo) {
      return this.success(userInfo);
    } else {
      return this.fail('获取用户信息失败');
    }
  }

  loginAction() {
    const {name, password} = this.get();
    if (userList[name] && password === userList[name]) {
      const token = this.updateAuth(name);
      return this.success(token);
    } else {
      return this.fail('登录失败');
    }
  }

  logoutAction() {
    this.updateAuth(null);
    return this.success('退出登录成功');
  }
}

jwt1 这个简单的 controller 包含了三个简单的功能,登录、退出与获取用户信息,其中获取用户信息要求必须登录之后才可以访问。 这里的登录只是进行了一个简单的模拟,真实项目中的用户验证会比这个复杂一些,原理是一致的。

Logic 权限验证

/src/logic/jwt1.js

const {checkAuth} = think.Controller.prototype;
module.exports = class extends think.Logic {

  @checkAuth
  userAction(){
    // 正常的参数验证逻辑
  }
}

这样一个验证就完成了! 如果该 Logic 中的所有 action 都需要进行验证,只需要给 __before 加 decorator 就可以了,其他的 action 就不用加了!

编辑于 2018-07-30

文章被以下专栏收录

    ThinkJS 是一款基于 Koa 2.0 面向未来的企业级 Node.js 框架,致力于整合了大量的项目最佳实践,让企业级开发变得如此简单、高效。本专栏会分享一些在 ThinkJS 项目开发过程中总结的一些经验以及问题,同时也非常欢迎大家投稿。