Editor’s note: I believe authentication should be one of the basic features required for most Web services. There are many ways to implement permission verification, among which JSON Web Token (JWT) is favored by more and more developers. It is more secure than traditional authentication and does not require additional database queries because the encrypted string contains permission information. Today, we invited Lu Shijie, a developer of ThinkJS, to explain how to use JWT authentication service in the download of ThinkJS.


JSON Web Token (JWT) is a very lightweight specification. This specification allows us to use JWT to deliver secure and reliable information between the user and the server. It provides jSON-formatted tokens for security authentication.

JWT composition

The JWT consists of three parts: header, payload, and signature. These three parts are connected by decimal points.

In this example, a cookie named jwt-token is used to store JWT. For example:

jwt-token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoibHVzaGlqaWUiLCJpYXQiOjE1MzI1OTUyNTUsImV4cCI6MTUzMjU5NTI3MH0.W Z9_poToN9llFFUfkswcpTljRDjF4JfZcmqYS0JcKO8;Copy the code

Among them:

Part of the value
header eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
payload eyJuYW1lIjoibHVzaGlqaWUiLCJpYXQiOjE1MzI1OTUyNTUsImV4cCI6MTUzMjU5NTI3MH0
signature WZ9_poToN9llFFUfkswcpTljRDjF4JfZcmqYS0JcKO8

header

The header is obtained by base64Encode of the type and hash algorithm. Base64Decode for the header in the ratio can be obtained:

{" alg ":" HS256 ", "typ" : "JWT"}Copy the code

payload

Payload is what we get when we base64Encode the information that we need to transmit. The payload in this example can be obtained by base64Decode:

{"name":"lushijie ", "IAT ":1532595255, // JWT release time "exp ": 1532595270 // JWT expiration time, 15 seconds later}Copy the code

In this example, IAT and exp are the default fields in KOA-JWT. In addition, jTI and ISS are optional declarations registered in JWT standards. Interested parties can check more relevant standards. Payload can be decoded on the client. Therefore, you are not advised to store sensitive information, such as the user password, in the payload.

signature

The signature contains the header, payload, and key. The formula is as follows:

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

Copy the code

Here the key is stored on the server and the client is unaware of it.

JWT validation

To verify that a JWT is valid, the server computes the signature part of the JWT and compares it with the signature part of the JWT. If the signature part is equal, the JWT is valid.

JWT practices in ThinkJS

The following implementation in ThinkJS uses the JWT implementation to access an interface only after login. ThinkJS is compatible with all middleware from KOA2, so look for a ready-made JWT plugin. Here we use the KOA-JWT plugin. The koA-jWT code is not a few lines long, so you can read it for a while. Let’s start using it.

Common configuration

/src/config/config.js:

module.exports = {
    // ...
    jwt: {
      secret: 'lushijie-password'.cookie: 'jwt-token'.expire: 30 / / SEC.}},Copy the code

Since these three parameters are used in different locations, we extracted them from the common config for unified management.

Middleware configuration

/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 is configured because in this example, JWT does not use the parsed request parameter
];

Copy the code

At first, I wanted to configure the match parameter to determine whether a URL requires login authentication, but later I found that this required a lot of re configuration, which was quite troublesome. Second, KOA-JWT did not provide hooks that had no access to custom errors, so the match scheme was abandoned.

The passthrough: true configuration provided by KOA-JWT is used. This parameter allows us to continue with the middleware regardless of whether the permission is passed or not, but sets the payload on the current CTX.

Error processing should be carried out at the Logic layer instead of the Controller layer, otherwise the following problems will occur: If some parameters in the Logic layer fail to pass the verification and have no access, the verification failure information will be reported first, and then no access will be reported, which obviously does not meet the requirements.

Extension think. Controller

Here we extend think.Controller. Here we don’t extend think.Logic because think.Logic inherits think.

/src/extend/controller.js

const jsonwebtoken = require('jsonwebtoken');
module.exports = {
  authFail() {
    return this.fail('JWT validation failed ');
  },

  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);
    returntoken; }}Copy the code

Where authFail is the JWT validation failure operation; UpdateAuth is an update JWT, where jsonWebToken is used to generate JWT and seed cookies. CheckAuth uses a decorator, but you can use it however you like.

The JWT generated here is recorded in the form of cookie, which can also be stored in other ways at the beginning. Koa-jwt provides getToken so that we can freely obtain JWT, which will not be detailed here.

Controller Business logic

/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('Failed to obtain user information');
    }
  }

  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('Login failed');
    }
  }

  logoutAction() {
    this.updateAuth(null);
    return this.success('Logged out successfully'); }}Copy the code

Jwt1, a simple controller, contains three simple functions: login, logout and obtaining user information, which can be accessed only after login. The login here is a simple simulation. User authentication in a real project would be more complex than this, and the principle is the same.

Logic Permission Verification

/src/logic/jwt1.js

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

  @checkAuth
  userAction(){
    // Normal parameter validation logic}}Copy the code

The verification is complete! If all the actions in this Logic need to be validated, just add a decorator to __before and leave the rest of the actions alone!