An extension to shiro’s custom annotations

According to my last article, the discussion of permission design involves the separation of the front and back ends, and the disconnection of the page and API from the surface of the table, and the finding of other ways to do so. Here we mainly take Shiro’s approach of custom annotations. This article mainly addresses the following problems.

  1. How to logically associate pages with API interfaces.
  2. The use of Shiro’s own annotations.
  3. How to write custom annotations.

How to logically associate a page with an API

Page and interface tables are ultimately associated with permission tables in a table-to-table structural relationship (see my previous article, “A Conversation about Permission Design” for details).

Business module
Operation type

  • Business module
    • Concept: Service modules in the system are abstracted into a kind of data, which can be expressed in the form of strings. For example, role management corresponds to role-manage, user management corresponds to user-manage, etc. We divide the existing business modules in the system according to the principle of “least privilege”, and finally form a batch of data that can be allocated.
    • Usage principle: API interface and page as well as function from essentially speaking, have logical relation with business module, therefore, we can carry on logical matching to API interface and page (as well as function point), judge the relation between page and interface.
  • Operation type
    • Concept: All operation types in the system are abstracted into a kind of data, which can also be expressed in the form of strings, for example: Add corresponds to add, allot corresponds to allocate, and so on. We divide all operation types in the system according to business modules through “data license”, and finally form a batch of data that can be allocated.
    • Usage principle: The page is to display, the function point is the action, and the interface is the resource provision of the final action. “Service module” determines the resource to be retrieved, and “Operation type” determines the resource usage mode. You can roughly judge whether the interface triggered by the function point of the page is within the authentication.

Now put forward these two concepts, their final actual use is what, we first from the following perspectives to think about.

  1. Is the data in the page table or API table in the database valid?
  2. Does the actual use of a page or interface depend on the existence of functionality or data in a database table?
  3. Is the database the only way to store “control objects” in the permission structure?

First, “control objects” can be stored not only in the database, but also in code and configuration files, not necessarily in the database. Then answer the second question, when the database has interface information, and the server did not develop this interface, the database itself has a problem, or, the database added interface must be deployed on the server interface to take effect; Then comes the first problem, the data in the database’s table of “control objects” is not necessarily valid. So we can come up with the following solutions

  1. We can annotate the interface with “business module” and “operation type” data, both of which can be stored in constant classes.
  2. Add the Business Module and Operation Type fields when adding the create page table structure and page function table structure to the database.
  3. Business module and operation type information can be stored in a dictionary table in the database.
  4. The addition of modules or operations must bring the addition of interfaces, so it will bring a system deployment activity, the operation and maintenance costs can not be reduced, and can not be reduced by table structure.

However, this solution is only suitable for non-strongly controlled interface projects, where the page is still bound to the interface, despite the heavy operation and maintenance costs. In addition, you can classify interfaces based on interface routing rules, for example, / API /page/ XXXX /(used only for pages), / API /mobile/ XXXXX (used only for mobile terminals). These interfaces can be classified only for authentication without authorization.

The use of Shiro’s own annotations

After a theoretical idea is recognized, the rest is put into technical practice. We adopt Apache Shiro security framework and apply it in the Spring Boot environment. A brief description of shiro’s notes follows.

Annotation name role
@RequiresAuthentication Class, method, and instance of. When called, the current subject must be authenticated.
@RequiresGuest Class, method, and instance of. When called, the Subject can be in guest state.
@RequiresPermissions Class, method, and instance of. When called, you need to determine whether the suject contains Permission (Permission information) from the current interface.
@RequiresRoles Class, method, and instance of. When called, determine whether the Subject contains roles (Role information) from the current interface.
@RequiresUser Class, method, and instance of. When called, you need to determine whether the Subject is a user in the current application.
    /** * 1. The current interface needs to be authenticated *@return* /
    @RequestMapping(value = "/info",method = RequestMethod.GET)
    @RequiresAuthentication
    public String test(a){
        return "Congratulations, you got the parameters.";
    }
    
    /** * 2.1. The current interface needs permission verification (including role query or menu query) *@return* /
    @RequestMapping(value = "/info",method = RequestMethod.GET)
    @RequiresPermissions(value={"role:search"."menu":"search"},logical=Logical.OR)
    public String test(a){
        return "Congratulations, you got the parameters.";
    }
    
    /** * 2.2. The current interface needs permission verification (including role query and menu query) *@return* /
    @RequestMapping(value = "/info",method = RequestMethod.GET)
    @RequiresPermissions(value={"role:search"."menu":"search"},logical=Logical.OR)
    public String test(a){
        return "Congratulations, you got the parameters.";
    }
    
    /** * 3.1. The current interface must pass role verification (including the role of admin) *@return* /
    @RequestMapping(value = "/info",method = RequestMethod.GET)
    @RequiresRoles(value={"admin"})
    public String test(a){
        return "Congratulations, you got the parameters.";
    }
    
    /** * 3.2. The current interface needs to be verified by roles and permissions (including the role of admin and the role query or menu query) *@return* /
    @RequestMapping(value = "/info",method = RequestMethod.GET)
    @RequiresRoles(value={"admin"})
    @RequiresPermissions(value={"role:search"."menu":"search"},logical=Logical.OR)
    public String test(a){
        return "Congratulations, you got the parameters.";
    }
    
    
Copy the code

For our purposes, @Requirespermissions and @RequiresAuthentication would have done just that. At the end of the previous section, we used a combination of business modules and operations to decoupage the page from the API interface. This is in line with apache Shiro’s approach. But for @requiresRoles, we want to avoid it, because there are too many roles to make the role name explicitly unique in the interface (it’s hard to specify that the interface belongs to a role, but it’s possible to know that the interface belongs to some operation of some business module).

Now let’s review the process.

Shiro permission validation process

How do I write custom annotations

But simply having these five annotations in Shiro is definitely not enough. In the actual use process, according to the demand, we will add our own unique business logic in the authorization authentication, we can use custom annotations for convenience. This approach is not only applicable to Apache Shiro, many other frameworks such as Hibernate Validator, SpringMVC, and even we can write a validation framework to verify permissions in AOP, which is fine. So custom annotations can be very useful. But here, I’m just implementing custom annotations for Shiro based on it.

  • Defining annotation classes
/** * The annotation of the interface used for authentication. The default combination is "or" */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Auth {
    /** * Business module *@return* /
    String[] module(a);/**
     * 操作类型
     */
    String[] action();

}

Copy the code
  • Define the annotation handler class
/** * The operation class for the Auth annotation */
public class AuthHandler extends AuthorizingAnnotationHandler {


    public AuthHandler(a) {
        // Write an annotation
        super(Auth.class);
    }

    @Override
    public void assertAuthorized(Annotation a) throws AuthorizationException {
        if (a instanceof Auth) {
            Auth annotation = (Auth) a;
            String[] module = annotation.module(a); String[] action = annotation.action();//1. Get the current topic
            Subject subject = this.getSubject();
            //2. Verify whether the permission of the current interface is included
            boolean hasAtLeastOnePermission = false;
            for(String m:module) {for(String ac:action){
                    // Use hutool's string utility class
                    String permission = StrFormatter.format("{}, {}",m,ac);
                    if(subject.isPermitted(permission)){
                        hasAtLeastOnePermission=true;
                        break; }}}if(! hasAtLeastOnePermission){throw new AuthorizationException("Do not have permission to access this interface"); }}}}Copy the code
  • Define shiro interception handling classes
/** * interceptor */
public class AuthMethodInterceptor extends AuthorizingAnnotationMethodInterceptor {


    public AuthMethodInterceptor(a) {
        super(new AuthHandler());
    }

    public AuthMethodInterceptor(AnnotationResolver resolver) {
        super(new AuthHandler(), resolver);
    }

    @Override
    public void assertAuthorized(MethodInvocation mi) throws AuthorizationException {
        // Verify permissions
        try {
            ((AuthHandler) this.getHandler()).assertAuthorized(getAnnotation(mi));
        } catch (AuthorizationException ae) {
            if (ae.getCause() == null) {
                ae.initCause(new AuthorizationException("Current method does not pass authentication:" + mi.getMethod()));
            }
            throwae; }}}Copy the code
  • Defines Shiro’s AOP aspect class
/** * the AOP aspect of Shiro */
public class AuthAopInterceptor extends AopAllianceAnnotationsAuthorizingMethodInterceptor {
    public AuthAopInterceptor(a) {
        super(a);// Add a custom annotation interceptor
        this.methodInterceptors.add(new AuthMethodInterceptor(newSpringAnnotationResolver())); }}Copy the code
  • Define Shiro’s custom annotation launcher class
/** * start custom annotations */
public class ShiroAdvisor extends AuthorizationAttributeSourceAdvisor {

    public ShiroAdvisor(a) {
        // You can add more than one here
        setAdvice(new AuthAopInterceptor());
    }

    @SuppressWarnings({"unchecked"})
    @Override
    public boolean matches(Method method, Class targetClass) {
        Method m = method;
        if(targetClass ! =null) {
            try {
                m = targetClass.getMethod(m.getName(), m.getParameterTypes());
                return this.isFrameAnnotation(m);
            } catch (NoSuchMethodException ignored) {

            }
        }
        return super.matches(method, targetClass);
    }

    private boolean isFrameAnnotation(Method method) {
        return null != AnnotationUtils.findAnnotation(method, Auth.class);
    }
}

Copy the code

The general order of thought is: define annotation classes (defining business-usable variables) -> define annotation handling classes (doing business logic processing with variables in annotations) -> define annotation interceptors -> define aop aspect classes -> finally define Shiro’s custom annotation enabling classes. Other custom annotations are written along similar lines.