1. 需求背景
用过Shiro
的小伙伴都知道,shiro
提供两种权限控制方式,通过过滤器
或注解
。我们项目是springboot + vue
前后分离项目,后台对于权限控制一直使用的是过滤器
的方式,并且还有自定义的过滤器
。大概如下:
@Bean("shiroFilter")
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
//过滤规则设置
Map<String, Filter> filters = new HashMap<>();
filters.put("shiro", new ShiroAuthenticatingFilter());
filters.put("user", new UserAuthcFilter());
shiroFilter.setFilters(filters);
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/captcha.jpg", "anon");
filterMap.put("/security/login", "anon");
filterMap.put("/getPublicKey", "anon");
filterMap.put("/user/logout", "anon");
filterMap.put("/user/queryByToken", "shiro");
filterMap.put("/**", "user");
shiroFilter.setFilterChainDefinitionMap(filterMap);
retrun shiroFilter;
}
如上图所示,我们自定义了两种过滤器shiro
、user
。shiro
过滤器用来登录时打通Shiro并存储身份
;user
过滤器用来校验剩余所有接口是否处于登录状态
。
当我们需要放开接口时,就像上图一样,配置多个anon
。但是由于当前Shiro
配置文件也算是项目中的一个主配置文件,总是让开发不断修改这个文件。
对于一个严谨的猴子
来说,这种事儿不能够发生。应该严格遵循开闭原则
的设计,对扩展开放、对修改关闭
。应该将所有需要修改的拿到外边,当前配置文件纳入到系统jar包里,只允许引用。我想要的效果如下:
2. 解决思路
因为从我们变化的地方来看,其实经常变化的就是增删那些不需要登录即可访问的接口
。
既然Shiro
自己提供注解,那可以通过过滤器+注解
的方式来解决,上图的配置就改成下边这样:
@Bean("shiroFilter")
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
//过滤规则设置
Map<String, Filter> filters = new HashMap<>();
filters.put("shiro", new ShiroAuthenticatingFilter());
filters.put("user", new UserAuthcFilter());
shiroFilter.setFilters(filters);
Map<String, String> filterMap = new LinkedHashMap<>();
/******只是将所有的anon提取出来,不再修改这里*******/
filterMap.put("/user/queryByToken", "shiro");
filterMap.put("/**", "user");
shiroFilter.setFilterChainDefinitionMap(filterMap);
retrun shiroFilter;
}
然后再利用Shiro
自带的注解@RequiresGuest
,想哪个接口放开,我就把这个注解加到那个Controller
对应的放开接口上即可,如下:
/**
* 获取学生详细信息
*/
@RequiresGuest
@PostMapping(value = "/get")
public Result getInfo(@RequestBody @Validated(SelectOne.class) DemoGradeStudentModel model) {
return Result.data(demoGradeStudentService.getById(model.getId()));
}
因为我们需要放开的接口数量远远少于需要拦截的接口,因此通过控制配置放开的注解来实现这样的功能,是最好的方式。
思路是不是很简单,对,我也是三下五除二就这么配置完了,然而事实打脸了,并不管用。
经过网上搜索,全都是需要添加以下注解:
@Bean({"lifecycleBeanPostProcessor"})
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
proxyCreator.setProxyTargetClass(true);
return proxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
然而并不管用。
3. 原因追踪
3.1 排查拦截过滤器逻辑
通过debug
来看,因为我的所有剩余拦截的方式,都基于自定义过滤器user
,大家可能有的是自定义,有的是基于Shiro
自带的authc
过滤器,具体需要大家各自找到对应的过滤器,因此我找到了我自己的那个过滤器里边。如下图:
通过Debug模式下
,发现如果在未登录情况下访问接口,首先进入上图的isAccessAllowed
方法,因为我的请求既不是登录页,也没有登录身份,那妥妥的在这个方法里返回false
。返回false
的结果就是会接着进入下边的方法onAccessDenied
,该方法里封装未登录的返回结果,之后也并没有进入到Shiro
自带的@RequiresGuest
注解的拦截,就返回提示了。
这是为什么呢?为什么不进入@RequiresGuest
对应的拦截。
从一方面来看,因为注解的这种AOP
方式,全都是拦截器
,而拦截器发生在过滤器
之后,因为在上边的过滤器
里已经处理为错误,因此也没法进入拦截器
,看起来不生效的原因显而易见。我也试过把我的拦截过滤器改成自带的authc
过滤器,仍然不行。
3.2 排查Shiro自带的注解
经过我的排查,我发现Shiro
的注解并不满足我的情况(基于我们自己的过滤器添加注解)。有以下几个原因:
- 配置繁琐。
Shiro
里如果要想把所有接口都拦截,都需要往每个Controller
里加@RequiresAuthentication
注解,如果没有登录,当前类下的所有方法都不能访问。我也不能写一个Controller
就加一个这个注解,多有失我的身份?当然肯定也可以自定义一个拦截器来控制,但是一定要防备像我上边的情况,过滤器给直接拦住了,注解都没有生效的情况。 - 不可交叉配置。比如我只想放开某个类里的固定一个接口,如果
Controller
上配置@RequiresAuthentication
注解,在要放开的接口上配置@RequiresGuest
注解,貌似是不生效的,即不是根据方法最优的方式来做的(这块我看过部分源码,其实看源码,感觉是满足的,但是实际情况下,我的确是出现不生效的情况,这里也不把准。)
注意:以前并没有用过这些注解,可能理解片面了,具体情况要分析下自己项目的过滤规则是怎么走的。
4. 最终解决思路优化
最终,我认为@RequiresGuest
注解很鸡肋有限。我感觉我需要解决的只是随便一个自定义的注解。我只要保证能够在我的user
过滤器中(3.1图)的isAccessAllowed
方法中,通过请求的Request
对象拿到请求uri
,根据uri
找到对应的接口方法,然后再看这个方法上对应的有没有我这个自定义的放开权限的注解,如果有,那就不需要验证,直接放行不就可以了吗?
等等,这个逻辑似曾相识:
这不就是我们正常访问一个后台接口,需要走的逻辑吗?比如我访问下图的/student/get
这样的后台接口。spring
如何通过请求request
找到的具体的方法?
/**
* 获取学生详细信息
*/
@RequiresGuest
@PostMapping(value = "/get")
public Result getInfo(@RequestBody @Validated(SelectOne.class) DemoGradeStudentModel model) {
return Result.data(demoGradeStudentService.getById(model.getId()));
}
因此我就开始搜spring
怎么做到的,最后,找到了RequestMappingHandlerMapping
这个家伙,在spring
启动后,容器里会把所有的接口地址与方法的关系维护在这个RequestMappingHandlerMapping
类中,它有一个方法getHandler(httpServletRequest)
,是可以通过request
找到对应的方法,在我使用的时候,我发现这个家伙真是好人,它肚子里连方法带的注解都有,不需要我再做处理,如下图所示:
通过上图来看,那就很明白了,只要我比较一下declaredAnnotations
集合中是否存在我自定义的@GuestAccess
注解,如果存在,那就放行,如果不存在,就正常的判断就可以了。
5. 处理步骤
根据上边的方式,开始进行修改
5.1 自定义注解
定义了一个简单的注解,目前我只允许加到方法上,并不支持加到类上。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义shiro注解,用于放开认证的接口
* 通过对controller的接口方法添加该注解,实现不需要登录既可以访问。
* @author lingsf
* @date 2021/1/25
*/
@Target(value ={ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface GuestAccess {
}
5.2 修改验证权限的过滤器
找到对应的UserAuthcFilter
过滤器(3.1所示图),如下图代码块:
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if(this.isLoginRequest(request, response)) {
return true;
} else {
Subject subject = this.getSubject(request, response);
return subject.getPrincipal() != null;
}
}
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
//省略
}
主要修改isAccessAllowed
方法:
下边仅仅支持将注解放到方法上,如果想支持类,可看本文最后的扩展部分。
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
/*********************添加如下内容***********************/
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
WebApplicationContext ctx = RequestContextUtils.findWebApplicationContext(httpServletRequest);
RequestMappingHandlerMapping mapping = ctx.getBean("requestMappingHandlerMapping", RequestMappingHandlerMapping.class);
HandlerExecutionChain handler = null;
try {
handler = mapping.getHandler(httpServletRequest);
Annotation[] declaredAnnotations = ((HandlerMethod) handler.getHandler()).
getMethod().getDeclaredAnnotations();
for(Annotation annotation:declaredAnnotations){
/**
*如果含有@GuestAccess注解,则认为是不需要验证是否登录,
*直接放行即可
**/
if(GuestAccess.class.equals(annotation.annotationType())){
return true;
}
}
} catch (Exception e) {
e.printStackTrace();
}
/*********************添加如上内容***********************/
return this.getSubject(request, response).getPrincipal() != null;
}
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
//省略
}
最后,再将shiroFilter
对应的anon
配置全部删除,如下图:
@Bean("shiroFilter")
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
//过滤规则设置
Map<String, Filter> filters = new HashMap<>();
filters.put("shiro", new ShiroAuthenticatingFilter());
filters.put("user", new UserAuthcFilter());
shiroFilter.setFilters(filters);
Map<String, String> filterMap = new LinkedHashMap<>();
/******只是将所有的anon提取出来,不再修改这里*******/
filterMap.put("/user/queryByToken", "shiro");
filterMap.put("/**", "user");
shiroFilter.setFilterChainDefinitionMap(filterMap);
retrun shiroFilter;
}
5.3 放行的接口上加@GuestAccess注解
最后,就可以在需要放开的接口上加@GuestAccess
就可以了。
/**
* 获取学生详细信息
*/
@GuestAccess
@PostMapping(value = "/get")
public Result getInfo(@RequestBody @Validated(SelectOne.class) DemoGradeStudentModel model) {
return Result.data(demoGradeStudentService.getById(model.getId()));
}
6. 扩展及总结
目前这个自定义注解仅仅在方法上有效,可以扩展为支持整个controller
,这样会更加好一些。
本次的功能实现,对于通过request
找到对应的方法有更深的了解,学习到了RequestMappingHandlerMapping
的使用方法。
7. 10.27号扩展
最近这个地方因为业务需要,必须要让整个Controller
的所有方法支持游客注解。又找到了更好的方法,有小伙伴问我这块,我就直接将逻辑贴在下边了。
其实主要是因为我有发现了一个更好的spring方法spring-core
包里的。AnnotationUtils.getAnnotation(var1,var2)
,这个更牛,支持类和方法。
代码如下,大家参考:
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
/*********************添加如下内容***********************/
HttpServletRequest httpRequest = WebUtils.toHttp(request);
WebUtils.saveRequest(request);
WebApplicationContext ctx = RequestContextUtils.findWebApplicationContext(httpRequest);
RequestMappingHandlerMapping mapping = ctx.getBean
("requestMappingHandlerMapping", RequestMappingHandlerMapping.class);
HandlerExecutionChain handler = null;
try {
handler = mapping.getHandler(httpRequest);
HandlerMethod handlerClass = (HandlerMethod)handler.getHandler();
Class<?> nowClass = handlerClass.getBeanType();
GuestAccess classWithGuestAccess = AnnotationUtils.getAnnotation(nowClass, GuestAccess.class);
if(classWithGuestAccess != null) {
return true;
}
GuestAccess methodWithGuestAccess = AnnotationUtils.getAnnotation(handlerClass.getMethod(), GuestAccess.class);
if(methodWithGuestAccess != null) {
return true;
}
} catch (Exception var12) {
var12.printStackTrace();
throw new RuntimeException(var12);
}
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
//省略
}