Shiro is a lightweight permission control framework with a wide range of applications. The focus of this article is to introduce Spring's integration of Shiro and to enable dynamic parameters such as @RequiresRoles to be supported by extending Spring's EL expressions. The introduction to Shiro is not within the scope of this article. If readers do not know much about Shiro, they can learn the corresponding information through its official website. There is also an article on infoq that provides a comprehensive introduction to shiro, and it is also officially recommended. Its address is https://www.infoq.com/articles/apache-shiro.
Shiro Integrates Spring
First, you need to add shiro-spring-xxx.jar to your project. If you are using Maven to manage your project, you can add the following dependencies to your dependencies. Here is the latest version 1.4.0 that I have selected.
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.0</version></dependency>
Next, you need to define a shiroFilter in your web.xml and apply it to intercept all requests that require permission control, usually configured as /*. In addition, the Filter needs to be added to the front to ensure that the request is first controlled through shiro's permissions after it comes in. The corresponding class of Filter here is configured with DelegatingFilterProxy, which is a Filter proxy provided by Spring. You can use a bean in the Spring bean container as the current Filter instance, and the corresponding bean will take the bean corresponding to filter-name. So the following configuration will look for a bean named shiroFilter in the bean container.
<filter> <filter-name>shiroFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> <init-param> <param-name>targetFilterLifecycle</param-name> <param-value>true</param-value> </init-param></filter><filter-mapping> <filter-name>shiroFilter</filter-name> <url-pattern>/*</url-pattern></filter-mapping>
When using Shiro independently, you usually define an org.apache.shiro.web.servlet.ShiroFilter to do something similar.
Next is to define our shiroFilter in the bean container. As follows, we define a ShiroFilterFactoryBean, which will produce a AbstractShiroFilter type bean. Through ShiroFilterFactoryBean we can specify a SecurityManager. The DefaultWebSecurityManager used here needs to specify a Realm. If multiple Realm needs to be specified, it is specified through realms. For simplicity, we use TextConfigurationRealm based on text definition directly. Use loginUrl to specify the login address, successUrl to specify the address that needs to be redirected after login is successful, and unauthorizedUrl to specify the prompt page when permissions are insufficient. filterChainDefinitions defines the relationship between the URL and the Filter to be used. The filter alias on the right side of the equal sign is the filter alias. The default alias are defined in the enumeration class org.apache.shiro.web.filter.mgt.DefaultFilter.
<bean id="shiroFilter"> <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="/login.jsp"/> <property name="successUrl" value="/home.jsp"/> <property name="unauthorizedUrl" value="/unauthorized.jsp"/> <property name="unauthorizedUrl" value="/unauthorized.jsp"/> <property name="filterChainDefinitions"> <value> /admin/** = authc, roles[admin] /logout = logout # Other addresses require that the user has logged in /** = authc,logger </value> </property></bean><bean id="securityManager"> <property name="realm" ref="realm"/></bean><bean id="lifecycleBeanPostProcessor"/>
<!-- For simplicity, we will use text-based Realm implementation here-><bean id="realm"> <property name="userDefinitions"> <value> user1=pass1,role1,role2 user2=pass2,role2,role3 admin=admin,admin </value> </property></bean>
If you need to use a custom Filter in the filterChainDefinitions definition, you can specify the custom Filter and its alias mapping relationship through the filters of ShiroFilterFactoryBean. For example, as shown below, we have added a Filter with alias logger, and specified /**Filter with alias logger in filterChainDefinitions.
<bean id="shiroFilter"> <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="/login.jsp"/> <property name="successUrl" value="/home.jsp"/> <property name="unauthorizedUrl" value="/unauthorized.jsp"/> <property name="unauthorizedUrl" value="/unauthorized.jsp"/> <property name="filters"> <util:map> <entry key="logger"> <bean/> </entry> </util:map> </property> <property name="filterChainDefinitions"> <value> /admin/** = authc, roles[admin] /logout = logout # Other addresses require that the user has logged in /** = authc,logger </value> </property></bean>
In fact, the Filter alias definition we need to apply can also be defined directly by ShiroFilterFactoryBean's setFilters(), but directly define the corresponding Filter corresponding bean in the corresponding bean container. Because by default, ShiroFilterFactoryBean will register all Filter type beans in the bean container with their id alias in filters. Therefore, the above definition is equivalent to the following.
<bean id="shiroFilter"> <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="/login.jsp"/> <property name="successUrl" value="/home.jsp"/> <property name="unauthorizedUrl" value="/unauthorized.jsp"/> <property name="unauthorizedUrl" value="/unauthorized.jsp"/> <property name="filterChainDefinitions"> <value> /admin/** = authc, roles[admin] /logout = logout # Other addresses require that the user has logged in /** = authc,logger </value> </property></bean><bean id="logger"/>
After the above steps, the integration of Shiro and Spring is completed. At this time, any path we requested for the project will require us to log in, and will automatically jump to the path specified by loginUrl and let us enter the username/password to log in. At this time, we should provide a form to obtain the username through username, password through password, and then when submitting the login request, the request needs to be submitted to the address specified by loginUrl, but the request method needs to be changed to POST. The username/password used when logging in is the username/password we defined in TextConfigurationRealm. Based on our above configuration, you can use user1/pass1, admin/admin, etc. After logging in successfully, it will jump to the address specified by the successUrl parameter. If we are logged in using user1/pass1, we can also try to access /admin/index, and at this time we will jump to unauthorized.jsp due to insufficient permissions.
Enable annotation-based support
Basic integration requires us to define all the permission controls that the URL needs to apply in the filterChainDefinitions of ShiroFilterFactoryBean. This sometimes isn't that flexible. Shiro provides us with annotations that can be used after integrating Spring. It allows us to add corresponding annotations to the Class or Method that require permission control to define the permissions required to access Class or Method. If it is on Class in the definition, it means that calling all methods in the Class requires corresponding permissions (note that it needs to be an external call, which is the limitation of dynamic proxy). To use these annotations, we need to add the following two bean definitions to Spring's bean container, so that we can determine whether the user has the corresponding permissions based on the annotation definition at runtime. This is achieved through Spring's AOP mechanism. If you don't know anything about Spring Aop, you can refer to the author's "Spring Aop Introduction Column" written by the author. The following two bean definitions, AuthorizationAttributeSourceAdvisor defines an Advisor, which will intercept and verify permissions based on the annotation configuration method provided by Shiro. DefaultAdvisorAutoProxyCreator provides the function of creating proxy objects for Class marked with permission control annotations provided by Shiro, and applying AuthorizationAttributeSourceAdvisor when intercepting the target method call. When a request from the user is intercepted and the user does not have permission marked on the corresponding method or class, an org.apache.shiro.authz.AuthorizationException exception will be thrown.
<bean depends-on="lifecycleBeanPostProcessor"/><bean> <property name="securityManager" ref="securityManager"//</bean>
If <aop:config/>或<aop:aspectj-autoproxy/> is already defined in our bean container, then the DefaultAdvisorAutoProxyCreator can no longer be defined. Because the previous two cases will automatically add beans similar to DefaultAdvisorAutoProxyCreator. For more information about DefaultAdvisorAutoProxyCreator, you can also refer to the author's principle of automatically creating proxy objects in Spring Aop.
The permission control annotations provided by Shiro are as follows:
RequiresAuthentication: The user needs to be authenticated in the current session, that is, he needs to log in with the username/password, and does not include RememberMe automatic login.
RequiresUser: The user is required to be authenticated. It can be authenticated by logging in with the username/password in this session, or it can be automatically logged in with RememberMe.
RequiresGuest: The user is not logged in.
RequiresRoles: The user requires that the specified role be owned.
RequiresPermissions: The user requires the specified permissions.
The first three are easy to understand, while the last two are similar. Here I use @RequiresPermissions as an example. First, let’s change the Realm defined above and add permissions to role. In this way, our user1 will have permissions to perm1, perm2 and perm3, and user2 will have permissions to perm1, perm3 and perm4.
<bean id="realm"> <property name="userDefinitions"> <value> user1=pass1,role1,role2 user2=pass2,role2,role3 admin=admin,admin </value> </property> <property name="roleDefinitions"> <value> role1=perm1,perm2 role2=perm1,perm3 role3=perm3,perm4 </value> </property></bean>
@RequiresPermissions can be added on a method to specify the permissions that need to be claimed when calling the method. In the following code, we specify that the permission of perm1 must be possessed when accessing /perm1. At this time, both user1 and user2 can be accessed.
@RequestMapping("/perm1")@RequiresPermissions("perm1")public Object permission1() { return "perm1";}If you need to specify that you must have multiple permissions at the same time to access a method, you can specify the permissions you need to specify in the form of an array (when specifying a single array attribute on the annotation, you can do not add braces, but when you need to specify multiple permissions, braces are required). For example, as follows, we specify that when accessing /perm1AndPerm4, the user must have both perm1 and perm4 permissions. At this time, only user2 can access it, because only it has perm1 and perm4 at the same time.
@RequestMapping("/perm1AndPerm4")@RequiresPermissions({"perm1", "perm4"})public Object perm1AndPerm4() { return "perm1AndPerm4";}When multiple permissions are specified at the same time, the relationship between multiple permissions is the relationship, that is, all the permissions specified at the same time are required. If you only need to have one of the specified multiple permissions to be accessible, we can specify the relationship between the or between multiple permissions through logical=Logical.OR. For example, as follows, we specify that when accessing /perm1OrPerm4, you only need to have perm1 or perm4 permissions, so that both user1 and user2 can access this method.
@RequestMapping("/perm1OrPerm4")@RequiresPermissions(value={"perm1", "perm4"}, logical=Logical.OR)public Object perm1OrPerm4() { return "perm1OrPerm4";}@RequiresPermissions can also be marked on Class, indicating that when accessing methods in Class externally, you need to have corresponding permissions. For example, in the following, we specify that we need to have permission perm2 at the Class level, while the index() method does not specify that we need any permissions, but we still need to have permissions specified at the Class level when accessing this method. At this time, only user1 can access it.
@RestController@RequestMapping("/foo")@RequiresPermissions("perm2")public class FooController { @RequestMapping(method=RequestMethod.GET) public Object index() { Map<String, Object> map = new HashMap<>(); map.put("abc", 123); return map; }}When both Class and method levels have @RequiresPermissions, the method level has a higher priority, and only permissions required by the method level will be verified at this time. As follows, we specify that the perm2 permission is required at the Class level and the perm3 permission is required at the method level. Then when accessing /foo, you only need to have perm3 permissions to access the index() method. So at this time both user1 and user2 can access /foo.
@RestController@RequestMapping("/foo")@RequiresPermissions("perm2")public class FooController { @RequestMapping(method=RequestMethod.GET) @RequiresPermissions("perm3") public Object index() { Map<String, Object> map = new HashMap<>(); map.put("abc", 123); return map; }}However, if we add @RequiresRoles("role1") to Class at this time to specify that we need to have role role1, then when accessing /foo, we need to have the role1 specified by @RequiresPermissions("perm3") on the role1 on the index() method on Class. Because RequiresRoles and RequiresPermissions belong to permission definitions of different dimensions, Shiro will check them once during verification, but if both Class and method have annotations of the same type of permission control definition, the definition in the method will only be based on the definition.
@RestController@RequestMapping("/foo")@RequiresPermissions("perm2")@RequiresRoles("role1")public class FooController { @RequestMapping(method=RequestMethod.GET) @RequiresPermissions("perm3") public Object index() { Map<String, Object> map = new HashMap<>(); map.put("abc", 123); return map; }}Although the example only uses RequiresPermissions, the usage of other permission control annotations is also similar. Please use other annotations by interested friends.
Principle of annotation control permissions
The permissions we specified above are static using @RequiresPermissions. One of the main purposes of this article is to introduce a method to make the specified permissions dynamic by extending the implementation. But before we expand, we have to know how it works, that is, the implementation principle, before we can expand. So let's take a look at how Shiro integrates Spring with @RequiresPermissions. When enabling support for @RequiresPermissions, we define the following bean, which is an Advisor, which is inherited from StaticMethodMatcherPointcutAdvisor. Its method matching logic is that as long as the Class or Method has several permission control annotations of Shiro, the processing logic after interception is specified by the corresponding Advice.
<bean> <property name="securityManager" ref="securityManager"/></bean>
The following is the source code of AuthorizationAttributeSourceAdvisor. We can see that in its constructor method, AopAllianceAnnotationsAuthorizingMethodInterceptor, is specified by setAdvice(), which is based on the implementation of MethodInterceptor.
public class AuthorizationAttributeSourceAdvisor extends StaticMethodMatcherPointcutAdvisor { private static final Logger log = LoggerFactory.getLogger(AuthorizationAttributeSourceAdvisor.class); private static final Class<? extends Annotation>[] AUTHZ_ANNOTATION_CLASSES = new Class[] { RequiresPermissions.class, RequiresRoles.class, RequiresUser.class, RequiresGuest.class, RequiresAuthentication.class }; protected SecurityManager securityManager = null; public AuthorizationAttributeSourceAdvisor() { setAdvice(new AopAllianceAnnotationsAuthorizingMethodInterceptor()); } public SecurityManager getSecurityManager() { return securityManager; } public void setSecurityManager(org.apache.shiro.mgt.SecurityManager securityManager) { this.securityManager = securityManager; } public boolean matches(Method method, Class targetClass) { Method m = method; if ( isAuthzAnnotationPresent(m) ) { return true; } //The 'method' parameter could be from an interface that doesn't have the annotation. //Check to see if the implementation has it. if ( targetClass != null) { try { m = targetClass.getMethod(m.getName(), m.getParameterTypes()); return isAuthzAnnotationPresent(m) || isAuthzAnnotationPresent(targetClass); } catch (NoSuchMethodException ignored) { //default return value is false. If we can't find the method, then obviously //there is no annotation, so just use the default return value. } } return false; } private boolean isAuthzAnnotationPresent(Class<?> targetClazz) { for( Class<? extends Annotation> annClass : AUTHZ_ANNOTATION_CLASSES ) { Annotation a = AnnotationUtils.findAnnotation(targetClazz, annClass); if ( a != null ) { return true; } } return false; } private boolean isAuthzAnnotationPresent(Method method) { for( Class<? extends Annotation> annClass : AUTHZ_ANNOTATION_CLASSES ) { Annotation a = AnnotationUtils.findAnnotation(method, annClass); if ( a != null ) { return true; } } return false; }}The source code of AopAllianceAnnotationsAuthorizingMethodInterceptor is as follows. The invoke method of the MethodInterceptor interface implemented in it calls the invoke method of the parent class. At the same time, we need to see that some AuthorizingAnnotationMethodInterceptor implementations have been created in its constructor method. These implementations are the core of implementing permission control. Later, we will select the PermissionAnnotationMethodInterceptor implementation class to see its specific implementation logic.
public class AopAllianceAnnotationsAuthorizingMethodInterceptor extends AnnotationsAuthorizingMethodInterceptor implements MethodInterceptor { public AopAllianceAnnotationsAuthorizingMethodInterceptor() { List<AuthorizingAnnotationMethodInterceptor> interceptors = new ArrayList<AuthorizingAnnotationMethodInterceptor>(5); //use a Spring-specific Annotation resolver - Spring's AnnotationUtils is nicer than the //raw JDK resolution process. AnnotationResolver resolver = new SpringAnnotationResolver(); //we can re-use the same resolver instance - it does not retain state: interceptors.add(new RoleAnnotationMethodInterceptor(resolver)); interceptors.add(new PermissionAnnotationMethodInterceptor(resolver)); interceptors.add(new UserAnnotationMethodInterceptor(resolver)); interceptors.add(new GuestAnnotationMethodInterceptor(resolver)); setMethodInterceptors(interceptors); } protected org.apache.shiro.aop.MethodInvocation createMethodInvocation(Object implSpecificMethodInvocation) { final MethodInvocation mi = (MethodInvocation) implSpecificMethodInvocation; return new org.apache.shiro.aop.MethodInvocation() { public Method getMethod() { return mi.getMethod(); } public Object[] getArguments() { return mi.getArguments(); } public String toString() { return "Method invocation [" + mi.getMethod() + "]"; } public Object proceed() throws Throwable { return mi.proceed(); } public Object getThis() { return mi.getThis(); } }; } protected Object continueInvocation(Object aopAllianceMethodInvocation) throws Throwable { MethodInvocation mi = (MethodInvocation) aopAllianceMethodInvocation; return mi.proceed(); } public Object invoke(MethodInvocation methodInvocation) throws Throwable { org.apache.shiro.aop.MethodInvocation mi = createMethodInvocation(methodInvocation); return super.invoke(mi); }}By looking at the implementation of the invoke method of the parent class, we will finally see that the core logic is to call the assertAuthorized method, and the implementation of this method (source code is as follows) is to determine whether the configured AuthorizingAnnotationMethodInterceptor supports permission verification of the current method (by judging whether it has its supported annotation on Class or Method). When supported, its assertAuthorized method will be called for permission verification, and the AuthorizingAnnotationMethodInterceptor will in turn call the assertAuthorized method of AuthorizingAnnotationHandler.
protected void assertAuthorized(MethodInvocation methodInvocation) throws AuthorizationException { //default implementation just ensures no deny votes are cast: Collection<AuthorizingAnnotationMethodInterceptor> aamis = getMethodInterceptors(); if (aamis != null && !aamis.isEmpty()) { for (AuthorizingAnnotationMethodInterceptor aami : aamis) { if (aami.supports(methodInvocation)) { aami.assertAuthorized(methodInvocation); } } }}Next, let's look back at the PermissionAnnotationMethodInterceptor defined by AopAllianceAnnotationsAuthorizingMethodInterceptor, the source code is as follows. Combining the source code of AopAllianceAnnotationsAuthorizingMethodInterceptor and the source code of PermissionAnnotationMethodInterceptor, we can see that PermissionAnnotationHandler and SpringAnnotationResolver are specified in PermissionAnnotationMethodInterceptor. PermissionAnnotationHandler is a subclass of AuthorizingAnnotationHandler. So our final permission control is determined by the assertAuthorized implementation of PermissionAnnotationHandler.
public class PermissionAnnotationMethodInterceptor extends AuthorizingAnnotationMethodInterceptor { public PermissionAnnotationMethodInterceptor() { super( new PermissionAnnotationHandler() ); } public PermissionAnnotationMethodInterceptor(AnnotationResolver resolver) { super( new PermissionAnnotationHandler(), resolver); }}Next, let's look at the assertAuthorized method implementation of PermissionAnnotationHandler, and the complete code is as follows. From the implementation we can see that it will obtain the configured permission value from Annotation, and the Annotation here is the RequiresPermissions annotation. Moreover, when performing permission verification, we directly use the text value specified when defining the annotation. We will start from here when we expand it later.
public class PermissionAnnotationHandler extends AuthorizingAnnotationHandler { public PermissionAnnotationHandler() { super(RequiresPermissions.class); } protected String[] getAnnotationValue(Annotation a) { RequiresPermissions rpAnnotation = (RequiresPermissions) a; return rpAnnotation.value(); } public void assertAuthorized(Annotation a) throws AuthorizationException { if (!(a instanceof RequiresPermissions)) return; RequiresPermissions rpAnnotation = (RequiresPermissions) a; String[] perms = getAnnotationValue(a); Subject subject = getSubject(); if (perms.length == 1) { subject.checkPermission(perms[0]); return; } if (Logical.AND.equals(rpAnnotation.logical())) { getSubject().checkPermissions(perms); return; } if (Logical.OR.equals(rpAnnotation.logical())) { // Avoid processing exceptions unecessarily - "delay" throwing the exception by calling hasRole first boolean hasAtLeastOnePermission = false; for (String permission : perms) if (getSubject().isPermitted(permission)) hasAtLeastOnePermission = true; // Cause the exception if none of the role match, note that the exception message will be a bit misleading if (!hasAtLeastOnePermission) getSubject().checkPermission(perms[0]); } }}Through the previous introduction, we know that the Annotation of the assertAuthorized method parameter of PermissionAnnotationHandler is passed by AuthorizingAnnotationMethodInterceptor when calling the assertAuthorized method of AuthorizingAnnotationHandler. The source code is as follows. From the source code, we can see that Annotation is obtained through the getAnnotation method.
public void assertAuthorized(MethodInvocation mi) throws AuthorizationException { try { ((AuthorizingAnnotationHandler)getHandler()).assertAuthorized(getAnnotation(mi)); } catch(AuthorizationException ae) { if (ae.getCause() == null) ae.initCause(new AuthorizationException("Not authorized to invoke method: " + mi.getMethod())); throw ae; } }Walking along this direction, we will eventually find the getAnnotation method implementation of SpringAnnotationResolver, which is implemented as follows. As can be seen from the following code, it is preferred to look for the method when looking for annotations. If it is not found on the method, it will look for the corresponding annotation from the Class of the current method call. From here, we can also see why the one that takes effect on the Method when we define the same type of permission control annotation on Class and Method before, and when it exists alone, the one that is defined takes effect.
public class SpringAnnotationResolver implements AnnotationResolver { public Annotation getAnnotation(MethodInvocation mi, Class<? extends Annotation> clazz) { Method m = mi.getMethod(); Annotation a = AnnotationUtils.findAnnotation(m, clazz); if (a != null) return a; //The MethodInvocation's method object could be a method defined in an interface. //However, if the annotation existed in the interface's implementation (and not //the interface itself), it won't be on the above method object. Instead, we need to //acquire the method representation from the targetClass and check directly on the //implementation itself: Class<?> targetClass = mi.getThis().getClass(); m = ClassUtils.getMostSpecificMethod(m, targetClass); a = AnnotationUtils.findAnnotation(m, clazz); if (a != null) return a; // See if the class has the same annotation return AnnotationUtils.findAnnotation(mi.getThis().getClass(), clazz); }} Through the above source code reading, I believe that readers have a deeper understanding of the principle of permission control annotations supported by Shiro after integrating Spring. The source code posted above is only some of the core ones that the author thinks are relatively core. If you want to know the complete content in detail, please read the complete code by yourself along the ideas mentioned by the author.
After understanding this principle of permission control based on annotations, readers can also expand accordingly according to actual business needs.
Extended using Spring EL expressions
Suppose there is now an interface like the following, which has a query method that receives a parameter type. Let's simplify it here, assuming that as long as such a parameter is received and different values will be returned.
public interface RealService { Object query(int type); }This interface is open to the outside world. This method can be requested through the corresponding URL. We define the corresponding Controller method as follows:
@RequestMapping("/service/{type}")public Object query(@PathVariable("type") int type) { return this.realService.query(type);}The above interface service has permissions for type when conducting query. Not every user can use each type to query, and it requires corresponding permissions. Therefore, for the above processor method, we need to add permission control, and the permissions required during control change dynamically with the parameter type. Suppose that the definition of each permission of type is the form of query:type. For example, the permission required when type=1 is query:1, and the permission required when type=2 is query:2. When not integrated with Spring, we do this as follows:
@RequestMapping("/service/{type}")public Object query(@PathVariable("type") int type) { SecurityUtils.getSubject().checkPermission("query:" + type); return this.realService.query(type);}However, after integration with Spring, the above practices are highly coupled, and we would rather use the integrated annotations to control permissions. For the above scenario, we would rather specify the required permissions through @RequiresPermissions, but the permissions defined in @RequiresPermissions are static text and fixed. It cannot meet our dynamic needs. At this time, you may think that we can split the Controller processing method into multiple and control the permissions separately. For example, the following is:
@RequestMapping("/service/1")@RequiresPermissions("query:1")public Object service1() { return this.realService.query(1);}@RequiresPermissions("query:2")@RequestMapping("/service/2")public Object service2() { return this.realService.query(2);}//...@RequestMapping("/service/200")@RequiresPermissions("query:200")public Object service200() { return this.realService.query(200);}This is OK when the value range of type is relatively small, but if there are 200 possible values like the above, it will be a bit troublesome to enumerate them exhaustively to define a separate processor method and perform permission control. In addition, if the value of type changes in the future, we have to add a new processor method. So the best way is to make @RequiresPermissions support dynamic permission definitions, while maintaining static definition support. Through the previous analysis, we know that the entry point is PermissionAnnotationHandler, and it does not provide extensions for permission verification. If we want to expand it, the simple way is to replace it as a whole. However, the permissions we need to process dynamically are related to method parameters, and the method parameters cannot be obtained in the PermissionAnnotationHandler. For this reason, we cannot directly replace the PermissionAnnotationHandler. PermissionAnnotationHandler is called by PermissionAnnotationMethodInterceptor. Method parameters can be obtained when PermissionAnnotationHandler is called in the assertAuthorized method of its parent class AuthorizingAnnotationMethodInterceptor. For this reason, our extension point is selected on the PermissionAnnotationMethodInterceptor class, and we also need to replace it as a whole. Spring's EL expressions can support parsing method parameter values. Here we choose to introduce Spring's EL expressions. When @RequiresPermissions define permissions, you can use Spring EL expressions to introduce method parameters. At the same time, in order to take into account static text. Here is a Spring EL expression template. For Spring's EL expression template, please refer to this blog post. We define our own PermissionAnnotationMethodInterceptor, inherit it from PermissionAnnotationMethodInterceptor, and override the assertAuthoried method. The implementation logic of the method refers to the logic in PermissionAnnotationHandler, but the permission definition in @RequiresPermissions used is the result of our use of Spring EL expression based on the currently called method as the result of parsing the EvaluationContext. The following is our own definition of PermissionAnnotationMethodInterceptor implementation.
public class SelfPermissionAnnotationMethodInterceptor extends PermissionAnnotationMethodInterceptor { private final SpelExpressionParser parser = new SpelExpressionParser(); private final ParameterNameDiscoverer paramNameDiscoverer = new DefaultParameterNameDiscoverer(); private final TemplateParserContext templateParserContext = new TemplateParserContext(); public SelfPermissionAnnotationMethodInterceptor(AnnotationResolver resolver) { super(resolver); } @Override public void assertAuthorized(MethodInvocation mi) throws AuthorizationException { Annotation annotation = super.getAnnotation(mi); RequiresPermissions permAnnotation = (RequiresPermissions) annotation; String[] perms = permAnnotation.value(); EvaluationContext evaluationContext = new MethodBasedEvaluationContext(null, mi.getMethod(), mi.getArguments(), paramNameDiscoverer); for (int i=0; i<perms.length; i++) { Expression expression = this.parser.parseExpression(perms[i], templateParserContext); //Replace the original permission definition with the permission definition parsed by Spring EL expression perms[i] = expression.getValue(evaluationContext, String.class); } Subject subject = getSubject(); if (perms.length == 1) { subject.checkPermission(perms[0]); return; } if (Logical.AND.equals(permAnnotation.logical())) { getSubject().checkPermissions(perms); return; } if (Logical.OR.equals(permAnnotation.logical())) { // Avoid processing exceptions unnecessarily - "delay" throwing the exception by calling hasRole first boolean hasAtLeastOnePermission = false; for (String permission : perms) if (getSubject().isPermitted(permission)) hasAtLeastOnePermission = true; // Cause the exception if none of the role match, note that the exception message will be a bit misleading if (!hasAtLeastOnePermission) getSubject().checkPermission(perms[0]); } }}After defining our own PermissionAnnotationMethodInterceptor, we need to replace the original PermissionAnnotationMethodInterceptor as our own PermissionAnnotationMethodInterceptor. According to the principle of Shiro integrating Spring with @RequiresPermissions and other annotations introduced earlier, we know that PermissionAnnotationMethodInterceptor is specified by AopAllianceAnnotationsAuthorizingMethodInterceptor, which is specified by AuthorizationAttributeSourceAdvisor. To do this, we need to display the AuthorizingAnnotationMethodInterceptor in the definition of AuthorizationAttributeSourceAdvisor by displaying the AuthorizingAnnotationMethodInterceptor in the definition of the AuthorizationAttributeSourceAdvisor by displaying the AuthorizingAnnotationMethodInterceptor in the definition of the AuthorizationAttributeSourceAdvisor by displaying the AuthorizingAnnotationMethodInterceptor in the definition of the AuthorizationAttributeSourceAdvisor by displaying the AuthorizingAnnotationMethodInterceptor in the definition of the AuthorizationAttributeSourceAdvisor by displaying the AuthorizingAnnotationMethodInterceptor in the definition of the AuthorizationAttributeSourceAdvisor by displaying the AuthorizingAnnotationMethodInterceptor in the definition of the AuthorizationAttributeSourceAdvisor by displaying the AuthorizingAnnotationMethodInterceptor in the definition of the AuthorizationAttributeSourceAdvisor by displaying the AuthorizingAnnotationMethodInterceptor in the definition of the AuthorizationAttributeSourceAdvisor by displaying the AuthorizingAnnotationMethodInterceptor in the definition of the AuthorizationAttributeSourceAdvisor by displaying the AuthorizingAnnotationMethodInterceptor in the definition of the AuthorizationAttributeSourceAdvisor by displaying the AuthorizationAdvisor by displaying the AuthorizationAdvisor by displaying the AuthorizationAdvisor by displaying the AuthorizationAdvisor by displaying the AuthorizationAdvisor by displaying the AuthorizationAdvisor by displaying the AuthorizationAdvisor by displaying the AuthorizationAdvisor by displaying the AuthorizationAdvisor by displaying the AuthorizationAdvisor by displaying the AuthorizationAdvisor by displaying the AuthorizationAdvisor by displaying the AuthorizationAdvisor by displaying the AuthorizationAdvisor by displaying the AuthorizationAdvisor by displaying the Author The replacement definition is as follows:
<bean> <property name="securityManager" ref="securityManager"/> <property name="advice"> <bean> <property name="methodInterceptors"> <util:list> <bean c:resolver-ref="springAnnotationResolver"/> <!-- 使用自定义的PermissionAnnotationMethodInterceptor --> <bean c:resolver-ref="springAnnotationResolver"/> <bean c:resolver-ref="springAnnotationResolver"/> <bean c:resolver-ref="springAnnotationResolver"/> <bean c:resolver-ref="springAnnotationResolver"/> </util:list> </property> </bean> </property></bean><bean id="springAnnotationResolver"/>
为了演示前面示例的动态的权限,我们把角色与权限的关系调整如下,让role1、role2和role3分别拥有query:1、query:2和query:3的权限。此时user1将拥有query:1和query:2的权限。
<bean id="realm"> <property name="userDefinitions"> <value> user1=pass1,role1,role2 user2=pass2,role2,role3 admin=admin,admin </value> </property> <property name="roleDefinitions"> <value> role1=perm1,perm2,query:1 role2=perm1,perm3,query:2 role3=perm3,perm4,query:3 </value> </property></bean>
此时@RequiresPermissions中指定权限时就可以使用Spring EL表达式支持的语法了。因为我们在定义SelfPermissionAnnotationMethodInterceptor时已经指定了应用基于模板的表达式解析,此时权限中定义的文本都将作为文本解析,动态的部分默认需要使用#{前缀和}后缀包起来(这个前缀和后缀是可以指定的,但是默认就好)。在动态部分中可以使用#前缀引用变量,基于方法的表达式解析中可以使用参数名或p参数索引的形式引用方法参数。所以上面我们需要动态的权限的query方法的@RequiresPermissions定义如下。
@RequestMapping("/service/{type}")@RequiresPermissions("query:#{#type}")public Object query(@PathVariable("type") int type) { return this.realService.query(type);}这样user1在访问/service/1和/service/2是OK的,但是在访问/service/3和/service/300时会提示没有权限,因为user1没有query:3和query:300的权限。
Summarize
以上所述是小编给大家介绍的Spring 整合Shiro 并扩展使用EL表达式的实例详解,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。 Thank you very much for your support to Wulin.com website!