shiro通过注解⽅式⾃定义控制接⼝⽆需认证访问的解决过程
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.Id()));
}
  因为我们需要放开的接⼝数量远远少于需要拦截的接⼝,因此通过控制配置放开的注解来实现这样的功能,是最好的⽅式。
  思路是不是很简单,对,我也是三下五除⼆就这么配置完了,然⽽事实打脸了,并不管⽤。
  经过⽹上搜索,全都是需要添加以下注解:
@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的注解并不满⾜我的情况(基于我们⾃⼰的过滤器添加注解)。有以下⼏个原因:
1. 配置繁琐。 Shiro⾥如果要想把所有接⼝都拦截,都需要往每个Controller⾥加@RequiresAuthentication注解,如果没有登录,当前类
下的所有⽅法都不能访问。我也不能写⼀个Controller就加⼀个这个注解,多有失我的⾝份?当然肯定也可以⾃定义⼀个来控制,但是⼀定要防备像我上边的情况,过滤器给直接拦住了,注解都没有⽣效的情况。
2. 不可交叉配置。⽐如我只想放开某个类⾥的固定⼀个接⼝,如果Controller上配置@RequiresAuthentication注解,在要放开的接⼝上配
置@RequiresGuest注解,貌似是不⽣效的,即不是根据⽅法最优的⽅式来做的(这块我看过部分源
码,其实看源码,感觉是满⾜的,但是实际情况下,我的确是出现不⽣效的情况,这⾥也不把准。)
  注意:以前并没有⽤过这些注解,可能理解⽚⾯了,具体情况要分析下⾃⼰项⽬的过滤规则是怎么⾛的。
4. 最终解决思路优化
  最终,我认为@RequiresGuest注解很鸡肋有限。我感觉我需要解决的只是随便⼀个⾃定义的注解。我只要保证能够在我的user过滤器中(3.1图)的isAccessAllowed⽅法中,通过请求的Request对象拿到请求uri,根据uri到对应的接⼝⽅法,然后再看这个⽅法上对应的有没有我这个⾃定义的放开权限的注解,如果有,那就不需要验证,直接放⾏不就可以了吗?
  等等,这个逻辑似曾相识:
拿到请求uri
  这不就是我们正常访问⼀个后台接⼝,需要⾛的逻辑吗?⽐如我访问下图的/student/get这样的后台接⼝。spring如何通过请
求request到的具体的⽅法?
/**
* 获取学⽣详细信息
*/
@RequiresGuest
@PostMapping(value ="/get")
public Result getInfo(@RequestBody@Validated(SelectOne.class) DemoGradeStudentModel model){
return Result.Id()));
}
  因此我就开始搜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 =Subject(request, response);
Principal()!=null;
}
}
protected boolean onAccessDenied(ServletRequest request, ServletResponse response)throws Exception {
//省略
}
  主要修改isAccessAllowed⽅法:
  下边仅仅⽀持将注解放到⽅法上,如果想⽀持类,可看本⽂最后的扩展部分。
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue){
/*********************添加如下内容***********************/
shiro权限控制HttpServletRequest httpServletRequest =(HttpServletRequest) request;
WebApplicationContext ctx = RequestContextUtils.findWebApplicationContext(httpServletRequest);
RequestMappingHandlerMapping mapping = Bean("requestMappingHandlerMapping", RequestMappingHandlerMapping.class);        HandlerExecutionChain handler =null;
try{
handler = Handler(httpServletRequest);
Annotation[] declaredAnnotations =((HandlerMethod) Handler()).
getMethod().getDeclaredAnnotations();
for(Annotation annotation:declaredAnnotations){
/**
*如果含有@GuestAccess注解,则认为是不需要验证是否登录,
*直接放⾏即可
**/
if(GuestAccess.class.equals(annotation.annotationType())){
return true;
}
}
}catch(Exception e){
e.printStackTrace();
}
/*********************添加如上内容***********************/
Subject(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.Id()));
}
6. 扩展及总结
  ⽬前这个⾃定义注解仅仅在⽅法上有效,可以扩展为⽀持整个controller,这样会更加好⼀些。
  本次的功能实现,对于通过request到对应的⽅法有更深的了解,学习到了RequestMappingHandlerMapping的使⽤⽅法。
7. 10.27号扩展
  最近这个地⽅因为业务需要,必须要让整个Controller的所有⽅法⽀持游客注解。⼜到了更好的⽅
法,有⼩伙伴问我这块,我就直接将逻辑贴在下边了。
  其实主要是因为我有发现了⼀个更好的spring⽅法spring-core包⾥的。Annotation(var1,var2),这个更⽜,⽀持类和⽅法。
  代码如下,⼤家参考:
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue){
/*********************添加如下内容***********************/
HttpServletRequest httpRequest = Http(request);
WebUtils.saveRequest(request);
WebApplicationContext ctx = RequestContextUtils.findWebApplicationContext(httpRequest);
RequestMappingHandlerMapping mapping = Bean
("requestMappingHandlerMapping", RequestMappingHandlerMapping.class);
HandlerExecutionChain handler =null;
try{
handler = Handler(httpRequest);
HandlerMethod handlerClass =(Handler();
Class<?> nowClass = BeanType();
GuestAccess classWithGuestAccess = Annotation(nowClass, GuestAccess.class);
if(classWithGuestAccess !=null){
return true;
}
GuestAccess methodWithGuestAccess = Method(), 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 {
//省略
}

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。