Based on egg JS framework node User management design of JS service construction

preface

Recently, the company needs to build a set of EMM (Enterprise Mobility Management) management platform. For this kind of enterprise oriented application management, the needs to be considered are very complex. The construction of the management end and the service end at the technical level is the core of the architecture. The client itself does not need to be so complex at the initial stage. As the person in charge of the mobile end (in fact, a small group leader), Naturally, I have to participate in this platform architecture. As a front-end jser, I always receive such jobs that are not very like the front-end. If I had been in the past, I might have some conflicts with many things that need to be considered at this business level. The technology implementation itself is not easy to accumulate technology growth. In this year, I have grown up too much. I always try to do something meaningful that I may not like, so I still want to do a good job in this task. Therefore, I want to consider participating in the construction of EMM server. In fact, if you want to do well in anything, how can there be a difference between meaningful and meaningless?

Considering the node The services built by JS are becoming more and more popular. It is also convenient to build microservices on the platform container cloud. In addition, as a programmer from the front-end jser, he uses node JS to build services. I have studied for a while before, egg JS, this time without hesitation, I chose to use egg JS framework.

Why egg JS?

Last year at gitchat Advanced JavaScript vue JS + node JS entry actual combat development China Amway egged JS, when I first came into contact with egg JS, but it still amazes me. Egg inherits from koa, pursues "Convention is better than configuration", carries out application development according to a set of unified conventions, and the plug-in mechanism is relatively perfect. Although egg inherits from koa, you may feel that you can implement a framework based on koa, and there is no need to do so based on this framework. In fact, you need to learn from each other's strengths to design such a framework yourself. In the short term, the time cost is not worth it. Koa is a small and sophisticated framework, and egg, as the document says, is born for enterprise level frameworks and applications, which is very convenient for us to quickly build a complete enterprise level application. The egg function has been relatively perfect. In addition, if there is no implemented function, it is not difficult to package it according to the plug-ins provided by the koa community.

ORM design and selection

In terms of database selection, MySQL is considered to be used in this project instead of MongoDB. The Egg MySQL plug-in is used at the beginning. After writing a part, it is found that too many things are written in the service, and the modification of table fields will affect too much code. There is a lack of Model management in the design. It is seen that the ORM framework, such as serialize, can be introduced, and Egg officially provides the Egg serialize plug-in.

What is ORM?

First, what is ORM?

Object Relational Mapping (English: Object Relational Mapping, referred to as ORM, or O/RM, or O/R mapping) is a programming technology used to realize the conversion between data of different types of systems in object-oriented programming languages. In effect, it actually creates a "virtual object database" that can be used in programming languages.

Similar to the DAO design pattern in J2EE, the data objects in the program are automatically transformed into the corresponding tables and columns in the relational database, and the references between data objects can also be transformed into tables through this tool. In this way, the problem I encountered can be well solved. Table structure modification and data object operation are two independent parts, which makes the code better maintained. In fact, whether or not to choose the ORM framework is the same as whether the front end used to choose a template engine or manually spell strings. The ORM framework avoids the manual splicing of SQL statements during development, which can prevent SQL injection. In addition, it also decouples the database from the data CRUD, making it easier to replace the database.

Serialize framework

Serialize is node An ORM framework popular in the JS community. Related documents:

Serialize use

Installation:

$ npm install --save sequelize

Establish connection:

const Sequelize = require("sequelize");

// Full usage
const sequelize = new Sequelize("database", "username", "password", {
  host: "localhost",
  dialect: "mysql" | "sqlite" | "postgres" | "mssql",
  operatorsAliases: false,
  pool: {
    max: 5,
    min: 0,
    acquire: 30000,
    idle: 10000
  },
  // SQLite only
  storage: "path/to/database.sqlite"
});

// Simple usage
const sequelize = new Sequelize("postgres://user:pass@example.com:5432/dbname");

Verify that the connection is correct:

sequelize
  .authenticate()
  .then(() => {
    console.log("Connection has been established successfully.");
  })
  .catch(err => {
    console.error("Unable to connect to the database:", err);
  });

Define Model:

Define the basic syntax of a Model:

sequelize.define("name", { attributes }, { options });

For example:

const User = sequelize.define("user", {
  username: {
    type: Sequelize.STRING
  },
  password: {
    type: Sequelize.STRING
  }
});

For a Model field type design, the following aspects are mainly considered:

Serialize will add createdAt and updatedAt by default, so that you can easily know the time of data creation and update. If you do not want to use attributes, you can set timestamps: false;

Serialize supports rich data types, such as STRING, CHAR, TEXT, INTEGER, FLOAT, DOUBLE, BOOLEAN, DATE, UUID
, JSON and other different data types. See the document for details: DataTypes.

Getters & setters support is very useful when we need to process fields, such as case conversion of field values.

const Employee = sequelize.define("employee", {
  name: {
    type: Sequelize.STRING,
    allowNull: false,
    get() {
      const title = this.getDataValue("title");
      return this.getDataValue("name") + " (" + title + ")";
    }
  },
  title: {
    type: Sequelize.STRING,
    allowNull: false,
    set(val) {
      this.setDataValue("title", val.toUpperCase());
    }
  }
});

There are two types of field verification: non empty verification and type verification. In serialize, non empty verification is determined by the allowNull attribute of the field, type verification is determined by validate, and the bottom layer is determined by validator.js Implemented. If a specific field of the model is set to allow null (allowNull:true) and the value is set to null, the validate property does not take effect. For example, there is a string field, allowNull is set to true, and validate verifies that its length is at least 5 characters, but it is also allowed to be empty.

const ValidateMe = sequelize.define("foo", {
  foo: {
    type: Sequelize.STRING,
    validate: {
      is: ["^[a-z]+$", "i"], // will only allow letters
      is: /^[a-z]+$/i, // same as the previous example using real RegExp
      not: ["[a-z]", "i"], // will not allow letters
      isEmail: true, // checks for email format (foo@bar.com)
      isUrl: true, // checks for url format (http://foo.com)
      isIP: true, // checks for IPv4 (129.89.23.1) or IPv6 format
      isIPv4: true, // checks for IPv4 (129.89.23.1)
      isIPv6: true, // checks for IPv6 format
      isAlpha: true, // will only allow letters
      isAlphanumeric: true, // will only allow alphanumeric characters, so "_abc" will fail
      isNumeric: true, // will only allow numbers
      isInt: true, // checks for valid integers
      isFloat: true, // checks for valid floating point numbers
      isDecimal: true, // checks for any numbers
      isLowercase: true, // checks for lowercase
      isUppercase: true, // checks for uppercase
      notNull: true, // won't allow null
      isNull: true, // only allows null
      notEmpty: true, // don't allow empty strings
      equals: "specific value", // only allow a specific value
      contains: "foo", // force specific substrings
      notIn: [["foo", "bar"]], // check the value is not one of these
      isIn: [["foo", "bar"]], // check the value is one of these
      notContains: "bar", // don't allow specific substrings
      len: [2, 10], // only allow values with length between 2 and 10
      isUUID: 4, // only allow uuids
      isDate: true, // only allow date strings
      isAfter: "2011-11-05", // only allow date strings after a specific date
      isBefore: "2011-11-05", // only allow date strings before a specific date
      max: 23, // only allow values <= 23
      min: 23, // only allow values >= 23
      isCreditCard: true, // check for valid credit card numbers

      // custom validations are also possible:
      isEven(value) {
        if (parseInt(value) % 2 != 0) {
          throw new Error("Only even values are allowed!");
          // we also are in the model's context here, so this.otherField
          // would get the value of otherField if it existed
        }
      }
    }
  }
});

Finally, we explain the design of the most important field primary key id, which needs to be specified as the primary key through the field primaryKey: true. There are two main ways to design the primary key in MySQL: Auto increment; UUID.

Auto increment can be set to autoIncrement: true. This method is the most convenient and efficient for general small-scale systems. However, it is not conducive to distributed cluster deployment. This method has been used in almost all MySQL applications, and will not be discussed in depth here.

UUID, also known as globally unique identifier, is a 128 bit (fixed length) unsigned integer, which can ensure uniqueness in space and time. And it can be generated at any time on demand without the guarantee of registration mechanism. According to WIKI, the repetition probability of UUID generated by random algorithm is one in 17billion. The serialize data types include UUID, UUID1, and UUID4, which are based on node-uuid follow RFC4122 . For example:

const User = sequelize.define("user", {
  id: {
    type: Sequelize.UUID,
    primaryKey: true,
    allowNull: false,
    defaultValue: Sequelize.UUID1
  }
});

In this way, the default id value generates a uuid string, for example,'1c572360-faca-11e7-83ee-9d836d45ff41', which we don't really want. We can set the defaultValue to achieve this. For example:

const uuidv1 = require("uuid/v1");

const User = sequelize.define("user", {
  id: {
    type: Sequelize.UUID,
    primaryKey: true,
    allowNull: false,
    defaultValue: function() {
      return uuidv1().replace(/-/g, "");
    }
  }
});

Use the Model object:

For Model object operations, serialize provides a series of methods:

  • find: search a specific element in the database through findById or findOne;
  • findOrCreate: search for a specific element or create it when it is unavailable;
  • findAndCountAll: search multiple elements in the database and return data and total number;
  • findAll: search multiple elements in the database;
  • Complex filtering / OR / NOT queries;
  • Operate the dataset using limit, offset, order, and group;
  • count: calculate the occurrence times of elements in the database;
  • max: get the maximum value of a specific attribute in a specific table;
  • min: obtain the minimum value of a specific attribute in a specific table;
  • Sum: sum the values of specific attributes;
  • Create: create a database Model instance;
  • Update: update the database Model instance;
  • Destroy: destroy the database Model instance.

CRUD can be realized through a series of methods provided above, for example:

User.create({ username: "fnord", job: "omnomnom" })
  .then(() =>
    User.findOrCreate({
      where: { username: "fnord" },
      defaults: { job: "something else" }
    })
  )
  .spread((user, created) => {
    console.log(
      user.get({
        plain: true
      })
    );
    console.log(created);
    /*
    In this example, findOrCreate returns an array like this:
    [ {
        username: 'fnord',
        job: 'omnomnom',
        id: 2,
        createdAt: Fri Mar 22 2013 21: 28: 34 GMT + 0100(CET),
        updatedAt: Fri Mar 22 2013 21: 28: 34 GMT + 0100(CET)
      },
      false
    ]
    */
  });

Egg serialize plug-in

Document: egg serialize: https://github.com/eggjs/egg-sequelize

Analysis of source code

Here, we will not analyze the egg plug-in specification for the time being. For the time being, we will just look at egg-serialize/lib/loader JS implementation:

"use strict";

const path = require("path");
const Sequelize = require("sequelize");
const MODELS = Symbol("loadedModels");
const chalk = require("chalk");

Sequelize.prototype.log = function() {
  if (this.options.logging === false) {
    return;
  }
  const args = Array.prototype.slice.call(arguments);
  const sql = args[0].replace(/Executed \(.+?\):\s{0,1}/, "");
  this.options.logging.info("[model]", chalk.magenta(sql), `(${args[1]}ms)`);
};

module.exports = app => {
  const defaultConfig = {
    logging: app.logger,
    host: "localhost",
    port: 3306,
    username: "root",
    benchmark: true,
    define: {
      freezeTableName: false,
      underscored: true
    }
  };
  const config = Object.assign(defaultConfig, app.config.sequelize);

  app.Sequelize = Sequelize;

  const sequelize = new Sequelize(
    config.database,
    config.username,
    config.password,
    config
  );

  // app.sequelize
  Object.defineProperty(app, "model", {
    value: sequelize,
    writable: false,
    configurable: false
  });

  loadModel(app);

  app.beforeStart(function*() {
    yield app.model.authenticate();
  });
};

function loadModel(app) {
  const modelDir = path.join(app.baseDir, "app/model");
  app.loader.loadToApp(modelDir, MODELS, {
    inject: app,
    caseStyle: "upper",
    ignore: "index.js"
  });

  for (const name of Object.keys(app[MODELS])) {
    const klass = app[MODELS][name];

    // only this Sequelize Model class
    if ("sequelize" in klass) {
      app.model[name] = klass;

      if (
        "classMethods" in klass.options ||
        "instanceMethods" in klass.options
      ) {
        app.logger
          .error(`${name} model has classMethods/instanceMethods, but it was removed supports in Sequelize V4.\
see: http://docs.sequelizejs.com/manual/tutorial/models-definition.html#expansion-of-models`);
      }
    }
  }

  for (const name of Object.keys(app[MODELS])) {
    const klass = app[MODELS][name];

    if ("associate" in klass) {
      klass.associate();
    }
  }
}

Obviously, the serialized object is instantiated during the plug-in initialization, and the serialized object is attached to the app object, that is, we can use app Serialize to access the serialize object. At the same time, we can use app Model accesses the serialize instantiation, and the model object file is stored in the app/model folder.

User Model design

Here we take the use of egg sequence as an example to illustrate.

Installation:

$ npm i --save egg-sequelize
$ npm install --save mysql2 # For both mysql and mariadb dialects

Configuration:

App/config/plugin JS configuration:

exports.sequelize = {
  enable: true,
  package: "egg-sequelize"
};

App/config/config Default JS configuration:

// Database information configuration
exports.sequelize = {
  // Database type
  dialect: "mysql",
  // host
  host: "localhost",
  // Port number
  port: "3306",
  // user name
  username: "root",
  // password
  password: "xxx",
  // Database name
  database: "AEMM"
};

Model layer:

Although it is possible to use serialize directly, there are some problems. During team development, some people like to add timestamp, while others like to add primary keys and customize table names. A large Web App usually has dozens of mapping tables, and one mapping table is a Model. If you follow your own preferences, the business code is not easy to write. Models are not unified, and many codes cannot be reused. Therefore, we need a unified Model to force all models to comply with the same specification, which is not only simple to implement, but also easy to unify the style.

The first thing we need to define is that the folder where the Model is stored must be in models and named after the Model, for example: pet JS, user JS and so on. Second, each Model must comply with a set of specifications:

  • Unified primary key. The name must be id and the type must be UUID;
  • All fields are NULL by default, unless explicitly specified;
  • Unified timestamp mechanism. Each Model must have createdAt, updatedAt and version, and record the creation time, modification time and version number respectively.

Therefore, instead of using the serialize API directly, we use db JS indirectly defines the Model. For example, user JS should be defined as follows:

app/db.js:

const uuidv1 = require("uuid/v1");

function generateUUID() {
  return uuidv1().replace(/-/g, "");
}

function defineModel(app, name, attributes) {
  const { UUID } = app.Sequelize;

  let attrs = {};
  for (let key in attributes) {
    let value = attributes[key];
    if (typeof value === "object" && value["type"]) {
      value.allowNull = value.allowNull && true;
      attrs[key] = value;
    } else {
      attrs[key] = {
        type: value,
        allowNull: true
      };
    }
  }

  attrs.id = {
    type: UUID,
    primaryKey: true,
    defaultValue: () => {
      return generateUUID();
    }
  };

  return app.model.define(name, attrs, {
    createdAt: "createdAt",
    updatedAt: "updatedAt",
    version: true,
    freezeTableName: true
  });
}

module.exports = { defineModel };

The defineModel we define is to enforce the above rules.

app/model/User.js:

const db = require("../db");

module.exports = app => {
  const { STRING, INTEGER, DATE, BOOLEAN } = app.Sequelize;

  const User = db.defineModel(app, "users", {
    username: { type: STRING, unique: true, allowNull: false }, // user name
    email: { type: STRING, unique: true, allowNull: false }, // mailbox
    password: { type: STRING, allowNull: false }, // password
    name: STRING, // name
    sex: INTEGER, // User gender: 1 male, 2 female, 0 unknown
    age: INTEGER, // Age
    avatar: STRING, // Avatar
    company: STRING, // company
    department: STRING, // department
    telePhone: STRING, // Contact number
    mobilePhone: STRING, // Mobile number
    info: STRING, // Remarks
    roleId: STRING, // Role id
    status: STRING, // User status
    token: STRING, // Authentication token
    lastSignInAt: DATE // Last logon time
  });

  return User;
};

In database operation design, we usually generate the table structure in advance through scripts. If we manually write the SQL to create the table, it is actually a trouble to modify the table structure each time. Sequenize provides Migrations Help to create or migrate databases. Egg-serialize also provides convenient methods. In the development phase, the following methods can be used for automatic execution:

// {app_root}/app.js
module.exports = app => {
  if (app.config.env === "local") {
    app.beforeStart(function*() {
      yield app.model.sync({ force: true });
    });
  }
};

Of course, it can also be found in package Add the following script to JSON:

command explain
npm run migrate:new In/ Create a migration file in migrations/ to
npm run migrate:up Perform migration
npm run migrate:down Rollback a migration

package.json:

...
"scripts": {
  "migrate:new": "egg-sequelize migration:create --name init",
  "migrate:up": "egg-sequelize db:migrate",
  "migrate:down": "egg-sequelize db:migrate:undo"
}
...

After executing npm run migrate:new, modify the files in the migrations folder:

module.exports = {
  async up(queryInterface, Sequelize) {
    const { UUID, STRING, INTEGER, DATE, BOOLEAN } = Sequelize;

    await queryInterface.createTable("users", {
      id: {
        type: UUID,
        primaryKey: true,
        allowNull: false
      }, // User ID (primary key)
      username: { 
        type: STRING, 
        unique: true, 
        allowNull: false 
      }, // user name
      email: { 
        type: STRING, 
        unique: true, 
        allowNull: false
      }, // mailbox
      password: { 
        type: STRING, 
        allowNull: false 
      }, // Login password
      name: STRING, // name
      age: INTEGER, // User age
      info: STRING, // Remarks
      sex: INTEGER, // User gender: 1 male, 2 female, 0 unknown
      telePhone: STRING, // Contact number
      mobilePhone: STRING, // Mobile number
      roleId: STRING, // Role ID
      location: STRING, // Permanent residence
      avatar: STRING, // Avatar
      company: STRING, // company
      department: STRING, // department
      emailVerified: BOOLEAN, // Mailbox verification
      token: STRING, // Authentication token
      status: { type: INTEGER, allowNull: false }, // User status: 1 enabled, 0 disabled, 2 hidden, 3 deleted
      createdAt: DATE, // User creation time
      updatedAt: DATE, // User information update time
      lastSignInAt: DATE // Last logon time
    });
  },

  async down(queryInterface, Sequelize) {
    await queryInterface.dropTable("users");
  }
};

User authentication and model selection

The so-called user Authentication is a mechanism that allows users to log in, and allow users to use their accounts when accessing the website in the next period of time without logging in again.

Tip: don't confuse user authentication with user Authorization. User Authorization refers to specifying and allowing users to use their own permissions, such as posting, managing sites, etc.

User authentication is mainly divided into two parts:

  • The user logs in through the user name and password to generate and obtain a Token;
  • The user verifies the user's identity through the Token to obtain relevant information.

JSON Web Token (JWT) specification

JSON Web Token (JWT) is a very lightweight specification . This specification allows us to use JWT to pass safe and reliable information between users and servers.

Composition of JWT

A JWT is actually a string, which consists of three parts: header, payload and signature.

Header

JWT requires a header, which is used to describe the most basic information about the JWT, such as its type and the algorithm used for signing. This can also be represented as a JSON object.

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

Here, we show that this is a JWT, and the signature algorithm we use is HS256 algorithm. It should also be Base64 encoded, and then the string becomes the JWT Header (Header).

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

Here we use the base64url module to perform Base64 encoding to obtain this string. The test code is as follows:

const base64url = require("base64url");

let header = {
  typ: "JWT",
  alg: "HS256"
};

console.log("header: " + base64url(JSON.stringify(header)));
// header: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
Little knowledge: Base64 is a kind of code, that is, it can be translated back to the original. It is not an encryption process.

Load (Payload)

To put it bluntly, the data we need to include is similar to the request body of a network request, for example:

{
  "iss": "zhaomenghaun",
  "sub": "*@agree.com.cn",
  "aud": "www.agree.com.cn",
  "exp": 1526875179,
  "iat": 1526871579,
  "id": "49a9dd505c9d11e8b5e86b9776bb3c4f"
}

The first five fields are defined by the JWT standard.

  • iss: issuer of the JWT
  • sub: the user that the JWT targets
  • aud: the party receiving the JWT
  • exp(expires): when to expire. Here is a Unix timestamp
  • iat(issued at): when was it issued

base64 encode the following JSON object to get the following string, which we call the Payload of JWT.

const base64url = require("base64url");

let payload = {
  id: "49a9dd505c9d11e8b5e86b9776bb3c4f",
  iat: 1526871579,
  exp: 1526875179
};
console.log("payload: " + base64url(JSON.stringify(payload)));
// payload: eyJpZCI6IjQ5YTlkZDUwNWM5ZDExZThiNWU4NmI5Nzc2YmIzYzRmIiwiaWF0IjoxNTI2ODcxNTc5LCJleHAiOjE1MjY4NzUxNzl9

Signature

Use a period for both encoded strings above Connected together (head in front) to form:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjQ5YTlkZDUwNWM5ZDExZThiNWU4NmI5Nzc2YmIzYzRmIiwiaWF0IjoxNTI2ODcxNTc5LCJleHAiOjE1MjY4NzUxNzl9

Finally, we use HS256 algorithm to encrypt the string spliced above. When encrypting, we also need to provide a secret. We can use node-jwa Perform HS256 algorithm encryption. If we use 123456 as the key, we can get our encrypted content, which is also called signature. The last step is to sign the header and payload contents.

 

const jwa = require("jwa");
const hmac = jwa("HS256");

let secret = "123456";
const signature = hmac.sign(
  "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjQ5YTlkZDUwNWM5ZDExZThiNWU4NmI5Nzc2YmIzYzRmIiwiaWF0IjoxNTI2ODcxNTc5LCJleHAiOjE1MjY4NzUxNzl9",
  secret
);
console.log("signature: " + signature);
// signature: JtrTx9QaN3BD1QkZhY58MTu6WHn_vQwRBxO9VwJgkhE

Finally, this part of the signature is spliced behind the signed string to obtain the complete JWT, as follows:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjQ5YTlkZDUwNWM5ZDExZThiNWU4NmI5Nzc2YmIzYzRmIiwiaWF0IjoxNTI2ODcxNTc5LCJleHAiOjE1MjY4NzUxNzl9.JtrTx9QaN3BD1QkZhY58MTu6WHn_vQwRBxO9VwJgkhE

After the whole process, we need to think about whether the Token is secure and can transmit sensitive information?

We now understand that a Token consists of three segments: Header Base64 encoding + Payload Base64 encoding + Signature. When other people get our Token, they can decode the first two segments of the Token to get the Header and Payload objects. Here we node-jsonwebtoken The module decode method directly "decrypts" our Token.

const jwt = require("jsonwebtoken");

let decoded = jwt.decode(
  "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjQ5YTlkZDUwNWM5ZDExZThiNWU4NmI5Nzc2YmIzYzRmIiwiaWF0IjoxNTI2ODcxNTc5LCJleHAiOjE1MjY4NzUxNzl9.JtrTx9QaN3BD1QkZhY58MTu6WHn_vQwRBxO9VwJgkhE",
  { complete: true }
);
console.log("jsonwebtoken: " + JSON.stringify(decoded));
// jsonwebtoken: {"header":{"typ":"JWT","alg":"HS256"},"payload":{"id":"49a9dd505c9d11e8b5e86b9776bb3c4f","iat":1526871579,"exp":1526875179},"signature":"JtrTx9QaN3BD1QkZhY58MTu6WHn_vQwRBxO9VwJgkhE"}

Therefore, our payload cannot contain sensitive information such as passwords. Our id here is a string of UUIDs. Even if we get it, we cannot directly determine the relevant content, so our content will not be directly disclosed.

Generally speaking, the output of encryption algorithm is always different for different inputs. For two different inputs, the probability of producing the same output is extremely small. If someone decodes and modifies the contents of the header and payload, and then encodes them, the signature of the new header and payload will be different from the previous signature. Moreover, if the key used by the server for encryption is not known, the resulting signature will be different.

Therefore, after receiving the JWT, the server will first verify whether the signature is expired, and whether the JWT obtained by re signing the contents of the header and the payload with the same algorithm (specified by the header alg field of the JWT) is consistent with the JWT passed by the user. If the server application signs the header and payload in the same way again and finds that the signature calculated by itself is different from the received signature, it means that the contents of the Token have been moved by others. We should reject the Token and return an HTTP 401 Unauthorized response.

Egg JWT plug-in

Documentation: https://github.com/okoala/egg-jwt

Egg JWT based on node-jsonwebtoken Implementation, complete documents can be referred to https://github.com/auth0/node-jsonwebtoken . jwt objects are attached to app objects, which can be accessed through app jwt has three ways to access jwt:

  • Jwt Sign (payload, secretorprivatekey, [options, callback]) -- generate token string
  • Jwt Verify (token, secretorpublickey, [options, callback]) -- verify the validity of the token
  • Jwt Decode (token [, options]) -- token decoding

Installation:

$ npm i egg-jwt --save

Configuration:

App/config/plugin JS configuration:

exports.jwt = {
  enable: true,
  package: "egg-jwt"
};

App/config/config Default JS configuration:

exports.jwt = {
  enable: false,
  secret: "xxxxxxxxxxxxx"
};

Call:

Request header:

Authorization: Bearer {access_token}

Note: access_token is the token value returned after login.

app/service/user.js:

/**
 * Generate Token
 * @param {Object} data
 */
createToken(data) {
  return app.jwt.sign(data, app.config.jwt.secret, {
    expiresIn: "12h"
  });
}

/**
 * Verify the legitimacy of the token
 * @param {String} token
 */
verifyToken(token) {
  return new Promise((resolve, reject) => {
    app.jwt.verify(token, app.config.jwt.secret, function(err, decoded) {
      let result = {};
      if (err) {
        /*
          err = {
            name: 'TokenExpiredError',
            message: 'jwt expired',
            expiredAt: 1408621000
          }
        */
        result.verify = false;
        result.message = err.message;
      } else {
        result.verify = true;
        result.message = decoded;
      }
      resolve(result);
    });
  });
}

extend/helper.js:

// Get Token
exports.getAccessToken = ctx => {
  let bearerToken = ctx.request.header.authorization;
  return bearerToken && bearerToken.replace("Bearer ", "");
};

// Verify Token
exports.verifyToken = async (ctx, userId) => {
  let token = this.getAccessToken(ctx);
  let verifyResult = await ctx.service.user.verifyToken(token);
  if (!verifyResult.verify) {
    ctx.helper.error(ctx, 401, verifyResult.message);
    return false;
  }
  if (userId != verifyResult.message.id) {
    ctx.helper.error(ctx, 401, "user ID And Token Inconsistent");
    return false;
  }
  return true;
};

// Process successful response
exports.success = (ctx, result = null, message = "Request succeeded", status = 200) => {
  ctx.body = {
    code: 0,
    message: message,
    data: result
  };
  ctx.status = status;
};

// Process failure response
exports.error = (ctx, code, message) => {
  ctx.body = {
    code: code,
    message: message
  };
  ctx.status = code;
};

Call in controller:

// Generate Token
let token = ctx.service.user.createToken({ id: user.id });

// Verify the validity of Token
let isVerify = await ctx.helper.verifyToken(ctx, id);
if (isVerify) {
  // Legal logic
  // ...
}

In this way, the restful API that needs identity authentication can be authenticated through token, so as to realize user authentication and authorization.

Postscript

The original purpose of this article is to explain the node The problems and gains encountered in the JS service process have not been written for a long time, so my thinking can not be divergent for the moment. I can only describe the basic usage of plug-ins used in the design process and some design thinking. I don't ask to help others, but to help me sort out my thinking. After writing, I found that my cognition has been much clearer, and many of my previous doubts have suddenly become clear.

Many people haven't written articles. In the past six months, they have been mainly responsible for the architecture design and module development of the hybrid mobile terminal. They have been working hard for almost a year. Their main energy has been spent on the following set of JS SDK and native base.

 

In the past six months, I have read a lot of framework source codes and tried to write some basic architectures, internal documents and notes, but I have not summarized and shared them in the open source community. Looking back, I have some regrets. Although I have been very busy and have no time to comfort myself, in fact, there is still a squeeze in time. Therefore, I will take more time to archive in the future. After all, I will understand it more deeply.

reference

Transferred from https://segmentfault.com/a/1190000014952764?utm_source=sf-related

Tags: egg

Posted by ramesh_iridium on Sun, 29 May 2022 23:35:40 +0530