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

相关文章

Python中pywin32 常用窗口操作的实现

《Python中pywin32常用窗口操作的实现》本文主要介绍了Python中pywin32常用窗口操作的实现,pywin32主要的作用是供Python开发者快速调用WindowsAPI的一个... 目录获取窗口句柄获取最前端窗口句柄获取指定坐标处的窗口根据窗口的完整标题匹配获取句柄根据窗口的类别匹配获取句

Java 中的 @SneakyThrows 注解使用方法(简化异常处理的利与弊)

《Java中的@SneakyThrows注解使用方法(简化异常处理的利与弊)》为了简化异常处理,Lombok提供了一个强大的注解@SneakyThrows,本文将详细介绍@SneakyThro... 目录1. @SneakyThrows 简介 1.1 什么是 Lombok?2. @SneakyThrows

在 Spring Boot 中实现异常处理最佳实践

《在SpringBoot中实现异常处理最佳实践》本文介绍如何在SpringBoot中实现异常处理,涵盖核心概念、实现方法、与先前查询的集成、性能分析、常见问题和最佳实践,感兴趣的朋友一起看看吧... 目录一、Spring Boot 异常处理的背景与核心概念1.1 为什么需要异常处理?1.2 Spring B

Redis 热 key 和大 key 问题小结

《Redis热key和大key问题小结》:本文主要介绍Redis热key和大key问题小结,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录一、什么是 Redis 热 key?热 key(Hot Key)定义: 热 key 常见表现:热 key 的风险:二、

利用python实现对excel文件进行加密

《利用python实现对excel文件进行加密》由于文件内容的私密性,需要对Excel文件进行加密,保护文件以免给第三方看到,本文将以Python语言为例,和大家讲讲如何对Excel文件进行加密,感兴... 目录前言方法一:使用pywin32库(仅限Windows)方法二:使用msoffcrypto-too

Java Spring 中 @PostConstruct 注解使用原理及常见场景

《JavaSpring中@PostConstruct注解使用原理及常见场景》在JavaSpring中,@PostConstruct注解是一个非常实用的功能,它允许开发者在Spring容器完全初... 目录一、@PostConstruct 注解概述二、@PostConstruct 注解的基本使用2.1 基本代

C#使用StackExchange.Redis实现分布式锁的两种方式介绍

《C#使用StackExchange.Redis实现分布式锁的两种方式介绍》分布式锁在集群的架构中发挥着重要的作用,:本文主要介绍C#使用StackExchange.Redis实现分布式锁的... 目录自定义分布式锁获取锁释放锁自动续期StackExchange.Redis分布式锁获取锁释放锁自动续期分布式

springboot使用Scheduling实现动态增删启停定时任务教程

《springboot使用Scheduling实现动态增删启停定时任务教程》:本文主要介绍springboot使用Scheduling实现动态增删启停定时任务教程,具有很好的参考价值,希望对大家有... 目录1、配置定时任务需要的线程池2、创建ScheduledFuture的包装类3、注册定时任务,增加、删

SpringBoot基于配置实现短信服务策略的动态切换

《SpringBoot基于配置实现短信服务策略的动态切换》这篇文章主要为大家详细介绍了SpringBoot在接入多个短信服务商(如阿里云、腾讯云、华为云)后,如何根据配置或环境切换使用不同的服务商,需... 目录目标功能示例配置(application.yml)配置类绑定短信发送策略接口示例:阿里云 & 腾

Spring Boot 整合 SSE的高级实践(Server-Sent Events)

《SpringBoot整合SSE的高级实践(Server-SentEvents)》SSE(Server-SentEvents)是一种基于HTTP协议的单向通信机制,允许服务器向浏览器持续发送实... 目录1、简述2、Spring Boot 中的SSE实现2.1 添加依赖2.2 实现后端接口2.3 配置超时时