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

本文主要是介绍高级鉴权验签方式的实践,技术方案为注解+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. 滑动窗口限流:防止限流不均匀,提高限流准确性,提高用户体验

这篇关于高级鉴权验签方式的实践,技术方案为注解+ASCII排序+多类型多层级动态拼接+RSA加密(或国密SM2)+Base64+Redis滑动窗口限流的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/574466

相关文章

SpringBoot首笔交易慢问题排查与优化方案

《SpringBoot首笔交易慢问题排查与优化方案》在我们的微服务项目中,遇到这样的问题:应用启动后,第一笔交易响应耗时高达4、5秒,而后续请求均能在毫秒级完成,这不仅触发监控告警,也极大影响了用户体... 目录问题背景排查步骤1. 日志分析2. 性能工具定位优化方案:提前预热各种资源1. Flowable

Oracle数据库常见字段类型大全以及超详细解析

《Oracle数据库常见字段类型大全以及超详细解析》在Oracle数据库中查询特定表的字段个数通常需要使用SQL语句来完成,:本文主要介绍Oracle数据库常见字段类型大全以及超详细解析,文中通过... 目录前言一、字符类型(Character)1、CHAR:定长字符数据类型2、VARCHAR2:变长字符数

前端高级CSS用法示例详解

《前端高级CSS用法示例详解》在前端开发中,CSS(层叠样式表)不仅是用来控制网页的外观和布局,更是实现复杂交互和动态效果的关键技术之一,随着前端技术的不断发展,CSS的用法也日益丰富和高级,本文将深... 前端高级css用法在前端开发中,CSS(层叠样式表)不仅是用来控制网页的外观和布局,更是实现复杂交

Python使用自带的base64库进行base64编码和解码

《Python使用自带的base64库进行base64编码和解码》在Python中,处理数据的编码和解码是数据传输和存储中非常普遍的需求,其中,Base64是一种常用的编码方案,本文我将详细介绍如何使... 目录引言使用python的base64库进行编码和解码编码函数解码函数Base64编码的应用场景注意

C#如何动态创建Label,及动态label事件

《C#如何动态创建Label,及动态label事件》:本文主要介绍C#如何动态创建Label,及动态label事件,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录C#如何动态创建Label,及动态label事件第一点:switch中的生成我们的label事件接着,

SpringCloud动态配置注解@RefreshScope与@Component的深度解析

《SpringCloud动态配置注解@RefreshScope与@Component的深度解析》在现代微服务架构中,动态配置管理是一个关键需求,本文将为大家介绍SpringCloud中相关的注解@Re... 目录引言1. @RefreshScope 的作用与原理1.1 什么是 @RefreshScope1.

MyBatis 动态 SQL 优化之标签的实战与技巧(常见用法)

《MyBatis动态SQL优化之标签的实战与技巧(常见用法)》本文通过详细的示例和实际应用场景,介绍了如何有效利用这些标签来优化MyBatis配置,提升开发效率,确保SQL的高效执行和安全性,感... 目录动态SQL详解一、动态SQL的核心概念1.1 什么是动态SQL?1.2 动态SQL的优点1.3 动态S

Spring Boot 配置文件之类型、加载顺序与最佳实践记录

《SpringBoot配置文件之类型、加载顺序与最佳实践记录》SpringBoot的配置文件是灵活且强大的工具,通过合理的配置管理,可以让应用开发和部署更加高效,无论是简单的属性配置,还是复杂... 目录Spring Boot 配置文件详解一、Spring Boot 配置文件类型1.1 applicatio

Java进行文件格式校验的方案详解

《Java进行文件格式校验的方案详解》这篇文章主要为大家详细介绍了Java中进行文件格式校验的相关方案,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录一、背景异常现象原因排查用户的无心之过二、解决方案Magandroidic Number判断主流检测库对比Tika的使用区分zip

Spring事务中@Transactional注解不生效的原因分析与解决

《Spring事务中@Transactional注解不生效的原因分析与解决》在Spring框架中,@Transactional注解是管理数据库事务的核心方式,本文将深入分析事务自调用的底层原理,解释为... 目录1. 引言2. 事务自调用问题重现2.1 示例代码2.2 问题现象3. 为什么事务自调用会失效3