Shiro is a lightweight permission control framework that is widely used. The focus of this article is on Spring’s integration with Shiro and extending the use of Spring’s EL expressions to support dynamic parameters such as @RequiresRoles. The introduction of Shiro is beyond the scope of this article. If you are not familiar with Shiro, please refer to its official website for information. Has an article to introduce shiro infoq is comprehensive, is also the official recommendation, the address is www.infoq.com/articles/ap… .

Shiro integration of Spring

If you are using Maven to manage your projects, you can add the following dependencies to your dependencies. Here is the latest version of the dependencies selected.

< the dependency > < groupId > org, apache shiro < / groupId > < artifactId > shiro - spring < / artifactId > < version > 1.4.0 < / version > </dependency>Copy the code

Next you need to define a shiroFilter in your web.xml and apply it to block 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 passes Through Shiro’s permission control first. The Filter class is configured with DelegatingFilterProxy, which is a Filter proxy provided by Spring. You can use a bean from the Spring Bean container as the current Filter instance. The corresponding bean will take the bean corresponding to filter-name. So the following configuration looks 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>Copy the code

Independent often define an org. When using Shiro. Apache Shiro. Web. Servlet. ShiroFilter to do similar things.

The next step is to define our shiroFilter in the bean container. Below we define a ShiroFilterFactoryBean that produces a bean of type AbstractShiroFilter. Through ShiroFilterFactoryBean we can specify a SecurityManager, here use DefaultWebSecurityManager need to specify a Realm, if you need to specify multiple Realm is specified by realms. For simplicity, we’ll use the TextConfigurationRealm, which is based on text definitions. Use loginUrl to specify the login address, successUrl to specify the login address, and unauthorizedUrl to specify a message indicating that the login permission is insufficient. FilterChainDefinitions defines the relationship between the URL and the Filter you want to use. The alias for the Filter to the right of the equals sign is filterChainDefinitions. Aliases are defined by default in org. Apache. Shiro. Web. Filter. MGT. The DefaultFilter this enumeration class.

<bean id="shiroFilter " class="org.apache.shiro.spring.web.ShiroFilterFactoryBean "> <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="filterChainDefinitions "> <value> /admin/** = authc, Logger </value> </property> </bean> <bean ID ="securityManager  " class="org.apache.shiro.web.mgt.DefaultWebSecurityManager "> <property name="realm " ref="realm "/> </bean> <bean id="lifecycleBeanPostProcessor " class="org.apache.shiro.spring.LifecycleBeanPostProcessor "/> <! B: For simplicity, Here is to use text-based Realm implementations - - > < bean id = "Realm" class = ". Org. Apache shiro. Realm. Text. TextConfigurationRealm "> < property name="userDefinitions "> <value> user1=pass1,role1,role2 user2=pass2,role2,role3 admin=admin,admin </value> </property> </bean>Copy the code

If you need to use a custom Filter in the filterChainDefinitions definition, you can specify the custom Filter and its alias mapping through the filters of the ShiroFilterFactoryBean. For example, we added a Filter named Logger and specified /** in filterChainDefinitions to apply the Filter alias logger.

<bean id="shiroFilter " class="org.apache.shiro.spring.web.ShiroFilterFactoryBean "> <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="filters "> <util:map> <entry key="logger "> <bean class="com.elim.chat.shiro.filter.LoggerFilter "/> </entry> </util:map> </property> <property name="filterChainDefinitions "> <value> /admin/** = authc, Roles [admin] /logout = logger </value> </property> </bean>Copy the code

Instead of using setFilters() in the ShiroFilterFactoryBean, the Filter alias can be defined directly in the bean container. Because by default, ShiroFilterFactoryBean registers all beans of type Filter in the bean container with their IDS as aliases into the filters. So the above definition is equivalent to the following.

<bean id="shiroFilter " class="org.apache.shiro.spring.web.ShiroFilterFactoryBean "> <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="filterChainDefinitions "> <value> /admin/** = authc, Logger </value> </property> </bean> <bean id="logger" class="com.elim.chat.shiro.filter.LoggerFilter "/>Copy the code

After the above steps, Shiro’s integration with Spring is complete. At this point, any path of our request project will ask us to login, and it will automatically redirect to the path specified by loginUrl for us to enter the username/password to login. In this case, we should provide a form, get the username through username, get the password through password, and then submit the login request to the address specified by loginUrl, but change the request mode to POST. The user name/password used for login is the one we defined in TextConfigurationRealm, user1/pass1, admin/admin, and so on, based on our configuration above. The successful login redirects to the address specified by the successUrl parameter. If you log in using user1/pass1, you can also try to access /admin/index, which will lead you to unauthorize.jsp due to a lack of permission.

Enable annotation-based support

The basic integration requires that we define the permission controls that the URL needs to apply in filterChainDefinitions of the ShiroFilterFactoryBean. It’s not always that flexible. Shiro provides annotations that we can use with Spring integration. It allows us to annotate a Class or Method that needs permission control to define the permissions needed to access that Class or Method. All methods in the Class need permissions to be called (note that they need to be external calls, which is a limitation of dynamic proxies). To use these annotations we need to add the following two bean definitions to Spring’s bean container so that we can use the annotation definitions to determine whether the user has the corresponding permissions at run time. This is done through Spring’s AOP mechanism. If you don’t have a special understanding of Spring AOP, you can refer to the author’s Introduction to Spring AOP column in ITEye. The following two bean definition, AuthorizationAttributeSourceAdvisor is defined a Advisor, it will be based on the Shiro offers annotation configuration method to intercept, check permissions. DefaultAdvisorAutoProxyCreator is provided for labeling with Shiro provides access control annotation Class to create a proxy object, and applications to intercept the target method invocation AuthorizationAttributeSourceAdvisor function. When the intercept a request of the user, the user does not have corresponding method or class on permissions, will throw org.. Apache shiro. Authz. AuthorizationException anomalies.

<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator " 
    depends-on="lifecycleBeanPostProcessor "/>
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor ">
    <property name="securityManager " ref="securityManager "/>
</bean>Copy the code

If we have defined in the bean container < aop: config / > or < aop: aspectj autoproxy / >, you can no longer define DefaultAdvisorAutoProxyCreator. Because the front two situations will automatically add and DefaultAdvisorAutoProxyCreator similar beans. More introduction about DefaultAdvisorAutoProxyCreator can also refer to the Spring Aop to automatically create the principle of proxy objects in this blog.

Shiro provides the following permission control notes:

  • RequiresAuthentication: Requires the user to be authenticated in the current session, that is, logged in with username/password, excluding automatic login with RememberMe.
  • RequiresUser: The user must be authenticated, either through username/password login in this session, or automatically login through a RememberMe.
  • RequiresGuest: Requires that the user is not logged in.
  • RequiresRoles: Requires the user to have a specified role.
  • RequiresPermissions: Requires the user to have specified permissions.

The first three are well understood, while the last two are similar. For example, I take @Requirespermissions. First let’s change the Realm defined above and add permissions to role. So our user1 will have perm1, perm2, and perm3 permissions, and User2 will have perM1, perm3, and perM4 permissions.

<bean id="realm " class="org.apache.shiro.realm.text.TextConfigurationRealm ">
    <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>Copy the code

RequiresPermissions can be added to a method to specify the permissions that it needs to have when calling that method. In the following code, we specify that we must have perm1 when accessing /perm1. At this point user1 and user2 can access it.

@RequestMapping("/perm1 ")
@RequiresPermissions("perm1 ")
public Object permission1() {
    return "permission1 ";
}Copy the code

If you need to specify that you must have multiple permissions to access a method, you can specify the permissions as an array (the array attribute on the annotation does not need to increase the parentheses when specifying a single one, but increases the parentheses when specifying multiple ones). For example, we specify that the user must have both perm1 and perm4 permissions when accessing /perm1AndPerm4. At this point, only user2 can access it because it is the only one that has both perm1 and perm4.

@RequestMapping("/perm1AndPerm4 ")
@RequiresPermissions({"perm1 ", "perm4 "})
public Object perm1AndPerm4() {
    return "perm1AndPerm4 ";
}Copy the code

If multiple permissions are specified, the relationship between these permissions is and by default, that is, you need to have all the specified permissions at the same time. If only one of the specified permissions is required for access, then we can specify the relationship between permissions as OR using logical= logical. OR. For example, we specify that only perm1 or perm4 permissions are required to access /perm1OrPerm4, so that user1 and user2 can access the method.

@RequestMapping("/perm1OrPerm4 ")
@RequiresPermissions(value={"perm1 ", "perm4 "}, logical=Logical.OR)
public Object perm1OrPerm4() {
    return "perm1OrPerm4 ";
}Copy the code

RequiresPermissions can also be annotated on a Class, indicating that permissions are required for external access to methods in the Class. For example, we specify perm2 at the Class level and no perm2 at the index() method, but we still need Class level permissions to access the method. Only User1 will be accessible at this point.

@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; }}Copy the code

When both Class and method levels have @RequiresperMissions, the method level takes higher precedence, and only permissions required by the method level will be verified at this point. Below we specify that perm2 is required at the Class level and perm3 is required at the method level, so only perm3 is required to access the index() method when accessing /foo. So both user1 and user2 can now 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; }}Copy the code

However, if we added @requiresRoles (“role1 “) on the Class to specify that we need to own role Role1, then accessing /foo would require @requirespermissions (“perm3 “on the Class and @requirespermissions (“perm3 “) on the index() method “) specifies perm3 permissions. Since RequiresRoles and RequiresPermissions belong to different dimension permission definitions, Shiro will verify each of them, but only the method definition will be used if both classes and methods have annotations with the same type of permission control 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; }}Copy the code

Although the sample uses only RequiresPermissions, the use of the other permissions control annotations is similar. For those of you who are interested, use the other annotations yourself.

Based on the principle of annotation control permissions

For example, @Requirespermissions we specified permissions that were static. One of the main purposes of this article was to introduce a way to extend the implementation so that the specified permissions could be dynamic. But before we can extend it, we need to know how it works at the bottom, how it works. So let’s take a look at how Shiro integrated Spring with @Requirespermissions. In enable support for @ RequiresPermissions we define the bean as follows, this is an Advisor, it inherited from StaticMethodMatcherPointcutAdvisor, Its Method matching logic requires only a few Shiro permission control annotations on a Class or Method, and the processing logic after interception is specified by the corresponding Advice.

<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor ">
    <property name="securityManager " ref="securityManager "/>
</bean>Copy the code

The following is a source of AuthorizationAttributeSourceAdvisor. We can see that passed in the constructor setAdvice () specifies the AopAllianceAnnotationsAuthorizingMethodInterceptor this Advice implementation class, this is based on the realization of the 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; }}Copy the code

AopAllianceAnnotationsAuthorizingMethodInterceptor source code is as follows. The invoke method of its implementation’s MethodInterceptor interface calls the invoke method of its parent class. At the same time we want to see in its constructor created some AuthorizingAnnotationMethodInterceptor implementation, the core of which is the realization of access control, Will we can pick out the PermissionAnnotationMethodInterceptor implementation class to see the 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 AuthenticatedAnnotationMethodInterceptor(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); }}Copy the code

By looking at the parent’s Invoke method implementation, we end up seeing that the core logic is to call the assertAuthorized method, And the realization of this Method (the source code below) is, in turn, determine whether the configuration AuthorizingAnnotationMethodInterceptor support current Method permissions check (by judging whether the Class or Method with its support for annotations), When supported, its assertAuthorized method is called for permission verification, And what will AuthorizingAnnotationMethodInterceptor call AuthorizingAnnotationHandler assertAuthorized method.

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); }}}}Copy the code

Then we back to see the definition of AopAllianceAnnotationsAuthorizingMethodInterceptor PermissionAnnotationMethodInterceptor, its source code is as follows. Combining AopAllianceAnnotationsAuthorizingMethodInterceptor source and PermissionAnnotationMethodInterceptor source, We can see that specifies the PermissionAnnotationHandler and SpringAnnotationResolver PermissionAnnotationMethodInterceptor in this time. PermissionAnnotationHandler is AuthorizingAnnotationHandler a subclass. So our final access control by PermissionAnnotationHandler assertAuthorized implementation decisions.

public class PermissionAnnotationMethodInterceptor extends AuthorizingAnnotationMethodInterceptor { public PermissionAnnotationMethodInterceptor() { super( new PermissionAnnotationHandler() ); } public PermissionAnnotationMethodInterceptor(AnnotationResolver resolver) { super( new PermissionAnnotationHandler(), resolver); }}Copy the code

Next we see PermissionAnnotationHandler assertAuthorized method, the complete code is as follows. As you can see from the implementation, it gets the configured permissions from the Annotation, which in this case is the RequiresPermissions Annotation. And when we do permission checks, we’re going to use the text value that we specified when we defined the annotation, and we’ll start with that when we extend it.

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 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]);
            
        }
    }
}Copy the code

Through introducing the front we know PermissionAnnotationHandler assertAuthorized method parameters of the Annotation is the AuthorizingAnnotationMethodInterceptor Authori the call ZingAnnotationHandler’s assertAuthorized method. The source code is as follows. From the source code, we can see that the Annotation is obtained by 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; }}Copy the code

Following this direction, we eventually find the SpringAnnotationResolver’s getAnnotation method implementation, which looks like this. As you can see from the code below, it looks for annotations on Method first. If it does not find annotations on Method, it looks for annotations on the Class of the current Method call. You can also see why we previously defined the same type of permission control annotation on both Class and Method when it is in effect on Method, and when it is in effect on Method when it is in effect on its own.

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); }}Copy the code

Through the above source code reading, I believe that the reader for Shiro after the integration of Spring support for the principle of permission control annotations have a more in-depth understanding. The source code posted above is only part of the author think more core, there is a detailed understanding of the complete content please readers along the author mentioned to read the complete code. After understanding the principle of annotation-based permission control, readers can also extend it according to their actual business needs.

The extension uses Spring EL expressions

Suppose you now have an internal interface that has a query method that takes a parameter type. So let’s simplify this a little bit by assuming that you just take one argument, and then you get different results for different values.

public interface RealService {

    Object query(int type);
    
}Copy the code

This interface is open to the outside world, and 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);
}Copy the code

The above interface service has the permission for each type of query. Not every user can use each type of query, but must have the corresponding permission. Therefore, we need to add permission control for the above processor method, and the permissions required to control change dynamically with the parameter type. Suppose that each permission on type is defined in the form query:type, such as query:1 for type=1 and query:2 for type=2. In the absence of Spring integration, we would do the following:

@RequestMapping("/service/{type} ")
public Object query(@PathVariable("type ") int type) {
    SecurityUtils.getSubject().checkPermission("query: " + type);
    return this.realService.query(type);
}Copy the code

But with the Spring integration, the above approach is so coupled that we would prefer to use integrated annotations for permission control. For the scenario above, we would have preferred @RequiresperMissions to specify the required permissions, but the permissions defined for @RequiresperMissions are static text, fixed. It doesn’t meet our dynamic needs. At this point you might be thinking that we could split the Controller handler into multiple, separate permission controls. Like this:

@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); }Copy the code

This is fine when the range of values for type is small, but when there are 200 possible values like the one above, it’s a bit cumbersome to break them down and define individual processor methods and permissions. The other thing is that if the value of type changes in the future, we have to add new handler methods. So it was best for @RequiresperMissions to support dynamic permission definitions while maintaining support for static definitions. Through the analysis we know that in front of the starting point is PermissionAnnotationHandler, and it is not to provide for the expansion of the permission check. If we want to extend it the easy way is to replace it all. But we need to deal with the dynamic permissions are associated with the method parameters, and PermissionAnnotationHandler is less than the method parameters, therefore we can’t replace PermissionAnnotationHandler directly. PermissionAnnotationHandler consists of PermissionAnnotationMethodInterceptor calls, In its parent class AuthorizingAnnotationMethodInterceptor assertAuthorized method called PermissionAnnotationHandler is access to the method parameter. Therefore we can choose the extension points on PermissionAnnotationMethodInterceptor class, we also need to make it as a whole to replace. Spring’s EL expressions support parsing method parameter values. Here we chose to introduce Spring’s EL expression, which can be used to introduce method parameters when permissions are defined for @Requirespermissions. And for static text. Spring’s EL expression template is introduced here. You can refer to this blog post about Spring’s EL expression template. We define our own PermissionAnnotationMethodInterceptor, it inherited from PermissionAnnotationMethodInterceptor, rewrite assertAuthoried method, Method of logic of the implementation of the logic reference PermissionAnnotationHandler, but used by @ RequiresPermissions permissions in the definition, Is the result of parsing the EvaluationContext based on the currently invoked method using the Spring EL expression. The following is our own PermissionAnnotationMethodInterceptor definition.

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); 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]); }}}Copy the code

After defines its own PermissionAnnotationMethodInterceptor, We need to replace the original PermissionAnnotationMethodInterceptor PermissionAnnotationMethodInterceptor for ourselves. According to the mentioned earlier Shiro integration after Spring using the @ RequiresPermissions principle we know by AopAllianceAnnotationsAuthor PermissionAnnotationMethodInterceptor is annotated IzingMethodInterceptor specified, and the latter is the designated by the AuthorizationAttributeSourceAdvisor. For that we need to define the definition by displaying AopAllianceAnnotationsAuthorizingMethodInterceptor AuthorizationAttributeSourceAdvisor way according to the definition of the Authoriz IngAnnotationMethodInterceptor, Then put own PermissionAnnotationMethodInterceptor SelfAuthorizingAnnotationMethodInterceptor replacement for our custom. The definition after replacement is as follows:

<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor "> <property name="securityManager " ref="securityManager "/> <property name="advice "> <bean class="org.apache.shiro.spring.security.interceptor.AopAllianceAnnotationsAuthorizingMethodInterceptor "> <property name="methodInterceptors "> <util:list> <bean class="org.apache.shiro.authz.aop.RoleAnnotationMethodInterceptor " c:resolver-ref="springAnnotationResolver "/> <! - use a custom PermissionAnnotationMethodInterceptor - > < bean class="com.elim.chat.shiro.SelfPermissionAnnotationMethodInterceptor " c:resolver-ref="springAnnotationResolver "/> <bean class="org.apache.shiro.authz.aop.AuthenticatedAnnotationMethodInterceptor " c:resolver-ref="springAnnotationResolver "/> <bean class="org.apache.shiro.authz.aop.UserAnnotationMethodInterceptor " c:resolver-ref="springAnnotationResolver "/> <bean class="org.apache.shiro.authz.aop.GuestAnnotationMethodInterceptor " c:resolver-ref="springAnnotationResolver "/> </util:list> </property> </bean> </property> </bean> <bean id="springAnnotationResolver " class="org.apache.shiro.spring.aop.SpringAnnotationResolver "/>Copy the code

To demonstrate the dynamic permissions of the previous example, we adjust the role-permission relationship so that Role1, Role2, and Role3 have query:1, Query :2, and Query :3, respectively. User1 will now have the query:1 and query:2 permissions.

<bean id="realm " class="org.apache.shiro.realm.text.TextConfigurationRealm ">
    <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>Copy the code

At this point in time specified in @Requirespermissions you can use the syntax supported by Spring EL expressions. Because we already specified when defining SelfPermissionAnnotationMethodInterceptor applications based on template expression parsing, the permissions defined in the text will be parsed as text, By default, the dynamic parts need to be wrapped with #{prefix and} suffixes (this prefix and suffix can be specified, but the default is fine). Variables can be referenced using # prefixes in the dynamic part, and method parameters can be referenced using parameter names or p-parameter indexes in method-based expression parsing. So the @requirespermissions definition for the query method above where we need dynamic permissions is as follows.

@RequestMapping("/service/{type} ")
@RequiresPermissions("query:#{#type} ")
public Object query(@PathVariable("type ") int type) {
    return this.realService.query(type);
}Copy the code

User1 will be OK to access /service/1 and /service/2, but will be told no permission to access /service/3 and /service/300 because user1 does not have query:3 and query:300 permissions.