Project-driven learning, practice to test knowledge

preface

The concept of permission can be seen everywhere: the level is not enough to enter a certain forum, I can only like and comment on other people’s posts but cannot delete or modify them, some I can read some I can not see in moments, some can see the latest news in seven days, some can see all the latest news, and so on.

Each system has different rights and functions, each has its own business characteristics, and the design of rights management has its own characteristics. However, no matter what kind of permission design, it can be roughly classified into three kinds: page authority (menu level), operation authority (button level), data authority, according to the dimension is: coarse granular authority, fine granular authority.

This article focuses on permissions, and I will omit non-permission related code for the sake of demonstration, such as login authentication, password encryption, and so on. If you don’t know much about Authentication, you can read my last article [Project Practice]. Before using the security framework, I want you to manually perform Authentication. As in the previous article, the purpose of this article is to get you to the heart of Authorization, so you can take Authorization by hand without using a security framework. Once the core is clear, what security framework is easy to understand and use.

I will start from the most simple, the most basic explanation, from shallow to deep, step by step with you to achieve each function. After reading the article, you will learn:

  • Core concepts of permission authorization
  • The design and implementation of page authority, operation authority and data authority
  • Evolution and use of the authority model
  • Interface scanning andSQLintercept

And this article all code, SQL statements are put on Github, clone can run, not only have back-end interface, front-end page is also some oh!

Basic knowledge of

Authentication verifies user identity, and Authorization verifies whether a user can ask for a resource. For example, if you enter your account password to log into a forum, this is authentication. You this account is administrator wants to enter so which plate to enter which plate, this is authorization. Permission authorization usually occurs after login authentication is successful, confirming who you are and then confirming what you can access. Here’s another example to make it clear:

System: Who are you?

User: I zhang SAN, this is my account password you see

System: ouch, the account password is right, look be outside law maniacal zhang SAN! What are you doing (login authentication)

Zhang SAN: I want to go into the vault

System: Fuck off, you can only go to jail, nowhere else (authorization)

You can see the concept of authority is not difficult at all, it is like a firewall, protect resources from infringement (yes, usually we always say that the network firewall is also a manifestation of authority, have to say that the name of the network firewall is really appropriate). Now, I think it’s pretty clear what permissions are, which is to protect resources. No matter what kind of functional requirements, the core of authority is around resources. Unable to access the forum section, which is a resource; Certain areas cannot be accessed. In this case, areas are resources.

The first step in designing a permission system is to think about what resource to protect, and then how to protect that resource. This sentence is the focus of this article, I will explain this sentence in detail!

implementation

We use SpringBoot to build Web projects, MySQL and Mybatis- Plus for data storage and operation. Here are the necessary dependency packages to use:

<dependencies>
    <! -- Web dependency package, required for Web applications -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <! MySQL > select * from 'MySQL';
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    <! --MyBatis plus, ORM framework, access and manipulate database -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.4.0</version>
    </dependency>
</dependencies>
Copy the code

Before you can design a table related to permissions, you must have a basic user table with three simple fields: primary key, username, password:

I will not write the entity class and SQL table construction sentence, you can see the structure of the table how to write (GIthub I put the complete SQL table construction file).

Let’s start with a very simple permission control!

Permissions page

Page permission is very easy to understand, is the permission of the user to access the page, not the permission of the user can not access, it is the dimension of the entire page, the control of the permission is not so fine, so it is a coarse granular permission.

The most intuitive example is that all menus are displayed for the authorized user and only part of the menu is displayed for the unauthorized user:

These menus are corresponding to a page, control the navigation menu is equivalent to control the page entrance, so the page permissions can also be called menu permissions.

Permissions on the core

As I said before, the first step in designing a permissions system is to think about what resource to protect, and the resource to protect page permissions is definitely the page. A page (menu) corresponds to a URI address, and when the user logs in, determine which page permissions the user has, and automatically know what navigation menu to render! The design of these clear tables naturally emerged:

This resource table is very simple but sufficient for now, assuming our page/menu URI map is as follows:

If we want to set the user’s permission, we only need to match the user ID with the URI:

User id 1 has all privileges, and user ID 2 only has data management privileges. At this point, we have completed the database table design of page permissions!

The data just sits there and doesn’t work, so the next thing we need to do is write code to use it. The code implementation is divided into back-end and front-end. When there is no separation of the front-end and back-end, the logic processing and page rendering are carried out in the back-end, so the overall logical link is like this:

After the user logs in to the page, let’s write the page interface:

@Controller // Note that this is not @restController, which means that all page views are returned
public class ViewController {
    @Autowired
    private ResourceService resourceService;
    
    @GetMapping("/")
    public String index(HttpServletRequest request) {
        // Menu name mapping dictionary. Key is the URI path, and value is the menu name, so that the view can render the menu name according to the URI path
        Map<String, String> menuMap = new HashMap<>();
        menuMap.put("/user/account"."User Management");
        menuMap.put("/user/role"."Rights Management");
        menuMap.put("/data"."Data Management");
        request.setAttribute("menuMap", menuMap);
        
        // Get all the page permissions of the current user and put the data into the Request object for the view to render
        Set<String> menus = resourceService.getCurrentUserMenus();
        request.setAttribute("menus", menus);
        return "index"; }}Copy the code

Index.html:

<! This syntax is thymeleaf syntax, which is a back-end template engine technology like JSP.
<ul>
    <! Let everyone see the home page, just render -->
    <li>Home page</li>
    
    <! Render the corresponding menu according to the permission data -->
    <li th:each="i : ${menus}">
        [[${menuMap.get(i)}]]
    </li>
    
</ul>
Copy the code

I’m just going to show you how to render it, so I’m not going to write the whole code, but I’m going to focus on the idea, not the details of the code, okay

When the front and back ends are not separated, the basic functions of the page permissions have been completed.

Now the backend is only responsible for providing JSON data, and the page rendering is the front-end’s job, so the overall logical link has changed:

Then, when the user logs in successfully, the back end will return the user’s permission data to the front end, which is our login interface:

@RestController // Note that @restController means that all interfaces of this class return JSON data
public class LoginController {
    @Autowired
    private UserService userService;

    @PostMapping("/login")
    public Set<String> login(@RequestBody UserParam user) {
        // Simply return a set of permission paths
        returnuserService.login(user); }}Copy the code

Specific business methods:

@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private ResourceMapper resourceMapper;
    @Autowired
    private UserMapper userMapper;

    @Override
    public Set<String> login(UserParam userParam) {
        // Query user data from the database according to the account password passed from the front end
        Select * from user where user_name = #{userName} and password = #{password}
        User user = userMapper.selectByLogin(userParam.getUsername(), userParam.getPassword());
        if (user == null) {
            throw new ApiException("Wrong account or password");
        }
        
        // Returns the set of permission paths for this user
        Select path from resource where user_id = #{userId}
        returnresourceMapper.getPathsByUserId(user.getId()); }}Copy the code

The front-end will receive JSON data from the back end after successful login:

[
    "/user/account"."/user/role"."/data"
]
Copy the code

In this case, the back end does not need to pass the menu name mapping to the front end as before, and the front end stores a dictionary of mappings itself. The front end stores this permission locally (such as LocalStorage), and then renders the menu based on the permission data, completing the permission function in the split mode of the front and back ends. Let’s take a look at the effect:

So far, the basic logical link of page permissions has been introduced, isn’t it very simple? After the basic logic is clear, what is left is just a very common add, delete, change and check: when I want to increase the user’s permission data, I will increase the user’s permission data, I want to make the user’s permission smaller, I will delete the user’s permission data… Next we complete this step, let the system users to be able to manage permissions, otherwise do everything to directly operate the database that is certainly not possible.

First of all, the user must be able to see a list of data before they can operate. I added some data to show the effect:

Here pagination, new account, delete account code how to write I will not explain, on the permission to edit the interface:

@RestController
public class LoginController {
    @Autowired
    private ResourceService resourceService;
    
    @PutMapping("/menus")
    private String updateMenus(@RequestBody UserMenusParam param) {
        resourceService.updateMenus(param);
        return "Operation successful"; }}Copy the code

Accepting the parameters passed from the front end is as simple as a user ID and a set of menu paths to set:

// Omit getters and setters
public class UserMenusParam {
    private Long id;
    private Set<String> menus;
}
Copy the code

The code for the business class is as follows:

@Override
public void updateMenus(UserMenusParam param) {
    // Delete the original permission data of the user based on the user ID
    resourceMapper.removeByUserId(param.getId());
    // If the set of permissions is empty, all permissions will be deleted
    if (Collections.isEmpty(param.getMenus())) {
        return;
    }
    // Add permission data based on the user ID
    resourceMapper.insertMenusByUserId(param.getId(), param.getMenus());
}
Copy the code

The SQL statement for deleting permission data and adding permission data is as follows:

<mapper namespace="com.rudecrab.rbac.mapper.ResourceMapper">
    <! Delete all privileges from user based on user ID -->
    <delete id="deleteByUserId">
        delete from resource where user_id = #{userId}
    </delete>
    
    <! Select * from user where user id = >
    <insert id="insertMenusByUserId">
        insert into resource(user_id, path) values
        <foreach collection="menus" separator="," item="menu">
            (#{userId}, #{menu})
        </foreach>
    </insert>
</mapper>
Copy the code

This completes the function of editing permission data:

As you can see, the root user can only access data management. After editing its permission, it can also access account management. Now our page permission management function is complete.

Doesn’t it feel very simple, we just use two tables to complete a permission management function.

The ACL model

Two tables are very convenient and easy to understand. The system is small and the amount of data is small, so it doesn’t matter. If the amount of data is large, it has its drawbacks:

  1. Great data duplication
    • Consumes storage resources. Such as/user/accountI have to store as many of these strings as I have access to. This is the simplest resource information. There is only one path. Some resources have a lot of information: resource name, type, level, introduction, etc
    • Changing resources is too costly. Such as/dataI want to change to/info, then the existing access data will have to be changed
  2. Poor design
    • Resources cannot be visually described. Just now we only have three resources, if I want to add a fourth, a fifth… There is no way to kind of resources, because now resources are dependent on the user and exist, can not be stored independently
    • The definition of the watch is not clear. Now ourresourceTables do not describe resources so much as the relationship between users and resources.

In order to solve the above problems, we should improve the current table design to clarify the relationship between resources and users and resources. The relationship between users and resources is many-to-many. One user can have multiple permissions, and one permission can have multiple users. We generally use intermediate tables to describe this many-to-many relationship. Then the resource table is no longer used to describe relationships, only resources. So here comes our new table design: create intermediate tables and improve resource tables!

Select id, user_id, and path from user_id. Select id, user_id, and path from user_id. Then we add an additional name field to describe the resource name (optional). After the transformation, the resource table is as follows:

The contents of the table are specifically used to put resources:

Create an intermediate table to describe the relationship between users and permissions. The intermediate table simply stores user ids and resource ids:

The previous permission relationship is stored in the middle table like this:

The current data shows that user 1 has permissions 1, 2, and 3, that is, user 1 has permissions to manage accounts, roles, and data. User 2 has only the resource permission of user 3, that is, user 2 has the data management permission!

The entire table design is thus upgraded, and now our table is as follows:

Mysql > alter table user_Resource; mysql > alter table user_resource; mysql > alter table user_resource;

The key is that before we are operating on the resource table path string, before and after the permission information is also passed path string, now are changed to operate on the resource table ID (Java code remember also changed, here I will only demonstrate SQL).

For a separate explanation, how does the front end render the page based on the resource ID when the front end only passes the resource ID? How do you display the resource name based on this ID? This is because the front end stores a mapping dictionary, which contains resource information, such as which path id corresponds to, name and so on. After the front end gets the user ID, it can perform corresponding functions according to the dictionary.

There are two management modes of this mapping dictionary in the actual development. One is that the front end adopts the form of convention, and the front end itself builds the dictionary in the code. If there are any changes in the subsequent resources, the front end personnel can communicate with each other. There is also a back-end to provide an interface, the interface returns all the resource data, whenever the user login or enter the system home page when the front-end call interface synchronization resource dictionary! We are using this method now, so we need to write an interface:

/** * returns all resource data */
@GetMapping("/resource/list")
public List<Resource> getList(a) {
    The SQL statement is very simple: select * from resource
    return resourceService.list();
}
Copy the code

Now, our permissions are designed to look something like this. This mode of binding between users and permission resources is the ACL model, namely Access Control List. It is convenient and easy to understand, and suitable for the system with simple permission functions.

We took the heat and went on to upgrade the whole design!

RBAC model

I didn’t set too many permissions (navigation menus) for the sake of demonstration, so the whole permissions system seems to be quite convenient to use, but once the permissions are too many, the current design is a bit stretched. Suppose we have 100 permission resources, user A needs to set 50 permissions, and the three BCD users need to set the same 50 permissions, so I have to repeat 50 times for each user! For example, all the employees of the sales department have the same permissions. I have to set the permissions for each new employee step by step. If I change the permissions of the sales department, all the employees’ permissions have to be changed one by one, which is extremely complicated:

Any problem in computer science can be solved by adding an indirect intermediate layer

Right now our permissions are tied to the user, so we have to set up a unique set of permissions for each new user. User permissions are the same for many users, so I will wrap a layer to mask the relationship between users and permissions.

This allows new users to have a set of permissions that can be easily changed in the future by simply binding them to the encapsulation layer. This layer of encapsulation we call roles! Roles are easy to understand. Sales is a role, logistics is a role, roles are bound to permissions, and users are bound to roles, as shown in the figure above.

Now that we’ve added a layer of characters, our table design has to change as well. There is no doubt that there must be a role table to describe the role information, simple two fields primary key ID, role name, add two role data to demonstrate:

Role_resource; roLE_role; roLE_role; roLE_role; roLE_role; roLE_role; roLE_role; roLE_role; roLE_role; roLE_role; roLE_role; roLE_role; roLE_role; roLE_role; roLE_role; roLE_role

The preceding data shows that role 1 (super administrator) has three permission resources, and role 2 (data administrator) has only one permission resource. Then user 1 has the super administrator role and user 2 has the data administrator role:

If there is another user who wants to have all the privileges of the super administrator, bind the user to the super administrator role. This completes the table design, and now our database table is as follows:

This is the famous and popular RBAC model, role-based Access Controller! It can satisfy most permission requirements and is one of the most commonly used permission models in the industry. Light said not to practice false handle, now the table is also designed, let’s improve our code and the front end of the tune up, complete a role-based permission management system!

We now have three entities in the system: users, roles, and resources (permissions). Before, we had a user page, and we could manage permissions on that page. Now we have the concept of roles, so we have to add a role page:

I’m not going to talk about the old paging, adding, deleting code, but I’m going to talk about the permission code.

Before our user page is the direct operation permission, now we want to change to the operation role, so the SQL statement should be written as follows:

<mapper namespace="com.rudecrab.rbac.mapper.RoleMapper">
    <! Add roles in batches by user ID -->
    <insert id="insertRolesByUserId">
        insert into user_role(user_id, role_id) values
        <foreach collection="roleIds" separator="," item="roleId">
            (#{userId}, #{roleId})
        </foreach>
    </insert>

    <! Delete all roles from user based on user ID -->
    <delete id="deleteByUserId">
        delete from user_role where user_id = #{userId}
    </delete>

    <! Select * from user id; select * from user ID;
    <select id="selectIdsByUserId" resultType="java.lang.Long">
        select role_id from user_role where user_id = #{userId}
    </select>
</mapper>
Copy the code

In addition to user actions on roles, we also need an interface that takes the user ID and directly obtains all permissions of that user so that the front end can render the page based on the current user’s permissions. Before, we joined resource and user_Resource to query all user permissions. Now, we join user_ROLE and ROLE_RESOURCE to get permission ID. The left side is our previous code and the right side is our modified code:

This completes the user part of the operation, and we move on to the role-related operations. The idea here is the same as before, the user before how to directly operate permissions, here role how to operate permissions:

<mapper namespace="com.rudecrab.rbac.mapper.ResourceMapper">
    <! Add privileges to a role by role id -->
    <insert id="insertResourcesByRoleId">
        insert into role_resource(role_id, resource_id) values
        <foreach collection="resourceIds" separator="," item="resourceId">
            (#{roleId}, #{resourceId})
        </foreach>
    </insert>

    <! Delete all privileges from role based on role ID -->
    <delete id="deleteByRoleId">
        delete from role_resource where role_id = #{roleId}
    </delete>

    <! Select * from user where role id = 1 -->
    <select id="selectIdsByRoleId" resultType="java.lang.Long">
        select resource_id from role_resource where role_id = #{roleId}
    </select>
</mapper>
Copy the code

Note that both the front and back ends pass the ID as well. Since it is id, the front end must have a mapping dictionary to render, so our two interfaces are essential:

/** * returns all resource data */
@GetMapping("/resource/list")
public List<Resource> getList(a) {
    The SQL statement is very simple: select * from resource
    return resourceService.list();
}

/** * returns all role data */
@GetMapping("/role/list")
public List<Role> getList(a) {
    The SQL statement is very simple: select * from role
    return roleService.list();
}
Copy the code

Now that we have dictionaries, methods for manipulating roles, and methods for manipulating permissions, we have completed page permissions based on the RBAC model:

The root user has the permission of the data administrator. At first, the data administrator can only see the data management page. Later, we add the permission of the account management page for the data administrator, root user can see the account management page without making any changes!

No matter how many tables, the core of permissions or the flow chart I showed before, the idea of mastering the model is OK.

I don’t know if you have noticed that in the mode of separation of the front and back ends, the back end will dump the permission data to the front end when logging in and then no longer care about it. If the user’s permission changes at this time, it is impossible to notify the front end, and the data stored in the front end is also easy to be directly tampered by users, so it is very insecure. Unlike unsplit, page requests have to go to the back end, which can easily determine the security of each page request:

@Controller
public class ViewController {
    @Autowired
    private ResourceService resourceService;
    
   	// All this logic can be put into the filter
    @GetMapping("/user/account")
    public String userAccount(a) {
        // Retrieve the permissions of the current logged-in user from the cache or database
		List<String> menus = resourceService.getCurrentUserMenus();
        
        // Check whether there is permission
        if (list.contains("/user/account")) {
             // If you have permission, return to normal page
        	return "user-account";
        }
        // Return 404 page without permission
        return "404"; }}Copy the code

First of all, permission data is stored in the back end, which may be directly tampered with by users. And every time the user visits the page, the back-end will query the data in real time, and when the user permission data changes, it can also be synchronized in real time.

Does that mean you have to admit defeat in front end separation mode? Of course not. In fact, there is a trick that the front end sends back the latest permission data to the front end for each backend request, which avoids this problem. However, this method puts too much pressure on the network and is not elegant or wise, so it is generally not done. The compromise is to retrieve permission data again when a user enters a page, such as the home page. However, this is not very safe, after all, as long as the user does not go to the home page it is useless.

So what’s the elegant, smart, and safe way to do this, which is to talk about permissions!

Operation permissions

Operation permissions are to treat operations as resources, such as delete operations, for some people and others not. On the back end, an operation is an interface. In the front end, the operation is often a button, so the operation permission is also called the button permission, is a kind of fine granular permission.

The button will not be displayed for those who do not have the delete permission, or the button will be disabled:

The front-end implementation of button permissions is still the same as the previous navigation menu rendering. Compare the current user’s permission resource ID with the permission resource dictionary. If there is permission, render it, and if there is no permission, do not render it.

The front-end logic about permissions is the same as before, so how can operation permissions be safer than page permissions? This security is mainly reflected in the back end, page rendering does not go to the back end, but the interface must go to the back end, it is easy to do as long as the back end, we only need to judge each interface for a permission is OK!

Basic implementation

We used to design for the page permissions, now we want to extend the existing resource table for a small extension, add a Type field to distinguish the page permissions and operation permissions

Here we use 0 for page permissions and 1 for action permissions.

After the table is extended, we next add data for the operation permission type. As mentioned earlier, an operation is an interface on the back end, so we should use the interface path as our permission resource.

DELETE:/API/user is divided into two parts. DELETE: indicates the request mode of the interface, such as GET, POST, etc. /API/user indicates the interface path, and the combination of the two can determine an interface request!

Now that we have the data, we can make a security judgment in the code, pay attention to the comment:

@RestController
@RequestMapping("/API/user")
public class UserController {... Omit the automatically injected service code@DeleteMapping
    public String deleteUser(Long[] ids) {
        // Get all permission paths and the permission paths owned by the current user
        Set<String> allPaths = resourceService.getAllPaths();
        Set<String> userPaths = resourceService.getPathsByUserId(UserContext.getCurrentUserId());
        
        // If the interface is included in all permission paths, then the interface needs permission processing.
        // Second check: Check whether the interface belongs to the permission scope of the current user. If not, it indicates that the interface user has no permission
        if (allPaths.contains("DELETE:/API/user") && !userPaths.contains("DELETE:/API/user")) {
            throw new ApiException(ResultCode.FORBIDDEN);
        }
        
        // This indicates that the interface user has permissions, and normal business logic processing is performed
        userService.removeByIds(Arrays.asList(ids));
        return "Operation successful"; }... Omit other interface declarations}Copy the code

After joint adjustment with the front end, the front end hides the corresponding operation button according to the permissions:

Buttons are hidden, but what if a user tampers with local permission data, causing buttons that shouldn’t be displayed to show up, or if the user learns that the interface bypassed the page and called itself? Anyway, he will eventually call our interface, so let’s call the interface to test the effect:

As you can see, bypassing the security judgment of the front end is also useless!

And then there’s the question we talked about earlier, if the current user permissions are changed, how do you synchronize with the front end in real time? For example, the role of user A has the delete permission at first, and then an administrator removes the permission. However, user A can still see the delete button if he does not log in again.

In fact, after the user has the operation permission, even if the user can see the button does not belong to their own security is not compromised, he clicked the button will still prompt no permission, just said that the user experience is a little bit less! Page, too, has only one container, used to carry data, and the data is to through the interface to invoke, demonstrates the paging data such as in the figure, we can be paged query interface also do a rights management, so that the user even if bypassed the permissions page, came to the account management section, so can’t see any data!

At this point, we are done with the button level permissions, isn’t that easy? Again: as long as you master the core idea, it’s really easy to implement, don’t think of complexity.

Readers who know my style will know that I’m up to the next level! Yes, the way we do it now is too crude and cumbersome. We are now manually added resource data, write an interface I need to manually add a data, to know that a system of hundreds of interfaces is too normal, so I manually add can not take off? That have what way, I write interface at the same time will automatically generate the resource data, that is I want to talk about the next interface scan!

Interface scanning

For SpringMVC RequestMappingInfoHandlerMapping provides a very convenient class, the class can get all your statement web interface information, after the get the rest of the matter is very simple, is through the batch code will interface information added to the database bai! However, we do not really want to add all interfaces to the permission resource, we want to generate permission resources for those interfaces that need permission processing, and some interfaces that do not need permission processing will not be generated. So we need to figure out a way to mark the interface as being permission managed!

Our interfaces are declared through methods, and the most convenient way to mark methods is to annotate them. Let’s first define a comment:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE}) // Indicates that the annotation can be added to a class or method
public @interface Auth {
    /** * Permission ID, which must be unique */
    long id(a);
    /** * Permission name */
    String name(a);
}
Copy the code

I’ll explain why this annotation is designed this way in a moment, but for now it’s important to know that as long as interface methods have this annotation, we are considered to need permission management:

@RestController
@RequestMapping("/API/user")
@auth (id = 1000, name = "user management ")
public class UserController {... Omit the automatically injected service code@PostMapping
    @auth (id = 1, name = "new user ")
    public String createUser(@RequestBody UserParam param) {... Omit business codereturn "Operation successful";
    }

    @DeleteMapping
    @auth (id = 2, name = "delete user ")
    public String deleteUser(Long[] ids) {... Omit business codereturn "Operation successful";
    }

    @PutMapping
    @auth (id = 3, name = "edit user ")
    public String updateRoles(@RequestBody UserParam param) {... Omit business codereturn "Operation successful";
    }
    
    @GetMapping("/test/{id}")
    @auth (id = 4,name = "to demonstrate path parameters ")
    public String testInterface(@PathVariable("id") String id) {... Omit business codereturn "Operation successful"; }... Omit other interface declarations}Copy the code

Before we talk about interface scanning and annotation design, let’s take a look at the final result.

As you can see from the above code, we add our custom Auth annotation to the class and method, and set the id and name values in the annotation. Database primary key ids are usually incremented. This is because there are many advantages to artificially controlling the primary key ID of a resource.

The first is the id and the interface path mapping special stability, if you want to use the increase, I an interface to start with the authorization id is 4, a lot of role bindings in the 4 above the resources, then I don’t need the business requirements for a period of time do rights management interface, so I will delete a period of time, the resources four subsequent add back again, However, when the data is added back, the ID will become 5, and the role bound to it will have to reset the resource, which is very troublesome! If the ID is fixed, I will add the interface permissions back, and all the permissions set before will take effect without perception, very very convenient. Therefore, the mapping between ID and interface path should be stable from the beginning and should not be changed easily!

As for the annotation of Auth on the class to facilitate modular management of interface permissions, a Controller class is regarded as a set of interface modules, and the final id of interface permissions is the module ID + method ID. If you think about it, if I didn’t do that, if I wanted to make sure that each interface had a unique permission ID, I would have to remember the ID of all the methods in each class, and set the new ID one by one. For example, the last method I set to 101, then I set to 102, 103… , as long as the attention is not set heavy. This class is 1000, the next class is 2000, and then all the methods in the class can be set independently according to 1, 2, 3, greatly avoiding the mental burden!

After introducing the design of annotations for so long, we will explain the concrete implementation of interface scanning! This scan must have happened when I finished writing the new interface and recompiled the package to restart the program! And only do a scan at the start of the program, it is impossible to repeat the scan during the subsequent run, repeat scan does not have any meaning! Since this is done logically at startup time, we can use the ApplicationRunner interface provided by SpringBoot to handle this, and methods that override this interface are executed at startup time. (There are many ways to execute the specified logic when a program is started, not limited to this one, depending on the requirements.)

Let’s now create a class that implements the interface and rewrite the run method with our interface scan logic in it. Note that the following code logic does not need to understand each line now, probably know this way on the line, the key is to read the comment to understand the general meaning, and then slowly study in the future:

@Component
public class ApplicationStartup implements ApplicationRunner {
    @Autowired
    private RequestMappingInfoHandlerMapping requestMappingInfoHandlerMapping;
    @Autowired
    private ResourceService resourceService;


    @Override
    public void run(ApplicationArguments args) throws Exception {
        // Scan and obtain all interface resources that require permission processing.
        List<Resource> list = getAuthResources();
        // Delete all operation permission resources first, and then add new resources later to achieve full update (note: do not set foreign key in database, otherwise delete failure)
        resourceService.deleteResourceByType(1);
        // If the permission resource is empty, do not go to the subsequent data insert step
        if (Collections.isEmpty(list)) {
            return;
        }
        // Add resource data to the database in batches
        resourceService.insertResources(list);
    }
    
	/** * scans and returns all interface resources that require permission processing */
    private List<Resource> getAuthResources(a) {
        // The resource to add to the database next
        List<Resource> list = new LinkedList<>();
        // Get all the interface information and start traversing
        Map<RequestMappingInfo, HandlerMethod> handlerMethods = requestMappingInfoHandlerMapping.getHandlerMethods();
        handlerMethods.forEach((info, handlerMethod) -> {
            // Get the permission annotation on the class (module)
            Auth moduleAuth = handlerMethod.getBeanType().getAnnotation(Auth.class);
            // Get the permission annotation on the interface method
            Auth methodAuth = handlerMethod.getMethod().getAnnotation(Auth.class);
            // Module annotations and method annotations are missing, indicating no permission processing
            if (moduleAuth == null || methodAuth == null) {
                return;
            }

            // GET the request method of the interface method (GET, POST, etc.)
            Set<RequestMethod> methods = info.getMethodsCondition().getMethods();
            // If an interface method marks multiple request methods, the permission ID is unrecognized and will not be processed
            if(methods.size() ! =1) {
                return;
            }
                // Concatenate the request mode and path with ':' to distinguish the interface. For example: GET:/user/{id}, POST:/user/{id}
                String path = methods.toArray()[0] + ":" + info.getPatternsCondition().getPatterns().toArray()[0];
                // Assemble the permission name, resource path, and resource type into a resource object and add it to the collection
                Resource resource = new Resource();
                resource.setType(1)
                        .setPath(path)
                        .setName(methodAuth.name())
                        .setId(moduleAuth.id() + methodAuth.id());
                list.add(resource);
        });
        returnlist; }}Copy the code

In this way, we have completed the interface scan! As long as the subsequent writing of new interface needs permission processing, as long as the Auth annotation can be added! The final data inserted is the data renderings shown before!

To this you think is over, as a stereotypical passer-by which can end so easily, I want to continue to optimize!

We now have core logic + interface scan, but not enough. Now every permission security judgment is written in the method, and the logical judgment code is the same, I have to write as much code as I need permission to handle the interface, which is disgusting:

@PutMapping
@auth (id = 1, name = "new user ")
public String deleteUser(@RequestBody UserParam param) {
    Set<String> allPaths = resourceService.getAllPaths();
    Set<String> userPaths = resourceService.getPathsByUserId(UserContext.getCurrentUserId());
    if (allPaths.contains("PUT:/API/user") && !userPaths.contains("PUT:/API/user")) {
        throw newApiException(ResultCode.FORBIDDEN); }... Omit business logic codereturn "Operation successful";
}

@DeleteMapping
@auth (id = 2, name = "delete user ")
public String deleteUser(Long[] ids) {
    Set<String> allPaths = resourceService.getAllPaths();
    Set<String> userPaths = resourceService.getPathsByUserId(UserContext.getCurrentUserId());
    if (allPaths.contains("DELETE:/API/user") && !userPaths.contains("DELETE:/API/user")) {
        throw newApiException(ResultCode.FORBIDDEN); }... Omit business logic codereturn "Operation successful";
}
Copy the code

This kind of repeated code, before also mentioned a mouth, of course, to use interceptors to do unified processing!

The interceptor

The code in the interceptor is roughly the same as the logic in the interface method.

public class AuthInterceptor extends HandlerInterceptorAdapter {
    @Autowired
    private ResourceService resourceService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // If the resource is static, permit it directly
        if(! (handlerinstanceof HandlerMethod)) {
            return true;
        }

        // Get the best matching path of the request, which means the /API/user/test/{id} path parameter shown in my previous data demonstration
        // API/user/test/100 = /API/user/test/100 = /API/user/test/100
        String pattern = (String)request.getAttribute(
                HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
        // Concatenate the request mode (GET, POST, etc.) and the request path with:. The resulting string would look like this: DELETE:/API/user
        String path = request.getMethod() + ":" + pattern;

        // Get all permission paths and the permission paths owned by the current user
        Set<String> allPaths = resourceService.getAllPaths();
        Set<String> userPaths = resourceService.getPathsByUserId(UserContext.getCurrentUserId());
        
        // If the interface is included in all permission paths, then the interface needs permission processing.
        // Second check: Check whether the interface belongs to the permission scope of the current user. If not, it indicates that the interface user has no permission
        if(allPaths.contains(path) && ! userPaths.contains(path)) {throw new ApiException(ResultCode.FORBIDDEN);
        }
        // If you have permission, let go
        return true; }}Copy the code

After the interceptor class is written, don’t forget to make it work. Here we directly make the SpringBoot boot class implement the WevMvcConfigurer interface to do this:

@SpringBootApplication
public class RbacApplication implements WebMvcConfigurer {

    public static void main(String[] args) {
        SpringApplication.run(RbacApplication.class, args);
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // Add a permission interceptor and exclude the login interface (if there is a login interceptor, put the permission interceptor after the login interceptor)
        registry.addInterceptor(authInterceptor()).excludePathPatterns("/API/login");
    }
	
    // The interceptor must be created this way, otherwise automatic injection in the interceptor will not take effect
    @Bean
    public AuthInterceptor authInterceptor(a) {return newAuthInterceptor(); }; }Copy the code

In this way, we can remove the related code in the interface method.

At this point, we just calculate the page level permissions + button level permissions have a relatively good implementation!

Note that the interceptor is now directly access to the database, the actual development must be stored in the cache (such as Redis), otherwise each interface will have to access the database, too much pressure! In order to reduce the mental burden, I will not integrate Redis here

Data access

The page and action permissions described above are both functional permissions, and the data permissions we are going to talk about are quite different.

The biggest difference between functional authority and data authority lies in that the former is to judge whether there is a certain authority, while the latter is to judge how much authority. The security of a resource can only be determined by YES or NO. Either you have the security or you don’t. Resource permissions require that different data sets be returned according to different permissions within the same data request.

A simple example of data permissions is this: now the list itself has 10 data, four of which I do not have permissions, so I can only query six data. Next I will take you to implement this function!

Hard coded

Now let’s simulate a business scenario: a company has established branches in various places, and each branch has its own order data, which cannot be seen without corresponding permissions. Each person can only view the orders belonging to his/her own permissions, like this:

It’s the same paginated list page, and different people find different results.

Company_id = company_id; company_id = company_id; company_id = company_id; company_id = company_id; company_id = company_id; Only then can the following permissions be divided:

Our permissions are also very simple, just as before, to create an intermediate table. Create a user_company table to indicate which company data permissions the user has:

The preceding data shows that user 1 has corporate data permissions of 1, 2, 3, 4, and 5, and user 2 has corporate data permissions of 4 and 5.

I believe that after learning the functional permissions, this table design has been handy. With the table design and data ready, it’s time to implement our key permissions functionality.

First, we need to comb through what a normal paging query looks like. We’ll do a paging query on data, and the SQL statement will be written as follows:

Sort by creation time in descending order
SELECT * FROM `data` ORDER BY create_time DESCLIMIT ? ,?Copy the code

There’s nothing to be said for this. Query data normally and then limit it to achieve paging effect. SQL > add data filter to SQL > add data filter to SQL

-- Query only the data of specified companies
SELECT * FROM `data` where company_id in(? ,? ,? ...).ORDER BY create_time DESCLIMIT ? ,?Copy the code

We just need to find out all the company ids that the user belongs to and put them in the paging statement.

We do not use in condition judgment, use continuous table can also achieve the effect:

-- Joins the user-company relationship table to query the company data associated with the specified user
SELECT
	*
FROM
	`data`
	INNER JOIN user_company uc ON data.company_id = uc.company_id AND uc.user_id = ? 
ORDER BY
	create_time DESCLIMIT ? ,?Copy the code

Of course, you can do this without having to use subqueries on the table, so you don’t have to expand it too much. In short, there are many SQL statements that can achieve the filtering effect, which should be optimized according to the business characteristics.

So far, I have introduced a very simple and crude way to implement data permissions: hard coding! That is, directly modify our original SQL statement, naturally achieve the effect ~

However, this way to the original code intrusion is too big, every permission filtering interface I have to modify, seriously affecting the open and close principle. Is there a way not to modify the original interface? Of course there is, this is the Mybatis interception plug-in I will introduce next.

Mybatis interceptor plugin

Mybatis provides an Interceptor interface. By implementing this interface, we can define our own Interceptor. This Interceptor can intercept SQL statements and then extend/modify them. Many pagination, sub-table, encryption and decryption plug-ins are done through this interface!

We just need to intercept the original SQL statement, add our additional statement, and achieve the same effect as just hard coding. Here’s a look at the interceptor effects I’ve already written:

As you can see, the part in red box is the statement added to the original SQL! This interception is not limited to paging queries, as long as we write the statement extension rule, other statements can intercept the extension!

I’m going to post the code for the interceptor. You don’t have to worry too much about this code, just take a quick look at it, because now we’re going to focus on the overall idea.

@Component
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class DataInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // Get some objects from mybatis
        StatementHandler statementHandler = PluginUtils.realTarget(invocation.getTarget());
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");

        / / mapper method of id to perform the full path name, such as com. Rudecrab. Mapper. UserMapper. InsertUser
        String id = mappedStatement.getId();
        log.info("mapper: ==> {}", id);
        // If the method is not specified, end the interception
        // If more methods can be stored in a collection, then determine whether the current interception exists in the collection, here to demonstrate only a mapper method interception
        if (!"com.rudecrab.rbac.mapper.DataMapper.selectPage".equals(id)) {
            return invocation.proceed();
        }

        // Get the original SQL statement
        String sql = statementHandler.getBoundSql().getSql();
        log.info(Original SQL statement: ==> {}, sql);
        // Parse and return the new SQL statement
        sql = getSql(sql);
        / / modify SQL
        metaObject.setValue("delegate.boundSql.sql", sql);
        log.info(SQL statement after interception: ==>{}, sql);

        return invocation.proceed();
    }

    /** * Parses SQL statements and returns a new SQL statement * Note that this method uses JSqlParser to manipulate SQL. Mybatis- Plus dependencies are already integrated. If you want to use it alone, import the dependency * * yourself@paramThe original SQL SQL *@returnNew SQL * /
    private String getSql(String sql) {
        try {
            // Parse statements
            Statement stmt = CCJSqlParserUtil.parse(sql);
            Select selectStatement = (Select) stmt;
            PlainSelect ps = (PlainSelect) selectStatement.getSelectBody();
            // Get the table information
            FromItem fromItem = ps.getFromItem();
            Table table = (Table) fromItem;
            String mainTable = table.getAlias() == null ? table.getName() : table.getAlias().getName();
            List<Join> joins = ps.getJoins();
            if (joins == null) {
                joins = new ArrayList<>(1);
            }

            // Create a join condition for the table
            Join join = new Join();
            join.setInner(true);
            join.setRightItem(new Table("user_company uc"));
            // first: the two tables are connected via company_id
            EqualsTo joinExpression = new EqualsTo();
            joinExpression.setLeftExpression(new Column(mainTable + ".company_id"));
            joinExpression.setRightExpression(new Column("uc.company_id"));
            // Second condition: matches the current login user ID
            EqualsTo userIdExpression = new EqualsTo();
            userIdExpression.setLeftExpression(new Column("uc.user_id"));
            userIdExpression.setRightExpression(new LongValue(UserContext.getCurrentUserId()));
            // concatenate the two conditions
            join.setOnExpression(new AndExpression(joinExpression, userIdExpression));
            joins.add(join);
            ps.setJoins(joins);

            // Modify the original statement
            sql = ps.toString();
        } catch (JSQLParserException e) {
            e.printStackTrace();
        }
        returnsql; }}Copy the code

SQL interceptor written after will be very convenient, before written code need not modify, directly with the interceptor unified processing can be! Thus, we complete a simple data permission function! Doesn’t it feel a little too easy to introduce data permissions in such a short time?

Simple to say and indeed simple, its core sentence can be stated: ** SQL interception and then achieve the effect of data filtering. * * but! I’m just demonstrating a very simple case here, with very few aspects to consider. If the requirements become complicated, there are many more things to consider. I’m afraid it would be difficult to finish this article by adding several times more content.

Data authority has a strong correlation with business, and there are many dimensions of authority division with its own industry characteristics, such as transaction amount, transaction time, region, age, user label and so on. We have only demonstrated the division of a department dimension. Some data permissions even need to cross multiple dimensions and perform data filtering on A certain field (for example, administrator A can see the mobile phone number and transaction amount, while administrator B can’t), which is far more difficult and complex than functional permissions.

So for data permissions, it must be demand first, technical means to keep up. As for you want to use Mybatis or what other framework, you want to use sub-query or even table, there is no formula, must be according to the specific business needs to develop targeted data filtering scheme!

conclusion

This brings us to the end of our discussion of permissions. In fact, this article said so much is only in the elaboration of the following points:

  1. The essence of permissions is to protect resources
  2. The core of permission design is what resources to protect and how to protect resources
  3. After mastering the core, you can make plans according to specific business requirements

It’s never about the code, it’s about the idea! It doesn’t matter if there are some things you don’t understand, you can refer to the project effect to help you understand the idea. All the code and SQL statements in this article are placed on Github, which can be cloned and run. There are not only back-end interfaces, but also front-end pages. I will continue more [project practice]!

These two articles cover authentication and authorization capabilities without using a security framework. The following article explains how to use Shiro and Spring Scurity for authentication and authorization. Stay tuned!

Blog, Github, wechat official account, please identify: RudeCrab, welcome to follow! If it is helpful to you, you can collect, like, star, look at, share ~~ your support, is the biggest power of my writing

Wechat reprint please contact the public number to open the white list, other places reprint please indicate the original address, the original author!