Egg.js реализует вход в систему и аутентификацию интерфейса на основе токена jsonwebtoken.

Egg.js
Egg.js реализует вход в систему и аутентификацию интерфейса на основе токена jsonwebtoken.

I. Обзор

Эта статья в основном посвящена тому, как использовать токен, сгенерированный JSON Web Token.js, для реализации аутентификации пользователя при входе в систему в среде Egg.js.

JSON Web Token(аббревиатура JWT) в настоящее время является самым популярным решением для междоменной сертификации. Его краткое введение выглядит следующим образом:

JWT

Веб-токен Json (JWT) — это стандарт разработки на основе JSON (RFC 7519) для передачи утверждений между средами сетевых приложений.Токен разработан, чтобы быть компактным и безопасным, особенно для сценария единого входа на распределенные сайты (SSO). . Заявки JWT обычно используются для передачи аутентифицированной информации об удостоверении пользователя между поставщиками удостоверений и поставщиками услуг для получения ресурсов с серверов ресурсов, а также могут добавлять некоторую дополнительную информацию о заявке, необходимую для другой бизнес-логики.Можно использовать непосредственно для проверки подлинности или зашифровать.

Что касается принципа и характеристик веб-токена JSON, в этой статье не будет подробного введения.Если вы хотите провести больше исследований, вы можете обратиться к Treasure Boy.Блог Ифэн Руана: Начало работы с веб-токеном JSON

Читатели этой статьи должны иметь определенное представление о Egg.js.

2. Идеи реализации и сопутствующие ресурсы

1. Реализовать идеи

Во-первых, функции, реализованные в статье, представляют собой простую проверку информации о пользователе, присвоение пользователю системных прав, единый вход и т. д. не рассматривались. Поэтому, исходя из этой характеристики, реализация данной функции анализируется следующим образом:

Function Workflow

Кроме того, в дополнение к входу пользователя в систему необходимо убедиться, что все интерфейсы должны быть защищены, путем проверки информации токена, чтобы получить соответствующие результаты. В Egg.js этот процесс проверки реализован в промежуточном программном обеспечении.

Function Workflow 2

Моя привычка состоит в том, чтобы попытаться как можно лучше понять задействованные концепции и процесс реализации, прежде чем создавать функцию, поэтому в статье всегда есть процесс, подобный этому анализу, прежде чем начнется код.Если есть что-то плохое, исправления приветствуются.

2. Связанные ресурсы

Используются функции шифрования и дешифрования, задействованные в веб-токенах json.jsonwebtokenплагин,

Шифрование и дешифрование, связанные с паролем пользователя egg.js,node-jsencryptплагин,

Шифрование и дешифрование, участвующие в пользовательском пароле в vue.js используетjsencryptПлагин, по названию легко понять, что он такой же, как метод шифрования и дешифрования в egg.js.

Обе части используют файл ключа RSA одновременно, поэтому метод создания файла ключа RSA также заранее опубликован здесь:

Используйте Openssl для создания закрытого ключа и открытого ключа

Сгенерировать открытый ключ:openssl genrsa -out rsa_private_key.pem 1024

Сгенерировать закрытый ключ:openssl rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem

требует внимания,Закрытый ключ не является публичным, в противном случае безопасность не может ждать, чтобы быть гарантированной.

3. Реализация кода

Бэкенд-фреймворк:Egg.js

Внешний фреймворк:Vue.js

База данных:MongoDB

В настоящее время функции, которые я реализую, выполняются в проекте в разделе «Разработка», поэтому полный код не будет загружен. Перед официальным внедрением функции в соответствии с вышеуказанным процессом необходимо выяснить, как производится токен, как это подтвердить, и что является методом шифрования RSA. Здесь мы сначала посмотрим на эти два метода. Если вы не знакомы с Framework.js framework, у вас может быть много вопросов. Рекомендуется прочитать большеДокументация по API egg.js

1. Логин пользователя

Function Workflow

(1) передняя часть

Как видно на блок-схеме, пользователь вводит имя пользователя и пароль, и после проверки он сначала проходит процесс шифрования, а затем инициирует запрос API, что гарантирует безопасность информации пользователя от внешнего интерфейса до внутреннего. . Внешний метод шифрования и дешифрования (в настоящее время дешифрование не требуется), как указано в пункте 2 выше,jsencrypt.

1.1 Код целевой страницы
// src\views\login\index.vue
<template>
  <div class="login">
    <div class="login-wrap">
      <div class="login-panel">
        <el-form :model="userInfo" :rules="rules" ref="userInfo">
          <el-form-item prop="checkUsername">
            <el-input
              type="text"
              class="user-info-item"
              v-model="userInfo.username"
              placeholder="Username"
              autocomplete="off"
            ></el-input>
          </el-form-item>

          <el-form-item prop="checkPass">
            <el-input
              type="password"
              class="user-info-item"
              v-model="userInfo.password"
              placeholder="Password"
              autocomplete="off"
            ></el-input>
          </el-form-item>
          <el-form-item>
            <el-button class="user-info-submit" type="success" @click="login">Ok</el-button>
          </el-form-item>
        </el-form>
      </div>
    </div>
  </div>
</template>

<script>
import { mapMutations } from "vuex";
export default {
  name: "",
  components: {},
  data() {
    let validateUsername = (rule, value, callback) => {
      if (this.userInfo.username === "") {
        callback(new Error("Please input user name."));
      } else {
        let isValid = /^[a-zA-Z0-9_]{3,16}$/g.test(this.userInfo.username);
        if (isValid) {
          callback();
        } else {
          callback(new Error("Please input valid user name."));
        }
      }
    };

    let validatePass = (rule, value, callback) => {
      if (this.userInfo.password === "") {
        callback(new Error("Please input password."));
      } else {
        callback();
      }
    };
    return {
      userInfo: {
        username: "",
        password: ""
      },
      key: "",
      rules: {
        checkUsername: [{ validator: validateUsername, trigger: "blur" }],
        checkPass: [{ validator: validatePass, trigger: "blur" }]
      }
    };
  },
  computed: {
    userInfoEncryped: function() {
      let username = this.userInfo.username;
      // 对用户密码 加密
      let password = this.key
        ? this.$utils.encrypt.rsaEncrypt(this.userInfo.password, this.key)
        : "";
      return {
        username,
        password
      };
    }
  },
  async mounted() {
    // 获取公钥信息
    // 使用 jsecrypt 时,必须用到公钥进行加密,这个公钥我放在服务端以接口形式提供的,因此这里我在页面初     // 始化时获取公钥并缓存
      
    let getKey = await this.$service.login.getKey();
    if (getKey.succeed) this.key = getKey.data;
  },
  methods: {
    ...mapMutations("user", ["SET_TOKEN_INFO", "SET_USER_NAME"]),
    async login() {
      this.$refs["userInfo"].validate(async valid => {
        if (valid) {
          let login = await this.$service.login.signIn(this.userInfoEncryped);
          if (login.succeed) {
              this.$router.push("/index");
          } else {
            this.$message.error("Faild to sign in .");
          }
        } else {
          console.log("error submit!!");
          return false;
        }
      });
    }
  }
};
</script>

<style lang="less">
	/* 略 */
</style>

Реализация внешнего интерфейса — это всего лишь пример, в основном для демонстрации этапа шифрования, а управление состоянием, которое необходимо использовать другим предприятиям, можно пропустить.

1.2 Добавьте параметр token в заголовок HTTP-запроса во внешнем перехватчике.

Вот аксиомы, используемые в vue

/**
 * request interceptor
 * @param  {Object} config
 * @return {Object}
 */
request.interceptors.request.use(
  config => {
    // do something before request is sent
    let urlParams = config.url + JSON.stringify(config.params);

    if (cancelRequest.has(urlParams) && repeatWhiteLst(urlParams)) {
      cancelRequest.get(urlParams)("Repeat Request");
    }
    config.cancelToken = new CancelToken(cancel => {
      cancelRequest.set(urlParams, cancel);
    });

	// 添加 token 信息到 请求头部
    let tokenInfo = getToken();
    config.headers["authorization"] = tokenInfo.token;
    return config;
  },
  error => {
    // Do something with request error
    // eslint-disable-next-line
    console.log(error);
    Promise.reject(error);
  }
);
1.3 Код ключа внешнего шифрования/дешифрования
// src\utils\encrypt.js

import JSEncrypt from "jsencrypt";

/**
 * Encrypt with the public key...
 * @param {String} text
 * @param {String} publicKey
 * @returns ciphertext
 */
export const rsaEncrypt = (text, publicKey) => {
  // public key 是来自后端保存好的公钥
  let _publicKey =
    "-----BEGIN PUBLIC KEY-----" + publicKey + "-----END PUBLIC KEY-----";
  let encrypt = new JSEncrypt();
  encrypt.setPublicKey(_publicKey);
  let encrypted = encrypt.encrypt(text);
  return encrypted;
};

/**
 * Decrypt with the private key...
 * @param {String} ciphertext
 * @param {String} privateKey
 * @returns text
 */
export const rsaDecrypt = (ciphertext, privateKey) => {
  // let _privateKey =
  //   "-----BEGIN RSA PRIVATE KEY-----" +
  //   privateKey +
  //   "-----END RSA PRIVATE KEY-----";
  
  let decrypt = new JSEncrypt();
  decrypt.setPrivateKey(privateKey);
  let uncrypted = decrypt.decrypt(ciphertext);
  return uncrypted;
};

export default { rsaEncrypt, rsaDecrypt };

(2) Серверная часть

Когда серверная часть завершает функцию входа в систему, прежде всего, обратите внимание на два момента: когда пользователь инициирует эти два запроса, во внешней части нет информации о токене, поэтому ее необходимо настроить в промежуточном программном обеспечении. Предметы. И промежуточное ПО здесь относится к jwt.js, промежуточному ПО JSON Web Token.

2.1 Конфигурация промежуточного программного обеспечения jwt
//
// config\config.default.js
//

"use strict";

module.exports = appInfo => {
  const config = (exports = {});

  // use for cookie sign key, should change to your own and keep security
  config.keys = appInfo.name + "_";

  // add your config here
  config.middleware = ["jwt", "compress", "errorHandler", "notfoundHandler"];

  // json web token 验证
  config.jwt = {
    enable: true,
    ignore: ["/sign/in", "/auth/pubkey"] // 哪些请求不需要认证
  };

  // Gzip 压缩阈值
  config.compress = {
    threshold: 1000
  };

  // 解决 csrf 安全策略,导致 API 无法访问
  config.security = {
    csrf: {
      enable: false
      // ignoreJSON: true
    },
    domainWhiteList: ["*"]
  };

  // 结局跨域的我问题
  config.cors = {
    origin: "*",
    allowMethods: "GET,HEAD,PUT,POST,DELETE,PATCH"
  };

  return config;
};

Function Workflow 2
Здесь и в кодовом логическом потоке JWT.js промежуточное программное обеспечение согласуется, она может быть понята в сочетании с наблюдаемым графиком.

//
// app\middleware\jwt.js
// config 中配置的 ["/sign/in", "/auth/pubkey"] 这个两个接口将不会通过此中间件
//

"use strict";

module.exports = () => {
  return async function Interceptor(ctx, next) {
    let reqUrl = ctx.request.url;
    if (reqUrl == "/") {
      await next();
    } else {
      // 获取header里的authorization
      let authToken = ctx.header.authorization; 
      if (authToken) {
        // 解密获取的Token
        const declassified = ctx.helper.login.verifyToken(authToken); 
        if (!declassified.exp) {
          // 从数据库获取用户信息进行 Token 验证
          let userInfo = await ctx.model.Internal.User.find({
            userName: declassified.username
          });

          let user = userInfo[0].toObject();

          if (user.token === authToken) {
            await next();
          } else {
            ctx.throwBizError("USER_INFO_EXPIRED");
          }
        } else {
          ctx.throwBizError("USER_INFO_EXPIRED");
        }
      } else {
        ctx.throwBizError("UNLOGGED");
      }
    }
  };
};

2.2 Контроллер входа пользователей
//
// app\controller\sign.js
//

"use strict";

const Controller = require("egg").Controller;

class SignController extends Controller {
  async signIn() {
    const { ctx } = this;
    const user = ctx.request.body.username;
    const pass = ctx.request.body.password;

    let passwordInput = ctx.helper.encrypt.rsaDecrypt(pass);

    let userInfo = await ctx.model.Internal.User.find({ userName: user });
    if (userInfo.lenght == 0) {
      ctx.throwBizError("USER_NOT_FOUND");
    } else if (userInfo.lenght > 1) {
      ctx.throwBizError("USER_CONFLICT");
    } else {
      // 数据库中的 用户密码 也需要用加密后的字符串, 因此需要解密后与请求中的用户信息做对比
      let passwordInDB = ctx.helper.encrypt.rsaDecrypt(userInfo[0].userPsw);
      if (passwordInput === passwordInDB) {
        // 用户核对成功后,生成新的 Token
        let newToken = ctx.helper.login.createToken({
          username: user,
          password: pass
        });
        // 更新数据库中的 Token
        let userUpdated = await ctx.model.Internal.User.updateOne(
          { userName: user },
          { token: newToken }
        );

        if (
          userUpdated.n === 1 &&
          userUpdated.nModified === 1 &&
          userUpdated.ok === 1
        ) {
          ctx.body = ctx.helper.response.success({
            token: newToken
          });
        } else {
          ctx.throwBizError("FAILD_TO_LOGIN");
        }
      } else {
        ctx.throwBizError("USER_INFO_ERROR");
      }
    }
  }
  async signOut() {
    const { ctx } = this;
    const user = ctx.request.body.username;
    let userUpdated = await ctx.model.Internal.User.updateOne(
      { userName: user },
      { token: "" }
    );
    if (
      userUpdated.n === 1 &&
      userUpdated.nModified === 1 &&
      userUpdated.ok === 1
    ) {
      ctx.body = ctx.helper.response.success({
        message: `User ${user} has sign out.`
      });
    } else {
      ctx.body = ctx.helper.response.success({
        message: `Faild to sign out.`
      });
    }
  }
  async signUp() {
    const { ctx } = this;
  }
  async getPublicKey() {
    const { ctx } = this;
    ctx.body = ctx.helper.response.success(ctx.helper.encrypt.getPublicKey());
  }
}

module.exports = SignController;

2.3 Код ключа для шифрования/дешифрования

Методы шифрования/дешифрования токена и шифрования/дешифрования пароля, использованные в двух предыдущих частях, монтируются под вспомогательным объектом.Для удобства обслуживания и вызова,

//	
// 为了方便维护,很多工具性的方法,我都挂载在 helper 下
//
// app\extend\helper.js

const login = require("../public/js/login");
const encrypt = require("../public/js/encrypt");

module.exports = {
  login,
  encrypt
};
//
// 用于加密和解密用户密码
// app\public\js\encrypt.js
//

const fs = require("fs");
const path = require("path");
const JSEncrypt = require("node-jsencrypt");

/**
 * Encrypt with the public key...
 * @param {String} text
 * @param {String} publicKey
 * @returns ciphertext
 */
exports.rsaEncrypt = text => {
  const _publicKey = fs.readFileSync(
    path.join(__dirname, "./../files/ssh-key/rsa_public_key.pem")
  );
  let encrypt = new JSEncrypt();
  encrypt.setPublicKey(_publicKey.toString());
  let encrypted = encrypt.encrypt(text);
  return encrypted;
};

/**
 * Decrypt with the private key...
 * @param {String} ciphertext
 * @param {String} privateKey
 * @returns text
 */
exports.rsaDecrypt = ciphertext => {
  const _privateKey = fs.readFileSync(
    path.join(__dirname, "./../files/ssh-key/rsa_private_key.pem")
  ); // 公钥,看后面生成方法
  let decrypt = new JSEncrypt();
  decrypt.setPrivateKey(_privateKey.toString());
  let uncrypted = decrypt.decrypt(ciphertext);
  return uncrypted;
};

exports.getPublicKey = () => {
  let _publicKey = fs.readFileSync(
    path.join(__dirname, "./../files/ssh-key/rsa_public_key.pem")
  );
  _publicKey = _publicKey.toString();
  _publicKey = _publicKey.split("\r\n");
  _publicKey = _publicKey.join("");

  return _publicKey.toString();
};
//
// 用于加密和解密 Token
// app\public\js\login.js
//

const fs = require("fs");
const path = require("path");
const jwt = require("jsonwebtoken"); //引入jsonwebtoken

exports.createToken = (data, expires = 7200) => {
  const exp = Math.floor(Date.now() / 1000) + expires;
  const cert = fs.readFileSync(
    path.join(__dirname, "./../files/ssh-key/rsa_private_key.pem")
  ); // 私钥,看后面生成方法

  const token = jwt.sign({ data, exp }, cert, { algorithm: "RS256" });
  return token;
};

// 解密,验证
exports.verifyToken = token => {
  const cert = fs.readFileSync(
    path.join(__dirname, "./../files/ssh-key/rsa_public_key.pem")
  ); // 公钥,看后面生成方法
  let res = "";

  try {
    const result = jwt.verify(token, cert, { algorithms: ["RS256"] }) || {};
    const { exp } = result,
      current = Math.floor(Date.now() / 1000);
    res = result.data || {};
    current <= exp ? (result.data["exp"] = false) : (result.data["exp"] = true);
  } catch (e) {
    console.log(e);
  }
  return res;
};

4. Резюме

До сих пор был введен код, связанный с использованием JSON Web Token для реализации функций входа пользователя и проверки API Token в Egg.js, но, поскольку эта функция связана с бизнесом, вероятность прямого использования кода может быть ниже, а Функция включает в себя На внешнем интерфейсе непрерывность кода может быть нелегко воспроизвести.

Словом, хотя официальная документация egg.js очень подробная, но в процессе практики неизбежны проблемы, надеюсь, друзья, имеющие опыт в этой области, поделятся еще. Это также то, чем, как мне кажется, стоит поделиться в последнее время.

Ссылка на статью:

Egg GG На основе механизма аутентификации реализации токена JSonWebToken

Блог Ифэн Руана: Начало работы с веб-токеном JSON