¶异常处理的一头一尾
在使用网关进行微服务管理的工程中,我们编写了包含鉴权、限流的解决方案,加在访问到业务代码API之前,下面进入流程:
SpringCloud 的异常处理不同于SpringMVC或者SpringBoot下的全局异常处理,因为底层的处理器不同,具体代码层表现为继承的类不同,SpringCloud需要继承DefaultErrorWebExceptionHandler进行异常处理,,SpringMVC和SpringBoot通过@ControllerAdvice和
@ExceptionHandler处理不同的自定义异常处理逻辑。
¶全局异常处理:
Order:@Order(Ordered.HIGHEST_PRECEDENCE)
进入全局异常处理类ErrorHandlerConfiguration
1 |
|
该类在容器初始化的时候首先加载,后面的加载顺序会在Order上有体现,在errorWebExceptionHandler里面实现自定义的ExceptionHandler,在经过了所有的过滤器之后,如果不在白名单(表示此类访问直接通过,不需要进行鉴权,会在下面讲到用户鉴权),异常返回自定义如下:
1 | public class ExceptionHandler extends DefaultErrorWebExceptionHandler { |
¶返回值包装
在正常返回之前结果之前,需要对用户访问总数等进行更新,调用ResultFilter对返回结果进行封装:
1 |
|
¶filter过滤请求
¶用户权限的过滤器
Order:Integer.MIN_VALUE
一头一尾简单说完,那中间经历了哪些filter的处理过程呢,根据bean的Order加载顺序,下一个我们进行的是用户的验证AuthFilter,Order:Integer.MIN_VALUE:
1 |
|
接口是否启用,通过Key值RedisPath.ALL_URL从Redis里面获取所有已经存在的API,然后对1、token是否带入参数;2、token是否为空进行判断,如果都通过,通过userId在Redis里面查找用户限制鉴权对象UserLimitEntity,UserLimitEntity和RedisPath.ALL_URL的获取都存在从MySQL缓存至Redis的过程,在下面的类InitHandler进行加载。
Order:1
1 |
|
可以看出类InitHandler中方法initUrl()初始化了所有的存在的urls(即已经存在于数据库中的API),缓存至Redis,我们看看Mysql视图:
Redis视图(只缓存url的Id):
initEnum()方法初始化返回值错误类型的枚举类加载,initUrlLimit()初始化url的令牌桶,至于令牌什么时候用,我们后面说。
¶用户访问速度的过滤器
Order:Integer.MIN_VALUE+1
下面进入用户访问速度的过滤器,这里用到了redis+Lua的控制逻辑,直接上代码:
1 |
|
类UserRateFilte,如果不在类AuthFilter过滤器中的白名单,进入自定义的MyFilter noPassFilter()进入过滤器逻辑,其中UserLimitEntity,在上面提到的类InitHandler中,已经由用户管理系统(不用于此网关系统)Mysql缓存到Redis中,如图:
其中字段USED(已访问次数),TOTAL(总访问次数),RATE(访问速率),如:{60:10,3600:300}表示两个限制条件,此用户60秒最多可累计可访问10次,3600秒最多累计可访问300次,这个二元组个数可以根据需求随意添加。其中方法acquire(),将对用户限制的次数进行判断,并返回是否通过过滤,这里会由userRateClient加载user_rate_limit.lua对用户是否能够获取访问权限进行判断。先看看类userRateClient:
1 |
|
UserRateClient里面的exec(path, UserRateLimitMethod.init, intervalSb.toString(), countSb.toString(), String.valueOf(max), String.valueOf(limit.size())) 方法,intervalSb和countSb都封装成{毫秒数:可访问数}这种多个二元组的形式,@Qualifier(“userRateLua”)通过Spring Configuration加载lua脚本user_rate_limit.lua:
1 | redis.replicate_commands() |
如果不懂lua脚本语法,可以去网上先了解一下,作为redis分布式锁原子性实现的利器(redis官方推出的解决方案),在进行redis分布式锁设计的时候也可以大展拳脚,实际顺手程度非常推荐。说远了,回来到上面的lua脚本。init方法初始化用户限流的各项参数,interval-时间间隔,max_count-最大访问次数,first_time-第一次访问时间,count-置0,当然是实际是{0}样子,上面做了字符串的拼接,这是lua的数组包装形式,在acquire方法里面如果redis里面没有进行初始化的话就进行初始化,在Redis里面的视图是:
acquire发现,如果当前时间减去第一次时间大于时间间隔,用户已使用次数count归0,并把curr_time(当前时间)赋值给first_time(第一次时间),如果在这个时间间隔内,使用次数count+1,如果访问次数大于最大访问次数,返回Result.LIMITED,如果返回Result.SUCCESS,通过类UserRateFilter的过滤条件,进入下一个过滤器。
¶接口访问速度的过滤器
Order:Integer.MIN_VALUE+2
类UrlRateFilter是对接口访问速度的控制,代码如下:
1 |
|
调用TokenBucketClient进行控制逻辑,此处的逻辑和上面的用户访问控制有点区别,这里面的初始化,即令牌的初始化在类initHandler里面的initUrlLimit()方法已经初始化了,缓存至Redis如图:
里面参数表示:bucket_max_size(令牌桶里面最大令牌数),interval(产生令牌的时间间隔),token(token数),更新时间(timestamp)。关于令牌桶的概念,不清楚的话,可先查看令牌桶概念、实现例子。
1 |
|
控制令牌产生的lua脚本如下:
1 | redis.replicate_commands() |
acquire方法里面 acquire_token传值是1(即每次访问消耗令牌数1),更新时间差与interval比值产生对应令牌数,如果桶里面令牌大于acquire_token,则放行,返回Result.SUCCESS,完成接口访问速率的控制。
¶用户总访问次数控制过滤器
Order:Integer.MIN_VALUE+3
1 |
|
UserLimitEntity用户控制里面有个total,表示用户可以访问最大次数,上面过滤器进行访问值的判断,如果大于,返回FailureResult.valueOf(“LIMITED_COUNT”),错误结果通过枚举形式把配置文件的自定义的错误码(下图),在前文提到的InitHandler类中进行加载。
FailureResult.properties:
¶通过网关加载访问业务层
经过上面过滤器,我们完成了对用户的鉴权和限流的设计和控制,之后正式进行业务层逻辑访问,本文完。
Gitlab:项目地址