Shiro uses annotations to customize the control interface without authentication access solution process

1. Demand background

ShiroFriends who   have used it know that shirothere are two access control methods, through 过滤器or 注解. Our project is springboot + vuea front-to-back separation project, and the background has always used the right 过滤器method for permission control, and there are also custom ones 过滤器. Probably as follows:

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

  As shown in the figure above, we have customized two filters shiro, user. shiroFilters are used for 登录时打通Shiro并存储身份; userfilters are used for 校验剩余所有接口是否处于登录状态.
  When we need to release the interface, as shown in the figure above, configure multiple anon. However, since the current Shiroconfiguration file is also regarded as a main configuration file in the project, the development is always allowed to continuously modify this file.
  For a serious 猴子person, this kind of thing cannot happen. 开闭原则The design should be strictly followed , 对扩展开放、对修改关闭. All the changes that need to be modified should be taken outside, and the current configuration file should be included in the system jar package, and only references are allowed. The effect I want is as follows:
insert image description here

2. Solutions

  Because judging from where we change, in fact, it is what changes often 增删那些不需要登录即可访问的接口.
  Since Shiroyou provide annotations by yourself, you can 过滤器+注解solve them by means of methods. The configuration in the above picture is changed to the following:

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

  Then use Shirothe built-in annotations @RequiresGuestto figure out which interface to release, and I will add this annotation Controllerto the corresponding release interface, as follows:

  /**
     * 获取学生详细信息
     */
    @RequiresGuest
    @PostMapping(value = "/get")
    public Result getInfo(@RequestBody @Validated(SelectOne.class) DemoGradeStudentModel model) {
    
    
        return Result.data(demoGradeStudentService.getById(model.getId()));
    }

  Because the number of interfaces we need to release is far less than the interfaces that need to be intercepted, it is the best way to realize this function by controlling the annotations that configure the release.
  Is the idea very simple? Yes, I also configured it in this way. However, the facts are slapped in the face and it doesn't work .
  After searching online, all of them need to add the following annotations:

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

  However it doesn't work .

3. Cause tracing

3.1 Troubleshoot interception filter logic

  Looking at it through debug, because all my remaining interception methods are based on custom filters user, some of you may be custom-made, and some are based on Shiroyour own authcfilters. Specifically, you need to find the corresponding filter, so I found my own inside the filter. As shown in the figure below:
insert image description here
   After passing Debug模式下, I found that if I access the interface without logging in, I first enter isAccessAllowedthe method in the above figure, because my request is neither a login page nor a login identity, so I will return it properly in this method false. The returned falseresult will then enter the following method , which encapsulates the returned result that is not logged in, and then returns the prompt onAccessDeniedwithout entering the interception of Shirothe built-in annotation.    Why is this? Why not enter the corresponding interception.    On the one hand, because the way of annotation is all , and the interceptor occurs later , because it has been processed as an error in the above, it cannot be entered , and the reason why it does not seem to be effective is obvious. I also tried to change my interception filter to the built-in filter, but it still doesn't work.@RequiresGuest
@RequiresGuest
AOP拦截器过滤器过滤器拦截器authc

3.2 Check the annotations that come with Shiro

   After my investigation, Shirothe annotations I found did not meet my situation (adding annotations based on our own filters). There are several reasons:

  1. The configuration is cumbersome. ShiroIf you want to intercept all interfaces, you need to add annotations to each Controllerri @RequiresAuthentication. If you do not log in, all methods under the current class cannot be accessed. I can't write one Controllerand just add this comment. How much does it lose my identity? Of course, you can also customize an interceptor to control it, but you must be on guard against the situation like mine above, where the filter is directly blocked and the annotations do not take effect.
  2. Not cross-configurable. For example, I just want to release a fixed interface in a certain class. If ControllerI @RequiresAuthenticationconfigure annotations on the interface to be released, @RequiresGuestit seems that it will not take effect, that is, it is not done according to the optimal method (this section I have read some of the source code, and I feel satisfied when I look at the source code, but in reality, it does not work for me, so I am not sure here.)

  Note: These annotations have not been used before, and the understanding may be one-sided. For the specific situation, you need to analyze how the filtering rules of your project work.

4. Optimizing the final solution

   Ultimately, I think @RequiresGuestannotations are very limited. I feel like all I need to fix is ​​a random custom annotation. I just need to ensure that I can get it through the requested object in the method of my userfilter (Figure 3.1) , find the corresponding interface method according to it , and then see if there is my custom release permission corresponding to this method Note, if there is, then there is no need to verify, can it be released directly?    Wait, this logic seems familiar:isAccessAllowedRequest请求uriuri

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

   Isn't this the logic we need to go to access a background interface normally? For example, I access the background interface as shown in the figure below /student/get. How to find the specific method springby request ?request

  /**
     * 获取学生详细信息
     */
    @RequiresGuest
    @PostMapping(value = "/get")
    public Result getInfo(@RequestBody @Validated(SelectOne.class) DemoGradeStudentModel model) {
    
    
        return Result.data(demoGradeStudentService.getById(model.getId()));
    }

   So I started to search springhow to do it, and finally, I found RequestMappingHandlerMappingthis guy. After springstartup, the relationship between all interface addresses and methods will be maintained in this RequestMappingHandlerMappingclass in the container. It has a method getHandler(httpServletRequest), which can be found by requestfinding the corresponding Method, when I used it, I found that this guy is really a good guy, it even has annotations in the method, and I don’t need to deal with it, as shown in the picture below: From the picture above, it is very clear
insert image description here
   , As long as I compare declaredAnnotationswhether my custom @GuestAccessannotation exists in the collection, if it exists, then let it go, if it does not exist, it can be judged normally.

5. Processing steps

   According to the above method, start to modify

5.1 Custom annotations

   A simple annotation is defined. Currently, I only allow it to be added to the method, not to the class.

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 Modify the filter for authentication permissions

   Find the corresponding UserAuthcFilterfilter (as shown in 3.1), as shown in the following code block:

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 {
    
    
    //省略
       }

   Main modification isAccessAllowedmethod:
   the following only supports the release of annotations on methods. If you want to support classes, you can see the extension part at the end of this article.

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 {
    
    
    //省略
       }

   Finally, delete all the shiroFiltercorresponding anonconfigurations, as shown in the figure below:

@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 Add @GuestAccess annotation to the released interface

   Finally, you can add on the interface that needs to be released @GuestAccess
.

  /**
     * 获取学生详细信息
     */
    @GuestAccess
    @PostMapping(value = "/get")
    public Result getInfo(@RequestBody @Validated(SelectOne.class) DemoGradeStudentModel model) {
    
    
        return Result.data(demoGradeStudentService.getById(model.getId()));
    }

6. Expansion and conclusion

  At present, this custom annotation is only valid on the method, and it can be extended to support the entire method controller, which will be better.
  The realization of this function requesthas a deeper understanding of finding the corresponding method, and learned RequestMappingHandlerMappinghow to use it.

7. Expansion on 10.27

  Recently, due to business needs in this place, all methods must Controllersupport tourist annotations. I found a better method. Some friends asked me about this, so I just posted the logic below.
   In fact, it is mainly because I have found a better spring method spring-corepackage. AnnotationUtils.getAnnotation(var1,var2), This one is even better, supporting classes and methods.
   The code is as follows, for your reference:

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 {
    
    
    //省略
       }

Guess you like

Origin blog.csdn.net/wohaqiyi/article/details/113176439