I. Обзор
Эта статья в основном посвящена тому, как использовать токен, сгенерированный JSON Web Token.js, для реализации аутентификации пользователя при входе в систему в среде Egg.js.
JSON Web Token(аббревиатура JWT) в настоящее время является самым популярным решением для междоменной сертификации. Его краткое введение выглядит следующим образом:
Веб-токен Json (JWT) — это стандарт разработки на основе JSON (RFC 7519) для передачи утверждений между средами сетевых приложений.Токен разработан, чтобы быть компактным и безопасным, особенно для сценария единого входа на распределенные сайты (SSO). . Заявки JWT обычно используются для передачи аутентифицированной информации об удостоверении пользователя между поставщиками удостоверений и поставщиками услуг для получения ресурсов с серверов ресурсов, а также могут добавлять некоторую дополнительную информацию о заявке, необходимую для другой бизнес-логики.Можно использовать непосредственно для проверки подлинности или зашифровать.
Что касается принципа и характеристик веб-токена JSON, в этой статье не будет подробного введения.Если вы хотите провести больше исследований, вы можете обратиться к Treasure Boy.Блог Ифэн Руана: Начало работы с веб-токеном JSON
Читатели этой статьи должны иметь определенное представление о Egg.js.
2. Идеи реализации и сопутствующие ресурсы
1. Реализовать идеи
Во-первых, функции, реализованные в статье, представляют собой простую проверку информации о пользователе, присвоение пользователю системных прав, единый вход и т. д. не рассматривались. Поэтому, исходя из этой характеристики, реализация данной функции анализируется следующим образом:
Кроме того, в дополнение к входу пользователя в систему необходимо убедиться, что все интерфейсы должны быть защищены, путем проверки информации токена, чтобы получить соответствующие результаты. В Egg.js этот процесс проверки реализован в промежуточном программном обеспечении.
Моя привычка состоит в том, чтобы попытаться как можно лучше понять задействованные концепции и процесс реализации, прежде чем создавать функцию, поэтому в статье всегда есть процесс, подобный этому анализу, прежде чем начнется код.Если есть что-то плохое, исправления приветствуются.
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. Логин пользователя
(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;
};
Здесь и в кодовом логическом потоке 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