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;
}

  如上图所示,我们自定义了两种过滤器shirousershiro过滤器用来登录时打通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的注解并不满足我的情况(基于我们自己的过滤器添加注解)。有以下几个原因:

  1. 配置繁琐。 Shiro里如果要想把所有接口都拦截,都需要往每个Controller里加@RequiresAuthentication注解,如果没有登录,当前类下的所有方法都不能访问。我也不能写一个Controller就加一个这个注解,多有失我的身份?当然肯定也可以自定义一个拦截器来控制,但是一定要防备像我上边的情况,过滤器给直接拦住了,注解都没有生效的情况。
  2. 不可交叉配置。比如我只想放开某个类里的固定一个接口,如果Controller上配置@RequiresAuthentication注解,在要放开的接口上配置@RequiresGuest注解,貌似是不生效的,即不是根据方法最优的方式来做的(这块我看过部分源码,其实看源码,感觉是满足的,但是实际情况下,我的确是出现不生效的情况,这里也不把准。)

  注意:以前并没有用过这些注解,可能理解片面了,具体情况要分析下自己项目的过滤规则是怎么走的。

4. 最终解决思路优化

   最终,我认为@RequiresGuest注解很鸡肋有限。我感觉我需要解决的只是随便一个自定义的注解。我只要保证能够在我的user过滤器中(3.1图)的isAccessAllowed方法中,通过请求的Request对象拿到请求uri,根据uri找到对应的接口方法,然后再看这个方法上对应的有没有我这个自定义的放开权限的注解,如果有,那就不需要验证,直接放行不就可以了吗?
   等等,这个逻辑似曾相识:

拿到请求uri
request
找到具体的方法
查看方法上的注解

   这不就是我们正常访问一个后台接口,需要走的逻辑吗?比如我访问下图的/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 {
    //省略
       }
Logo

前往低代码交流专区

更多推荐