高级鉴权验签方式的实践,技术方案为注解+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

相关文章

Java调用DeepSeek API的最佳实践及详细代码示例

《Java调用DeepSeekAPI的最佳实践及详细代码示例》:本文主要介绍如何使用Java调用DeepSeekAPI,包括获取API密钥、添加HTTP客户端依赖、创建HTTP请求、处理响应、... 目录1. 获取API密钥2. 添加HTTP客户端依赖3. 创建HTTP请求4. 处理响应5. 错误处理6.

Android 悬浮窗开发示例((动态权限请求 | 前台服务和通知 | 悬浮窗创建 )

《Android悬浮窗开发示例((动态权限请求|前台服务和通知|悬浮窗创建)》本文介绍了Android悬浮窗的实现效果,包括动态权限请求、前台服务和通知的使用,悬浮窗权限需要动态申请并引导... 目录一、悬浮窗 动态权限请求1、动态请求权限2、悬浮窗权限说明3、检查动态权限4、申请动态权限5、权限设置完毕后

在 Spring Boot 中使用 @Autowired和 @Bean注解的示例详解

《在SpringBoot中使用@Autowired和@Bean注解的示例详解》本文通过一个示例演示了如何在SpringBoot中使用@Autowired和@Bean注解进行依赖注入和Bean... 目录在 Spring Boot 中使用 @Autowired 和 @Bean 注解示例背景1. 定义 Stud

golang内存对齐的项目实践

《golang内存对齐的项目实践》本文主要介绍了golang内存对齐的项目实践,内存对齐不仅有助于提高内存访问效率,还确保了与硬件接口的兼容性,是Go语言编程中不可忽视的重要优化手段,下面就来介绍一下... 目录一、结构体中的字段顺序与内存对齐二、内存对齐的原理与规则三、调整结构体字段顺序优化内存对齐四、内

Python如何计算两个不同类型列表的相似度

《Python如何计算两个不同类型列表的相似度》在编程中,经常需要比较两个列表的相似度,尤其是当这两个列表包含不同类型的元素时,下面小编就来讲讲如何使用Python计算两个不同类型列表的相似度吧... 目录摘要引言数字类型相似度欧几里得距离曼哈顿距离字符串类型相似度Levenshtein距离Jaccard相

redis群集简单部署过程

《redis群集简单部署过程》文章介绍了Redis,一个高性能的键值存储系统,其支持多种数据结构和命令,它还讨论了Redis的服务器端架构、数据存储和获取、协议和命令、高可用性方案、缓存机制以及监控和... 目录Redis介绍1. 基本概念2. 服务器端3. 存储和获取数据4. 协议和命令5. 高可用性6.

Go语言中三种容器类型的数据结构详解

《Go语言中三种容器类型的数据结构详解》在Go语言中,有三种主要的容器类型用于存储和操作集合数据:本文主要介绍三者的使用与区别,感兴趣的小伙伴可以跟随小编一起学习一下... 目录基本概念1. 数组(Array)2. 切片(Slice)3. 映射(Map)对比总结注意事项基本概念在 Go 语言中,有三种主要

Spring排序机制之接口与注解的使用方法

《Spring排序机制之接口与注解的使用方法》本文介绍了Spring中多种排序机制,包括Ordered接口、PriorityOrdered接口、@Order注解和@Priority注解,提供了详细示例... 目录一、Spring 排序的需求场景二、Spring 中的排序机制1、Ordered 接口2、Pri

Idea实现接口的方法上无法添加@Override注解的解决方案

《Idea实现接口的方法上无法添加@Override注解的解决方案》文章介绍了在IDEA中实现接口方法时无法添加@Override注解的问题及其解决方法,主要步骤包括更改项目结构中的Languagel... 目录Idea实现接China编程口的方法上无法添加@javascriptOverride注解错误原因解决方

Redis的数据过期策略和数据淘汰策略

《Redis的数据过期策略和数据淘汰策略》本文主要介绍了Redis的数据过期策略和数据淘汰策略,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一... 目录一、数据过期策略1、惰性删除2、定期删除二、数据淘汰策略1、数据淘汰策略概念2、8种数据淘汰策略