JS案例:接口加解密与防重放

「目录」

前言[1]

功能设计[2]

客户端的功能点(client)[3]

服务端的功能点(server)[4]

功能实现[5]

工具函数[6]

client.js(客户端)[7]

server.js(服务端)[8]

实现效果[9]

写在最后[10]


前言

「在网络通信中,如果数据包是明文传输,并且包含敏感信息,那么就很容易被抓包窃取,因此加密手段也成了开发者耳熟能详的知识技能;常见的加密方法有对称加密和非对称加密。对称加密使用同一个密钥进行加密和解密,而非对称加密使用公钥和私钥分别进行加密和解密。」

「另一个需要知识点是防重放措施,防重放攻击是指攻击者会拦截请求并重新发送,从而导致重复处理。常见的防重放攻击方法有使用令牌桶算法和使用 Nonce 值(随机数)。」

「那么这二者为何会结合在一起呢?」

「原因是我之前做的零食商贩[11]的案例暴露出来的问题,虽然接口做了加密处理,使用者不容易轻易知道数据包的内容,但是如果复制一个接口再次发起请求还是可以成功,因为接口没有做类似文件阅后即焚的功能,所以做个分享。」

功能设计

「因为客户端和服务端都在 node 中实现,所以通信暂时摒弃请求的方式,使用消息中心[12]模拟前后端请求的操作」

「客户端的功能点(client)」

  • 「通过 invoke 发送请求」
  • 「创建 Nonce 随机值」
  • 「crypto.aes 加密参数」

「服务端的功能点(server)」

  • 「通过 watch 接收请求」
  • 「Nonce 查重」
  • 「crypto.aes 解密参数」
  • 「通过 bcryptjs 哈希处理对比密码是否正确」
  • 「jsonwebtoken 创建及校验 token」
JS案例:接口加解密与防重放

「功能实现」

工具函数

「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;
  },
  tokenasync ({ 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;
  },
  checkPasswordasync (data) => {
    // 密码校验
    const { resolve, reject, promise } = defer();
    if (!!!checkBcrypt(data.password, userInfo.password)) reject("密码错误");
    else resolve(data);
    return promise;
  },
  chackUserasync (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 };
  });
};

实现效果

JS案例:接口加解密与防重放

「将 init 函数多执行几次,发现参数均不相同。」

「那么此时传递相同的参数会发生什么?」

JS案例:接口加解密与防重放

「可以看到,总共发送了五次请求,只有一次返回了结果,其余的全被中间件阻止」

「写在最后」

「感谢你耐心的看到了最后,如果文章对你有帮助还望多多支持,感谢!」

「源码:myCode: 基于 js 的一些小案例或者项目Gitee.com[13]

Reference

[1]

前言: #%E5%89%8D%E8%A8%80

[2]

功能设计: #%E5%8A%9F%E8%83%BD%E8%AE%BE%E8%AE%A1

[3]

客户端的功能点(client): #%E5%AE%A2%E6%88%B7%E7%AB%AF%E7%9A%84%E5%8A%9F%E8%83%BD%E7%82%B9%EF%BC%88client%EF%BC%89

[4]

服务端的功能点(server): #%E6%9C%8D%E5%8A%A1%E7%AB%AF%E7%9A%84%E5%8A%9F%E8%83%BD%E7%82%B9%EF%BC%88server%EF%BC%89

[5]

功能实现: #%E5%8A%9F%E8%83%BD%E5%AE%9E%E7%8E%B0

[6]

工具函数: #%E5%B7%A5%E5%85%B7%E5%87%BD%E6%95%B0

[7]

client.js(客户端): #client.js%EF%BC%88%E5%AE%A2%E6%88%B7%E7%AB%AF%EF%BC%89

[8]

server.js(服务端): #server.js%EF%BC%88%E6%9C%8D%E5%8A%A1%E7%AB%AF%EF%BC%89

[9]

实现效果: #%E5%AE%9E%E7%8E%B0%E6%95%88%E6%9E%9C

[10]

写在最后: #%E5%86%99%E5%9C%A8%E6%9C%80%E5%90%8E

[11]

零食商贩: https://gitee.com/DieHunter/myCode/tree/master/shopping

[12]

消息中心: https://gitee.com/DieHunter/message-center

[13]

myCode: 基于js的一些小案例或者项目 – Gitee.com: https://gitee.com/DieHunter/myCode/tree/master/Node%E6%8E%A5%E5%8F%A3%E5%8A%A0%E5%AF%86%E8%A7%A3%E5%AF%86


原文始发于微信公众号(阿宇的编程之旅):JS案例:接口加解密与防重放

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/247055.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!