1. Demand background
Shiro
Friends who have used it know that shiro
there are two access control methods, through 过滤器
or 注解
. Our project is springboot + vue
a 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
. shiro
Filters are used for 登录时打通Shiro并存储身份
; user
filters are used for 校验剩余所有接口是否处于登录状态
.
When we need to release the interface, as shown in the figure above, configure multiple anon
. However, since the current Shiro
configuration 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:
2. Solutions
Because judging from where we change, in fact, it is what changes often 增删那些不需要登录即可访问的接口
.
Since Shiro
you 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 Shiro
the built-in annotations @RequiresGuest
to figure out which interface to release, and I will add this annotation Controller
to 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 Shiro
your own authc
filters. Specifically, you need to find the corresponding filter, so I found my own inside the filter. As shown in the figure below:
After passing Debug模式下
, I found that if I access the interface without logging in, I first enter isAccessAllowed
the 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 false
result will then enter the following method , which encapsulates the returned result that is not logged in, and then returns the prompt onAccessDenied
without entering the interception of Shiro
the 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, Shiro
the annotations I found did not meet my situation (adding annotations based on our own filters). There are several reasons:
- The configuration is cumbersome.
Shiro
If you want to intercept all interfaces, you need to add annotations to eachController
ri@RequiresAuthentication
. If you do not log in, all methods under the current class cannot be accessed. I can't write oneController
and 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. - Not cross-configurable. For example, I just want to release a fixed interface in a certain class. If
Controller
I@RequiresAuthentication
configure annotations on the interface to be released,@RequiresGuest
it 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 @RequiresGuest
annotations 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 user
filter (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:isAccessAllowed
Request
请求uri
uri
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 spring
by 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 spring
how to do it, and finally, I found RequestMappingHandlerMapping
this guy. After spring
startup, the relationship between all interface addresses and methods will be maintained in this RequestMappingHandlerMapping
class in the container. It has a method getHandler(httpServletRequest)
, which can be found by request
finding 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
, As long as I compare declaredAnnotations
whether my custom @GuestAccess
annotation 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 UserAuthcFilter
filter (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 isAccessAllowed
method:
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 shiroFilter
corresponding anon
configurations, 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 request
has a deeper understanding of finding the corresponding method, and learned RequestMappingHandlerMapping
how to use it.
7. Expansion on 10.27
Recently, due to business needs in this place, all methods must Controller
support 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-core
package. 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 {
//省略
}