高级鉴权验签方式的实践,技术方案为注解+ASCII排序+多类型多层级动态拼接+RSA加密(或国密SM2)+Base64+Redis滑动窗口限流

背景

虽然大多数企业的流量没有那么大,不过限流还是要有的,毕竟还有外部调用我方系统接口,需要验证访问权限进行,同时防止万一接口并发量大影响我方系统, 所以要增加流控处理;不同的来源在独立配置,可以做到不同来源的限流

鉴权设计技术方案:采用注解+ASCII排序+多类型多层级动态拼接+RSA加密(或国密SM2)+一次Base64转码

限流设计:采用Redis的zset滑动窗口限流的方式

建议用国密,SM2比RSA的效率要高,

话不多说,先说方式,后说好处

鉴权设计

定义好一个注解AuthSign,注解中有字段sign

注解处理具体如下

@Around("@annotation(authSign)")
public Object around(ProceedingJoinPoint point, AuthSign authSign) throws Throwable {//获取参数Object[] args = point.getArgs();if (args == null || args.length <= 0) {throw new ParameterException("参数为空");}Map<String, Object> argsMap = new HashMap<>();for (Object obj : args) {//将obj转为map,不转下划线,去空argsMap = BeanUtil.beanToMap(obj, false, true);break;}//获取配置,这个配置可以配置到缓存中Map<String, String> map = checkAndGetBsConfig(argsMap);//是否鉴权,默认鉴权String authFlag = map.getOrDefault(AUTH_FLAG, "true");if (StrUtil.equals(authFlag, "false")) {log.warn("此请求不做鉴权");return point.proceed();}//是否限流,默认不限流if (StrUtil.equals(map.getOrDefault(LIMIT_FLAG, "false"), "true")) {//限流,systemNo作为key,划分不同来源的限流limitValidation(map);}String sign = null;//从注解中取签名,注解没有从参数中取if (StrUtil.isEmpty(authSign.sign())) {if (argsMap.get(SIGN) != null) {sign = argsMap.get(SIGN).toString();}else {throw new BusinessException("签名必传");}} else {sign = parseExpression(authSign.sign(), args, point);}//排除不参与签名的字段,注意:BaseRequest中若添加非前端传入参数需要在此排除!具体排除啥你说的算RSAUtil.execludeField(argsMap);//ASCII排序后拼接一个string,此处就是这个所有动态参数的map经过处理后生成的String sortASCIIStr = SortSignParamUtil.getSortASCIIStr(argsMap);//签名并验证boolean verify = RSAUtil.verify(sortASCIIStr, sign, map.get(PUBLIC_KEY));if (!verify) {throw new BusinessException("签名错误");}Object proceed = point.proceed();return proceed;
}

拿到参数后转为Map(去空处理)

checkAndGetBsConfig方法:验证参数并去bs拿到相关配置参数,有鉴权和限流开关

getSortASCIIStr方法:去掉空字段,只对第一层ASCII排序,通过key=value&形式进行拼接,数组以及List内部使用#拼接,对于每个List或对象内部仍然有List或对象的情况做递归处理;具体如下

/*** 去掉空字段,ASCII排序* 字段支持对象,list,不支持Map(Map用对象表示)* 外层排序,内部对象和list不排序*/
public static String getSortASCIIStr(Map<String, Object> map) throws IllegalAccessException {// 对所有传入参数按照字段名的 ASCII 码从小到大排序(字典序)List<Map.Entry<String, Object>> infoIds = new ArrayList<Map.Entry<String, Object>>(map.entrySet());Collections.sort(infoIds, new Comparator<Map.Entry<String, Object>>() {@Overridepublic int compare(Map.Entry<String, Object> o1, Map.Entry<String, Object> o2) {return (o1.getKey()).compareTo(o2.getKey());}});// 构造签名键值对的格式StringBuilder sb = new StringBuilder();for (Map.Entry<String, Object> item : infoIds) {if (item.getKey() != null || item.getKey() != "") {String key = item.getKey();Object val = item.getValue();if (!(val == "" || val == null)) {if (val instanceof Map) {continue;}//对象if (BeanUtil.isBean(val.getClass())) {StringBuilder objAppend = objAppend(val);sb.append(key + "=" + objAppend.toString() + "&");continue;}//判断list,不支持Map,如果以Map形式直接用对象表示if (val instanceof List) {List<Object> list = (List<Object>) val;StringBuilder listAppend = listAppend(list);sb.append(key + "=" + listAppend.toString() + "&");continue;}//数组 直接拼接if (ArrayUtil.isArray(val)) {//数组 #直接拼接StringBuilder sArray = new StringBuilder();Object[] objects = (Object[]) val;for (Object os : objects) {sArray.append(os + "#");}sb.append(key + "=" + sArray.toString() + "&");continue;}//普通字段sb.append(key + "=" + val + "&");}}}return sb.delete(sb.length() - 1, sb.length()).toString();
}/*** 对象组装*/
private static StringBuilder objAppend(Object obj) throws IllegalAccessException {StringBuilder sb = new StringBuilder();Field[] declaredFields = obj.getClass().getDeclaredFields();for (Field field : declaredFields) {field.setAccessible(true);Object o = field.get(obj);if (o != null) {//对象中还有list和对象if (BeanUtil.isBean(o.getClass())) {sb.append(objAppend(o));continue;}if (o instanceof List) {sb.append(listAppend((List) o));continue;}if (ArrayUtil.isArray(o)) {//数组 #直接拼接Object[] objects = (Object[]) o;for (Object os : objects) {sb.append(os + "#");}continue;}//对象内字段使用#拼接sb.append(field.getName() + "=" + o + "#");}}return sb;
}/*** list组装* 数组,对象*/
private static StringBuilder listAppend(List list) throws IllegalAccessException {StringBuilder s = new StringBuilder();for (Object obj : list) {if (obj != null) {if (BeanUtil.isBean(obj.getClass())) {//每一个对象s.append(objAppend(obj));continue;}//数组s.append(obj + "#");}}return s;
}

支持List,数组,对象,以及普通字段的处理;不支持Map(用对象表示),这里比较麻烦的就是参数如果是多层的情况,大家可以研究一下有没有更好的处理

限流设计

​ 单位时间内允许的请求数:采用Redis的zset滑动窗口限流的方式,具体设计如下

/*** 先根据时间滑动清除过期成员* 判断key的value中的有效访问次数是否超过最大限定值maxCount,若没超过,调用increment方法,将窗口内的访问数加一* 判断与数量增长同步处理** @param key            redis key* @param windowInSecond 窗口间隔,秒* @param maxCount       最大计数* @return 可访问 or 不可访问*/
public boolean canAccess(String key, int windowInSecond, long maxCount) {key = SLIDING_WINDOW + key;long currentMs = System.currentTimeMillis();// 窗口开始时间long windowStartMs = currentMs - windowInSecond * 1000;// 清除窗口过期成员Long aLong = cacheManager.zsetRemoveRangeByScore(NSP, key, 0, windowStartMs);//按key统计集合中的有效数量Long count = cacheManager.zsetZCard(NSP, key);if (count < maxCount) {increment(key, currentMs);return true;} else {log.warn("滑动窗口流控:key:{}, count:{}", key, count);return false;}
}/*** 滑动窗口计数增长** @param key            redis key*/
public void increment(String key, long currentMs) {// 单例模式(提升性能)// 添加当前时间 value=当前时间戳 score=当前时间戳cacheManager.zsetAdd(NSP, key, String.valueOf(currentMs), currentMs, 300);// 设置key过期时间
}

通过缓存或后台配置可以拿到窗口间隔、最大计数,保证在使用过程中可以后台更改限流策略实时生效,动态控制限流

  1. 清除窗口过期成员,zset remove从0到当前窗口开始时间,此时zset中全部是窗口区间的请求数
  2. 获取有效数量进行比较是否溢出
  3. 没有溢出则添加当前时间的记录

好处:

  1. 注解灵活控制哪些接口做鉴权处理,耦合性低,可用性高
  2. BS获取鉴权以及限流配置,实时生效,同时支持多端秘钥配置等分开管理,提高可维护性和相互之间的安全性
  3. ASCII排序+多类型多层级动态拼接:动态组装加密参数,增加复杂度,提高安全性
  4. RSA加密:非对称加密,公钥私钥分开使用,提高安全性,可用SM2替代
  5. 滑动窗口限流:防止限流不均匀,提高限流准确性,提高用户体验

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/599065.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

嵌入式工程师有什么热门的发展方向?

随着5G的普及和物联网时代的到来&#xff0c;各种技术不断融合创新&#xff0c;嵌入式技术已成为互联网行业中具有前景的职业之一。 嵌入式工程师在当今数字化时代拥有广泛的就业前景&#xff0c;新技术的发展也为嵌入式工程师提供了更多机会&#xff0c;给大家列举了四个嵌入…

【JAVA】实验二 类与对象

实验名称 实验二 类与对象 实验目的 1. 深刻理解类的封装与继承&#xff1b; 2. 熟练掌握类的定义、包与路径、对象的创建、方法的调用、类的继承、方法的重写、运行时多态、访问权限修饰符的使用等&#xff1b; 3. 熟练运用JDK提供的常用类及API。 实验内容&…

文件二维码能下载文件吗?扫码看文件效率更高

为了让文件更快的传递&#xff0c;现在将文件制作二维码图片后&#xff0c;让其他人通过扫码查看或者下载文件的方式&#xff0c;被越来越多的人应用。一般想要制作文件二维码&#xff0c;大多会使用文件二维码生成器&#xff08;文件二维码生成器_word、excel、ppt、pdf文档制…

linux下安装Nginx及其常用命令

安装Nginx 接下来在Linux服务器进行操作就可以了 安装插件 yum -y install gcc pcre-devel zlib-devel openssl openssl-devel直接使用wget进行安装(如果没有wget需要先安装wget) yum install wgetwget https://nginx.org/download/nginx-1.24.0.tar.gz解压 tar -zxvf nginx..…

代码训练day59|单调栈part02

参考&#xff1a; 代码随想录 如何高效解决接雨水问题 | labuladong 的算法笔记 503.下一个更大元素II 与下一个更大元素&#xff5c;的区别就是要把数组考虑为环形&#xff08;只有数组内最大值为-1&#xff09; 按照之前的环形为题解决经验&#xff0c;直接拼接两个数组解…

解决mock单元测试中 无法获取实体类xxx对应的表名

错误描述&#xff1a;在执行单元测试时&#xff0c;执行到new Example时抛出异常&#xff0c;提示无法获取实体类xxx对应的表名 Example example new Example(ServeSubscribeRecord.class);Example.Criteria criteria example.createCriteria();criteria.andEqualTo("se…

【Linux】Linux Page Cache页面缓存的原理

Page cache&#xff08;页面缓存&#xff09;是计算机操作系统中的一种机制&#xff0c;用于将频繁访问的数据从磁盘存储到内存中&#xff0c;以便更快地访问。当程序从磁盘请求数据时&#xff0c;操作系统会检查该数据是否已经存在于页面缓存中。如果存在&#xff0c;数据可以…

QT上位机开发(动态库dll的开发)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 有的时候&#xff0c;我们不想把所有的代码都放在一个exe里面&#xff0c;这个时候我们就需要引入dll动态库的概念。在windows平台上面&#xff0c…

IC工程师到底有哪些?每个岗位具体有哪些要求?

随着摩尔定律和技术的发展&#xff0c;芯片集成度也越来越高&#xff0c;与之伴随的就是岗位愈加细分。芯片产业链很长且环环相扣&#xff0c;每一个环节都需要不同的工程师角色分工协作。 很多人以为芯片工程师就是单纯搞芯片的工程师&#xff0c;殊不知这其中可能要分十几个…

FlagData 2.0:全面、高效的大模型训练数据治理工具集

数据是大模型训练至关重要的一环。数据规模、质量、配比&#xff0c;很大程度上决定了最后大模型的性能表现。无论是大规模的预训练数据、精益求精的SFT数据都依托于一个完整的“获取-准备-处理-分析”数据流程。然而&#xff0c;当前的开源工具通常只专注于流程中的某一环节&a…

ThreeJs通过canvas和Sprite添加标签

在3D场景中很多时候会用到给模型添加标签&#xff0c;以表示这个模型的代号&#xff0c;尤其是大量重复模型的时候&#xff0c;添加标签是为了更直观的看到场景中每个模型的数据和名称&#xff0c;比如在仓库中有很多货架&#xff0c;就会需要查看每个货架的编号&#xff0c;如…

Flink Connector 开发

Flink Streaming Connector Flink是新一代流批统一的计算引擎&#xff0c;它需要从不同的第三方存储引擎中把数据读过来&#xff0c;进行处理&#xff0c;然后再写出到另外的存储引擎中。Connector的作用就相当于一个连接器&#xff0c;连接Flink计算引擎跟外界存储系统。Flin…

ES6定义一个类(函数内部定义属性,,原型定义方法 ), 实现继承?

ES6中使用class关键字定义一个类&#xff0c;使用extends关键字实现继承。下面是一个示例&#xff1a; class Animal {constructor(name) {this.name name;}sayHello() {console.log(Hello, my name is ${this.name});} }class Dog extends Animal {constructor(name, breed)…

长亭牧云主机管理助手——免费轻量的服务器管理软件初体验

优点 安装十分简单&#xff0c;新手友好&#xff0c;一行命令搞定界面简洁&#xff0c;操作流畅无需公网 IP&#xff0c;可以面对复杂 NAT 环境进行救急可以统一管理大量主机&#xff0c;无需记住主机秘钥 地址 https://rivers.chaitin.cn/app/collie 安装 安装很简单&…

向下取整和向上取整的定义,各有什么用处。

问题描述&#xff1a;向下取整和向上取整的定义&#xff0c;各有什么用处。 问题解答&#xff1a; 向下取整&#xff08;Floor&#xff09;&#xff1a; 向下取整是指将一个实数向下舍入到最接近的、不超过它的整数。用数学符号表示为 ⌊x⌋&#xff0c;其中 x 是实数。例如&am…

基于 listmonk 的电子邮件营销解决方案

背景 电子邮件营销&#xff08;EDM&#xff09;在广告、电商、供应链物流等行业应用广泛&#xff0c;亚马逊云科技的市场部门持续不断的收到客户反馈&#xff0c;希望可以提供简单便捷的方案。 亚马逊云科技产品体验链接&#xff1a;点击我立即体验 对于发送邮件的需求&…

深耕汽车检测设备领域,引领行业技术革新

在汽车工业飞速发展的今天&#xff0c;汽车检测技术作为保障车辆安全、提升维修效率的重要手段&#xff0c;日益受到行业内外的高度关注。康士柏汽车检测线设备厂家&#xff0c;作为这一领域的佼佼者&#xff0c;凭借其深厚的技术积累和卓越的产品品质&#xff0c;正引领着行业…

c# 学习笔记 - 委托(Delegate)

文章目录 1. 委托1.1 委托概述1.2 委托使用1.3 委托的传播 2. 匿名方法2.1 匿名方法概述2.2 匿名方法 1. 委托 1.1 委托概述 委托简介 委托就是对方法的引用&#xff0c;可以理解为例如整型变量的容器可以存储整形数据&#xff0c;委托就是某种方法的容器&#xff0c;可以用来…

Future、CompletionService、CompletableFuture介绍与对比

目录 Future1、基本介绍2、按照提交任务的顺序获取执行结果 CompletionService1、介绍2、按照任务完成的先后顺序获取结果 CompletableFuture1、介绍2、CompletableFuture怎么非阻塞的获取任务结果 Future 1、基本介绍 Future是JDK1.5 提供的接口&#xff0c;是用来以阻塞的方…