为了校验个权限,我返工了两次

144 阅读6分钟

最近公司来了一个需求,是要在接口里添加鉴权逻辑,并且根据不同的接口,获取相应的参数去鉴别读或写权限,leader说完成之后有奖励,我当时一听就来劲了,不管三七二十一就立马接下了这个活,心想,在每个接口里去添加相应逻辑不就可以了,于是马上写完就去找leader交货去了,具体代码类似下面:

image.png

在每个需要鉴权的接口里加了相应逻辑。

我兴高采烈地去找leader交工,但是狠狠的被打了回来,要我去重新设计,说一点也不优雅。看来心急是真的吃不了热豆腐啊。

这种方案让鉴权逻辑和我们的业务代码逻辑耦合在一起,维护起来很不方便。于是我想到了过滤器filter,实现filter接口,重写doFilter方法即可。

并且为了方便维护,我还接入了配置中心,去动态地配置哪些接口鉴别读权限还是写权限,代码类似如下: image.png 我们在过滤器中主要干这几件事:

1.拿到当前登陆人id

2.拿到url判断是否需要鉴权

3.拿到请求中的参数

前两点都非常容易,就是去header里面拿userId,以及拿到请求url去看看map里的值,在写第三件事的时候我也以为会非常顺利,但是事实却出乎意料:

拿到请求中的参数,如果请求是get方法那么很简单就能从请求中拿到相应的参数, 通过request.getParameter就可以但是由于post方法请求参数是放在body里的,方法也就是去读取request 的inputStream, 于是:

image.png 这样就拿到body,然后转换成map不就可以得到我想要的参数了,但是在我调试的时候却发现我想的太简单了。 在我运行时出现了下面的异常

image.png

what? 请求体is missing?我只是读了一下body,也没有去更新它呀,怎么会missing了?

于是我去网上寻找了大量资料,发现request的inputstream只能够读取一次,在读取时有一个记录读到哪的偏移量,当我们读完之后这个偏移量并没有复原,于是后面再去读的时候就出现了body丢失的异常,具体原因可参考如下链接:

blog.csdn.net/qq_40233503…

解决方案就是对request进行包装,先将body读取出来并记录下来,然后重写getInputStream方法,给他重新返回一个新的流就可以了:

image.png

后面就可以愉快的根据配置的url和参数,去鉴别读/写权限了。

我自认为已经完美无缺了,跑去找leader,但是leader说:如果新增一个接口,我还需要去配置中心配置?他接受不了,又要我去优化。leader说的非常有道理,我也没有办法,谁让他是leader呢。

于是我就致力于去解决掉手动配置的问题。然后我就想到了在项目启动时去扫描所有的controller,拿到相应的requestMapping里面配置的url,之后再自定义一个注解,用来标识是鉴别读权限还是写权限还是跳过,对于增量接口且忘记标注解的,我默认采用鉴别写权限。

image.png

applicationContext.getBeansWithAnnotation(Controller.class);

这段代码会将标注@Controller和@RestController的controller都拿到。 然后去处理类中的每个方法。

image.png

注意普通的

declaredMethod.getAnnotation(RequestMapping.class)

是拿不到GetMapping和PostMapping的,只能获取到@RequestMapping,所以需要用上图所示的方法

RequestMapping methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(declaredMethod, RequestMapping.class);

如果这个为null就说明这个方法不是接口,跳过就可以了。

然后去拿到这个方法是鉴别什么权限

image.png 先去方法上拿,拿不到再去类上拿,都没有就默认写权限。

剩下的就好办了,只需要拿到类上的@RequestMapping和方法上的@RequestMapping配置的url,之后进行拼接不就行了,但是当我看到某些接口时,我收回了那句话。因为每个人写的方式都不一样

类上的注解有的人这样写

@RequestMapping("/api")

有人这样写

@RequestMapping("/api/")

还有人只写一个字母

这就不只是简单拼接这么简单了,我脑子里顿时想到了很复杂的拼接逻辑,感觉要花大时间了。

好在Spring还是给我们提供了相应的工具的。 我们可以通过下面的方式来拿到真正的url

image.png

image.png

经历了两天之后我又拿着这一版的代码去找我的leader,虽然这个小需求我写了好多天,但他还是如约给我了奖励,那就是带全组 人去团建吃烤全羊...我呵呵一笑。

最后简单总结一下这三种方案吧:

方案一:在每个需要鉴权的方法里拿到对应参数去校验对应权限.

  • 优点: 实现简单

  • 缺点: 每个地方都需要添加重复性的代码。并且鉴权和业务代码耦合

方案二:统一在filter中进行鉴权, 在配置中心中配置接口校验权限类型, 请求时获取url根据配置去校验相关权限

  • 优点: 鉴权逻辑和业务代码解耦, 减少代码量. 

  • 缺点: 需要手动去配置, 并且每增加一个接口就需要去配置

  • 难点: post请求需要去请求body中去拿到参数, todo深挖原理.

  • 注意:如果没有权限的话不能直接抛异常,这样前端会返回status=500,需要自定义response

方案三: 通过自定义注解,注解有属性,通过属性判断是鉴权读还是写,在项目启动的时候扫描全部controller里面的@AccessAuth注解, 然后将标有此注解的方法所对应的请求路径以及读/写权限记录下来,替代方案二中的手动配置读/写权限。

  • 注意:在标注请求路径时,不能只进行简单的拼接,因为类上的RequestMapping和方法上的GetMapping可能“/”会重复或丢失

使用UriComponentsBuilder.fromPath(prefix).path(url).build().getPath();可以得到realUrl

  • 缺点:实现复杂。 其实还有一种方案就是通过写自定义注解的切面来实现,但是这种方案对于那些忘记标注解的接口来说不太友好,不过也是可以实现的,后续准备去实现一下,顺便补足一下在AOP这方面的短板。

还有其他方案的朋友或者对上面方案有建议的可以在评论区讨论一下