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

相关文章

Redis连接失败:客户端IP不在白名单中的问题分析与解决方案

《Redis连接失败:客户端IP不在白名单中的问题分析与解决方案》在现代分布式系统中,Redis作为一种高性能的内存数据库,被广泛应用于缓存、消息队列、会话存储等场景,然而,在实际使用过程中,我们可能... 目录一、问题背景二、错误分析1. 错误信息解读2. 根本原因三、解决方案1. 将客户端IP添加到Re

详谈redis跟数据库的数据同步问题

《详谈redis跟数据库的数据同步问题》文章讨论了在Redis和数据库数据一致性问题上的解决方案,主要比较了先更新Redis缓存再更新数据库和先更新数据库再更新Redis缓存两种方案,文章指出,删除R... 目录一、Redis 数据库数据一致性的解决方案1.1、更新Redis缓存、删除Redis缓存的区别二

Redis与缓存解读

《Redis与缓存解读》文章介绍了Redis作为缓存层的优势和缺点,并分析了六种缓存更新策略,包括超时剔除、先删缓存再更新数据库、旁路缓存、先更新数据库再删缓存、先更新数据库再更新缓存、读写穿透和异步... 目录缓存缓存优缺点缓存更新策略超时剔除先删缓存再更新数据库旁路缓存(先更新数据库,再删缓存)先更新数

Redis事务与数据持久化方式

《Redis事务与数据持久化方式》该文档主要介绍了Redis事务和持久化机制,事务通过将多个命令打包执行,而持久化则通过快照(RDB)和追加式文件(AOF)两种方式将内存数据保存到磁盘,以防止数据丢失... 目录一、Redis 事务1.1 事务本质1.2 数据库事务与redis事务1.2.1 数据库事务1.

mac安装redis全过程

《mac安装redis全过程》文章内容主要介绍了如何从官网下载指定版本的Redis,以及如何在自定义目录下安装和启动Redis,还提到了如何修改Redis的密码和配置文件,以及使用RedisInsig... 目录MAC安装Redis安装启动redis 配置redis 常用命令总结mac安装redis官网下

Java中的密码加密方式

《Java中的密码加密方式》文章介绍了Java中使用MD5算法对密码进行加密的方法,以及如何通过加盐和多重加密来提高密码的安全性,MD5是一种不可逆的哈希算法,适合用于存储密码,因为其输出的摘要长度固... 目录Java的密码加密方式密码加密一般的应用方式是总结Java的密码加密方式密码加密【这里采用的

Mysql 中的多表连接和连接类型详解

《Mysql中的多表连接和连接类型详解》这篇文章详细介绍了MySQL中的多表连接及其各种类型,包括内连接、左连接、右连接、全外连接、自连接和交叉连接,通过这些连接方式,可以将分散在不同表中的相关数据... 目录什么是多表连接?1. 内连接(INNER JOIN)2. 左连接(LEFT JOIN 或 LEFT

关于Java内存访问重排序的研究

《关于Java内存访问重排序的研究》文章主要介绍了重排序现象及其在多线程编程中的影响,包括内存可见性问题和Java内存模型中对重排序的规则... 目录什么是重排序重排序图解重排序实验as-if-serial语义内存访问重排序与内存可见性内存访问重排序与Java内存模型重排序示意表内存屏障内存屏障示意表Int

Redis主从复制实现原理分析

《Redis主从复制实现原理分析》Redis主从复制通过Sync和CommandPropagate阶段实现数据同步,2.8版本后引入Psync指令,根据复制偏移量进行全量或部分同步,优化了数据传输效率... 目录Redis主DodMIK从复制实现原理实现原理Psync: 2.8版本后总结Redis主从复制实

SpringBoot使用注解集成Redis缓存的示例代码

《SpringBoot使用注解集成Redis缓存的示例代码》:本文主要介绍在SpringBoot中使用注解集成Redis缓存的步骤,包括添加依赖、创建相关配置类、需要缓存数据的类(Tes... 目录一、创建 Caching 配置类二、创建需要缓存数据的类三、测试方法Spring Boot 熟悉后,集成一个外