前言
在网络通信中,如果数据包是明文传输,并且包含敏感信息,那么就很容易被抓包窃取,因此加密手段也成了开发者耳熟能详的知识技能;常见的加密方法有对称加密和非对称加密。对称加密使用同一个密钥进行加密和解密,而非对称加密使用公钥和私钥分别进行加密和解密。
另一个需要知识点是防重放措施,防重放攻击是指攻击者会拦截请求并重新发送,从而导致重复处理。常见的防重放攻击方法有使用令牌桶算法和使用 Nonce 值(随机数)。
那么这二者为何会结合在一起呢?
原因是我之前做的零食商贩的案例暴露出来的问题,虽然接口做了加密处理,使用者不容易轻易知道数据包的内容,但是如果复制一个接口再次发起请求还是可以成功,因为接口没有做类似文件阅后即焚的功能,所以做个分享。
功能设计
因为客户端和服务端都在node中实现,所以通信暂时摒弃请求的方式,使用消息中心模拟前后端请求的操作
客户端的功能点(client)
- 通过invoke发送请求
- 创建Nonce随机值
- crypto.aes加密参数
服务端的功能点(server)
- 通过watch接收请求
- Nonce查重
- crypto.aes解密参数
- 通过bcryptjs哈希处理对比密码是否正确
- jsonwebtoken创建及校验token
功能实现
工具函数
helper.bcrypt.js(针对密码进行哈希盐加密)
const bcryptjs = require("bcryptjs"); // 哈希盐加密 exports.createBcrypt = (password, salt = bcryptjs.genSaltSync(10)) => { return bcryptjs.hashSync(password, salt); }; // 校验密码 exports.checkBcrypt = (_password, _hash) => { return bcryptjs.compareSync(_password, _hash); };
helper.random.js(生成随机数+时间戳的字符串)
const { randomNum } = require("utils-lib-js"); // 生成Nonce随机数 exports.createRandom = () => { const date = new Date().getTime(); const start = 0x000000; const end = 0xffffff; return randomNum(start, end) + date; };
helper.jwt.js(JSONwebtoken加解密)
const { sign, verify } = require("jsonwebtoken"); const { defer } = require("utils-lib-js"); const { TokenKey } = require("../config"); // 新建令牌 exports.createToken = ({ payload = {}, tokenKey = TokenKey, expiresIn = "1d", ...others }) => { return sign({ payload }, tokenKey, { expiresIn, ...others, }); }; // 校验令牌 exports.checkToken = ({ token, tokenKey = TokenKey, options }) => { const { reject, resolve, promise } = defer(); verify(token, tokenKey, options, (err, decoded) => { if (err) return reject(err); return resolve(decoded.payload); }); return promise; };
helper.crypto.js(使用AES对参数加解密)
const cryptoJS = require("crypto-js"); const { CryptoKey } = require("../config"); const { jsonToString, stringToJson } = require("utils-lib-js"); const defaultOpt = { mode: cryptoJS.mode.ECB, padding: cryptoJS.pad.Pkcs7, }; const __key = CryptoKey; // 加密关键字 // 加密 const setCrypto = ({ data, key = __key, opts = defaultOpt }) => { return cryptoJS.AES.encrypt(jsonToString(data), key, opts); }; // 解密 const getCrypto = ({ str, key = __key, resToStr = true, opts = defaultOpt, }) => { str = decodeURIComponent(str); //前端传参有特殊字符(中文)时转义(替换百分号) const bytes = cryptoJS.AES.decrypt(str, key, opts); const source = bytes.toString(cryptoJS.enc.Utf8); return resToStr ? stringToJson(source) : source; }; module.exports = { setCrypto, getCrypto, };
client.js(客户端)
const { createRandom } = require("./utils/helper.random"); const { setCrypto } = require("./utils/helper.crypto"); const { messageCenter } = require("event-message-center"); const { initServer } = require("./server"); const { catchAwait } = require("utils-lib-js"); let __token = null; // 登录信息 const userInfo = { username: "zhangsan", password: "123123", }; // 初始化服务端 initServer(); // 客户端加密混淆操作 const encryption = ({ query = {}, key = "params", token = __token }) => { query.id = createRandom(); //生成随机id混淆参数 query.token = token; return { [key]: setCrypto({ data: query }).toString(), }; }; // 模拟前端用户登录操作 const userLogin = (query) => { const params = encryption({ query }); return messageCenter.invoke("/login", params); }; // 模拟登录成功后请求 const getInfo = (query) => { const params = encryption({ query }); return messageCenter.invoke("/info", params); }; // 初始化函数 const init = async () => { const [err, res] = await catchAwait(userLogin(userInfo)); if (err) return console.error(err); __token = res.token; const [err2, info] = await catchAwait(getInfo()); if (err2) return console.error(err2); console.log(info); }; init();
server.js(服务端)
const { messageCenter } = require("event-message-center"); const { defer, catchAwait } = require("utils-lib-js"); const { getCrypto } = require("./utils/helper.crypto"); const { createBcrypt, checkBcrypt } = require("./utils/helper.bcrypt"); const { createToken, checkToken } = require("./utils/helper.jwt"); const __temp = new Map(); // 将请求过的id存起来(后续可以加定时任务清除缓存,或者增加长度限制) const userInfo = { // 指代数据库取数据 username: "zhangsan", password: createBcrypt("123123"), }; // 解密操作 const decrypt = (query) => { return getCrypto({ str: query }); }; // 请求去重 const checkRepeat = (query = {}) => { const __id = query.id; if (!!!__id || __temp.has(__id)) return; return __temp.set(__id, query); }; // 抛错 const promiseRej = (err) => Promise.reject(err); // 加个简单的中间件,做校验 const middleware = { decrypt: (data) => { // 解密,重复请求校验 const { resolve, reject, promise } = defer(); const params = decrypt(data.params); if (!!!checkRepeat(params)) reject("重复请求或id为空"); else resolve(params); return promise; }, token: async ({ token, ...data }) => { // token校验 const { resolve, reject, promise } = defer(); const [err, username] = await catchAwait(checkToken({ token })); if (err) reject("token过期或失效"); else resolve({ ...data, username }); return promise; }, checkPassword: async (data) => { // 密码校验 const { resolve, reject, promise } = defer(); if (!!!checkBcrypt(data.password, userInfo.password)) reject("密码错误"); else resolve(data); return promise; }, chackUser: async (data) => { // 用户校验 const { resolve, reject, promise } = defer(); if (data.username !== userInfo.username) reject("没找到用户"); else resolve(data); return promise; }, }; exports.initServer = () => { messageCenter.watch("/login", async (data) => { const [err, params] = await catchAwait(middleware.decrypt(data)); const [err2, params2] = await catchAwait(middleware.checkPassword(params)); if (err || err2) return promiseRej(err ?? err2); return { token: createToken({ payload: params2.username }) }; }); messageCenter.watch("/info", async (data) => { const [err, params] = await catchAwait(middleware.decrypt(data)); const [err2, params2] = await catchAwait(middleware.token(params)); const [err3, params3] = await catchAwait(middleware.chackUser(params2)); if (err || err2 || err3) return promiseRej(err ?? err2 ?? err3); return { msg: "获取成功", username: params3.username }; }); };
实现效果
将init函数多执行几次,发现参数均不相同。
那么此时传递相同的参数会发生什么?
可以看到,总共发送了五次请求,只有一次返回了结果,其余的全被中间件阻止
写在最后
感谢你耐心的看到了最后,如果文章对你有帮助还望多多支持,感谢!