[project practice] takes you through page permissions, button permissions and data permissions

Time:2021-1-26

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 articles, but I can’t delete or modify them, I can see some in my circle of friends, I can’t see some, I can see the dynamics in seven days, I can see all the dynamics, and so on.

The authority functions of each system are not the same, each has its own business characteristics, and the design of authority management also has its own characteristics. However, no matter what kind of permission design, it can be roughly divided into three types:Page authority (menu level), operation authority (button level), data authorityAccording to the dimension, it is:Coarse grain authority, fine grain authority

“2020 latest Java foundation intensive lecture video tutorial and learning route! 》

I will start from the simplest and most basic explanation, and lead you to realize various functions step by step. After reading the article, you can gain:

  • The core concept of authorization
  • Design and implementation of page authority, operation authority and data authority
  • Evolution and application of permission model
  • Interface scanning andSQLintercept

And all the codeSQLAll the statements are in theGithubYou can clone it and run it,There are not only back-end interfaces, but also front-end pages!

Basic knowledge

Login authentication is the key toIdentity of the userTo confirm, authorization is toCan a user ask a resourceConfirm. For example, you enter your account password to log in to a forum, which is authentication. Your account is an administrator, so you can enter any board you want. This is authorization. Permission authorization usually occurs after successful login authentication, that is, first confirm who you are, and then confirm what you can access. Let’s take another example

System: who are you?

User: I’m Zhang San. Here’s my account number and password

System: ouch, the account number and password are right. It seems that it’s Zhang San, an outlaw maniac! What are you doing

Zhang San: I want to go into the vault

System: gunduzi, you can only enter the detention center, and you can’t go anywhere else (authorized)

As you can see, the concept of permissions is not difficult at all. It is like a firewall to protect resources from infringement (yes, the network firewall that we always talk about is also an embodiment of permissions. We have to say that the name of network firewall is really appropriate). Now we have made it clear what the essence of authority is, that isProtect resources. No matter what kind of functional requirements, the core of authority is aroundresourcesIn two words. If you can’t access the forum section, the section is a resource; if you can’t access some areas, the area is a resource

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

What resources to protect determines the granularity of your permissions. How to protect resources determines your

realization

We useSpringBootBuild a web project,MySQLandMybatis-plusFor data storage and operation. Here are the required dependency packages we want to use:

<dependencies>
    <! -- Web dependency package, essential for web application -- >
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <! -- mysql, a prerequisite for connecting to MySQL -- >
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    <! -- mybatis plus, ORM framework, access and operate database -- >
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.4.0</version>
    </dependency>
</dependencies>

Before designing permission related tables, there must be a basic user table with three simple fields: primary key, user name and password

[project practice] takes you through page permissions, button permissions and data permissions

I won’t write the corresponding entity class and SQL table building statement. You all know how to write when you look at the table structure (I put the complete SQL table building file on GitHub).

Next, we will implement a very simple permission control!

Page permissions

Page permissions are very easy to understand, that is, users with this permission can access this page, and users without this permission can not access it. It takes the whole page as the dimension, and the control of permissions is not so detailed, so it is a kind of securityCoarse grained permissions

The most intuitive example is that users with permission will display all menus, while users without permission will display only part of the menus

[project practice] takes you through page permissions, button permissions and data permissions

These menus all correspond to a page. Controlling the navigation menu is equivalent to controlling the page entrance, so the page permission is also called “page permission”Menu permissions

Authority core

As I said before, the first step in designing a permission system is to consider what resources to protect, page permissions, which are resources to be protected, must be pages. One page (menu) corresponds to oneURIAddress, when the user logs in, judge which page permissions the user has, and naturally know what navigation menu to render! The design of the table naturally emerges after these things are sorted out

[project practice] takes you through page permissions, button permissions and data permissions

This resource table is very simple, but it’s enough for the moment. Let’s assume that our page / menuURIThe mapping is as follows:

[project practice] takes you through page permissions, button permissions and data permissions

If we want to set the user’s permission, we only need to set the user ID andURICorresponding to each other:

[project practice] takes you through page permissions, button permissions and data permissions

The data above shows that,idby1Users with all permissions,idby2The user only has data management rights (we let all users access the home page, after all, a user, you at least have to let him see some of the most basic things). So far, we have completed the database table design of page permissions!

It’s useless to put the data there, so we’ll write the code to use the data. The code implementation is divided into back-end and front-end. When the front-end and back-end are not separated, the logic processing and page rendering are carried out in the back-end, so the overall logic link is as follows:

[project practice] takes you through page permissions, button permissions and data permissions

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

@Controller // note that this is not @ restcontroller, which means that all returned views are page views
public class ViewController {
    @Autowired
    private ResourceService resourceService;
    
    @GetMapping("/")
    public String index(HttpServletRequest request) {
        //Menu name mapping dictionary. The key is the URI path and the value is the menu name. It is convenient for the view to 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", "permission management");
        c. 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 view rendering
        Set<String> menus = resourceService.getCurrentUserMenus();
        request.setAttribute("menus", menus);
        return "index";
    }
}

index.html:

<! -- this syntax is thymeleaf syntax, which is a back-end template engine technology like JSP -- >
<ul>
    <! -- the home page can be seen by everyone and rendered directly -- >
    <li>Home page</li>
    
    <! -- render the corresponding menu according to the permission data -- >
    <li th:each="i : ${menus}">
        [[${menuMap.get(i)}]]
    </li>
    
</ul>

Here is just a general demonstration of how to render, instead of writing the full picture of the code, the key is the idea, without too much entanglement in the details of the code

In the mode of front-end and back-end, the basic functions of page permissions have been completed.

Now in the mode of front end and back end separation, the back end is only responsible for providingJSONData and page rendering are the front-end business. At this time, the overall logical link changes

[project practice] takes you through page permissions, button permissions and data permissions

When the user logs in successfully, the back end will return the user’s permission data to the front end. This is our login interface

@Restcontroller // note that this is @ restcontroller, which 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) {
        //Here, the simple point is to return only one permission path set
        return userService.login(user);
    }
}

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 the user data from the database according to the account password passed by the front end
        //The SQL statement of this method is: select * from user where user_ name = #{userName} and password = #{password}
        User user = userMapper.selectByLogin(userParam.getUsername(), userParam.getPassword());
        if (user == null) {
            Throw new apiexception ("account or password error");
        }
        
        //Returns the set of permission paths for the user
        //The SQL statement of this method: select path from resource where user_ id = #{userId}
        return resourceMapper.getPathsByUserId(user.getId());
    }
}

We have finished the programming of the back-end interface, and the front-end will receive the message from the back-end after successful loginJSONData:

[
    "/user/account",
    "/user/role",
    "/data"
] 

In this case, the back end does not need to transfer the menu name mapping to the front end as before,The front end stores a mapping dictionary. The front end stores this permission locally (for exampleLocalStorage)Then, according to the permission data rendering menu, the permission function in front and back end separation mode is completed. Let’s look at the effect

[project practice] takes you through page permissions, button permissions and data permissions

So far, the basic logic link of page permission has been introduced, isn’t it very simple? After the basic logic is clear, the only thing left is the very common addition, deletion and query: when I want to make a user’s permission larger, I will add the user’s permission data, and when I want to make a user’s permission smaller, I will delete the user’s permission data Next, we will complete this step, so that the users of the system can manage the permissions. Otherwise, we have to directly operate the database for everything, which is definitely not feasible.

First of all, users must be able to see a data list before they can operate. I added some data to facilitate the display effect

[project practice] takes you through page permissions, button permissions and data permissions

I won’t explain how to write the code for pagination, adding an account and deleting an account here. Let’s talk about the interface for editing permissions

@RestController
public class LoginController {
    @Autowired
    private ResourceService resourceService;
    
    @PutMapping("/menus")
    private String updateMenus(@RequestBody UserMenusParam param) {
        resourceService.updateMenus(param);
        Return "operation successful";
    }
}

It’s very simple to accept the parameters passed by the front end. It’s just a set of user ID and menu path to be set

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

The code of business class is as follows:

@Override
public void updateMenus(UserMenusParam param) {
    //First, delete the original user permission data according to the user ID
    resourceMapper.removeByUserId(param.getId());
    //If the permission set is empty, it means that all permissions are deleted, and there is no need to follow the new process
    if (Collections.isEmpty(param.getMenus())) {
        return;
    }
    //Add authority data according to user ID
    resourceMapper.insertMenusByUserId(param.getId(), param.getMenus());
}

The SQL statements for deleting permission data and adding permission data are as follows:

<mapper namespace="com.rudecrab.rbac.mapper.ResourceMapper">
    <! -- delete all permissions of the user according to the user ID -- >
    <delete id="deleteByUserId">
        delete from resource where user_id = #{userId}
    </delete>
    
    <! -- add menu permission according to user ID -- >
    <insert id="insertMenusByUserId">
        insert into resource(user_id, path) values
        <foreach collection="menus" separator="," item="menu">
            (#{userId}, #{menu})
        </foreach>
    </insert>
</mapper>

In this way, the function of authority data editing is completed

[project practice] takes you through page permissions, button permissions and data permissions

You can see thatrootUsers can only accessdata managementAfter permission editing, he can also access itAccount managementNow our page permission management function is complete.

Does it feel very simple? We only used two tables to complete a permission management function.

ACL model
The two tables are very convenient and easy to understand. The system is small and the amount of data is small. It’s nothing to play with. If the amount of data is large, it has its disadvantages

  1. The data is very repetitive
  • Consume storage resources. such as/user/accountI have to store as many strings as I have this permission. You should know that this is the simplest resource information. There is only one path. Some resources have a lot of information: resource name, type, level, introduction, etc
  • The cost of changing resources is too high. such as/dataI want to change it to/infoThe existing permission data should be changed
  1. Unreasonable design
  • The resource cannot be described visually. Just now we have only three resources. If I want to add the fourth, Fifth… Resources to my system, there is no way, because the current resources are all dependent on users and can’t be stored independently
  • The definition of table is not clear. Now ourresourceTables are not so much describing resources as describing the relationship between users and resources.

In order to solve the above problems, we should improve the current table designresourcesandThe relationship between users and resourcesClean it up. The relationship between users and resources is many to many. A user can have multiple permissions, and there can be multiple users under one permission. We usually use the intermediate table to describe this many to many relationship. Then the resource table is not used to describe the relationship, it is only used to describe the resources. In this way, our new table design comes out: create intermediate table, improve resource table!

Let’s transform the resource table first,iduser_idpathThese are the first three fields,user_idIt is not used to describe resources, so we delete it. Then we’ll add another onenameField is used to describe the resource name (not required). After modification, the resource table is as follows:

[project practice] takes you through page permissions, button permissions and data permissions

The contents in the table are specially used to put resources:

[project practice] takes you through page permissions, button permissions and data permissions

The resource table is finished. Let’s create an intermediate table to describe the relationship between users and permissions. The intermediate table simply stores only user ID and resource ID

[project practice] takes you through page permissions, button permissions and data permissions

This is how the previous permission relationship is stored in the middle table:

[project practice] takes you through page permissions, button permissions and data permissions

Now the data shows that ID is1User with ID1、2、3That is, user 1 hasAccount management, role management and data managementjurisdiction. ID is2Only users with ID3That is, user 2 hasdata managementjurisdiction!

The entire table design has been upgraded. Now our table is as follows:

[project practice] takes you through page permissions, button permissions and data permissions

Because the table has changed, our code has to be adjusted accordingly. The adjustment is very simple, that is, all the previous operations on permissions are operationsresourceTable, we change to operationuser_resourceThe left side is the old code, and the right side is the improved code

[project practice] takes you through page permissions, button permissions and data permissions

The key point is that we used to operate the resource tablepathString, the permission information between the front end and the back end is also passedpathString, now all changed to the operation of the resource tableid(remember to change it in Java code. I’ll just demonstrate SQL here.).

Here, I want to explain separately. If the front end only passes the resource ID, how can the front end render the page according to this ID? How to display the resource name according to this ID? This is because the front-end stores a local mapping dictionary, which contains information about resources, such as which path and name the ID corresponds to. After the front-end gets the user’s ID, it can judge according to the dictionary to achieve the corresponding functions.
There are two management modes in the actual development of this mapping dictionary. One is that the front-end and back-end adopt the form of agreement. The front-end builds the dictionary in the code itself. If there is any change in the follow-up resources, the front-end and back-end personnel should communicate with each other. This mode is only suitable for the situation where the permission resources are very simple. Another is that the back-end provides an interface, which returns all the resource data. Whenever the user logs in or enters the home page of the system, the front-end calls the interface to synchronize the resource dictionary! We use this method now, so we have to write an interface

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

Now, our permission design looks like something. The mode of binding relationship between users and permission resources isACL modelAccess control list, which is convenient and easy to understand, is suitable for the system with simple permission function.

Let’s take advantage of the heat and continue to upgrade the whole design!

RBAC model

For the convenience of demonstration, I don’t set too many permission resources (that is, navigation menu), so the whole permission system seems to be very convenient to use. However, once there are more permission resources, the current design is a bit stretched. Suppose we have 100 permission resources, user a needs to set 50 permissions, and three BCD users need to set the same 50 permissions, then I have to repeat 50 operations for each user! This kind of requirement is also very common. For example, all employees in the sales department have the same permissions. For each new employee, I have to repeatedly set permissions step by step. If I change the permissions of this Sales Department, then the permissions of all employees under my department have to be changed one by one, which is extremely cumbersome

[project practice] takes you through page permissions, button permissions and data permissions

Any problem in the field of computer science can be solved by adding an indirect middle layer

Now our permission relationship is bound to users, so we have to set a set of exclusive permissions for each new user. Since the permissions of many users are the same, I will encapsulate another layer to shield the relationship between users and permissions

[project practice] takes you through page permissions, button permissions and data permissions

In this way, when there are new users, they only need to bind them to this encapsulation layer, and then they can have a whole set of permissions. Even if the permissions are changed in the future, it is very convenient. This encapsulation layer is what we call itrole! Roles are very easy to understand. Sales staff is a kind of role, logistics is a kind of role. Roles and permissions are bound, and users and roles are bound, as shown in the figure above.

Now that we have added a layer of roles, our table design will also change. No doubt, there must be a role table to describe the role information, just two fieldsPrimary key IDRole nameHere, two role data are added for demonstration:

[project practice] takes you through page permissions, button permissions and data permissions

The permission mentioned just now is linked to the role, so the previoususer_resourceThe watch will be changed torole_resourceAnd then users are linked to roles, so there’s another oneuser_roleTable:

[project practice] takes you through page permissions, button permissions and data permissions

The above data shows that ID is1The role (super administrator) of has three permission resources with ID2The role of (data administrator) has only one permission resource. Then users1With super administrator role, users2Having the role of data administrator:

[project practice] takes you through page permissions, button permissions and data permissions

If there is a user who wants to have all the permissions of super administrator, just bind the user and super administrator role! In this way, we have completed the design of the table. Now our database table is as follows:

[project practice] takes you through page permissions, button permissions and data permissions

This is the very famous and very popular RBAC model, that is role-based access controller based access control model! It can meet most of the permission requirements, and is one of the most commonly used permission models in the industry. Let’s improve our code and debug with the front end to complete a role-based privilege management system!

Now there are three entities in our system: user, role and resource (permission). Before, we had a user page where we could manage permissions. Now that we have the concept of role, we have to add a role page

[project practice] takes you through page permissions, button permissions and data permissions

[project practice] takes you through page permissions, button permissions and data permissions

I won’t explain the old code of pagination, addition and deletion, but I will focus on the code of permission operation.

Before, our user page was used to operate permissions directly. Now we want to change it to operation role, so SQL statements should be written as follows:

<mapper namespace="com.rudecrab.rbac.mapper.RoleMapper">
    <! -- batch add roles according to 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 of the user according to the user ID -- >
    <delete id="deleteByUserId">
        delete from user_role where user_id = #{userId}
    </delete>

    <! -- Query role ID set according to user ID -- >
    <select id="selectIdsByUserId" resultType="java.lang.Long">
        select role_id from user_role where user_id = #{userId}
    </select>
</mapper>

In addition to the user’s operation on the role, we also have to have an interface to directly obtain all the permissions of the user with the user ID, so that the front end can render the page according to the permissions of the current user. Before, we were going toresourceanduser_resourceAll the permissions of the user can be found in the table. Now we willuser_roleandrole_resourceConnect the table to get the permission ID. on the left is our previous code, and on the right is our modified code

[project practice] takes you through page permissions, button permissions and data permissions

This is the end of the user section. Let’s deal with the role related operations. The idea of roles here is the same as before. How users used to operate permissions directly before is how roles operate permissions

<mapper namespace="com.rudecrab.rbac.mapper.ResourceMapper">
    <! -- batch add permissions according to 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 permissions under the role according to the role ID -- >
    <delete id="deleteByRoleId">
        delete from role_resource where role_id = #{roleId}
    </delete>

    <! -- get permission ID according to role ID -- >
    <select id="selectIdsByRoleId" resultType="java.lang.Long">
        select resource_id from role_resource where role_id = #{roleId}
    </select>
</mapper>

Note that the front and back end of the transmission are alsoidSince it isidSo the front end has to have a mapping dictionary to render, so our two interfaces are indispensable

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

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

With the dictionary, the method of operating role and the method of operating permission, we have completed the page permission function based on RBAC model

[project practice] takes you through page permissions, button permissions and data permissions

rootUser ownedData managerAt the beginningData managerI can only see itdata managementPage, later we aredata managementI’ve added moreAccount managementPage permissions,rootUsers can see it without making any changesAccount managementPage!

No matter how many tables, the core of permissions, or the flow chart I showed before, it’s OK to master the model

I don’t know if you find that in the mode of front-end and back-end separation, the back-end leaves the permission data to the front-end when logging in, and then doesn’t care. If the user’s permission changes at this time, the front-end can’t be notified, and the data stored in the front-end is also easy to be tampered by the user directly, so it’s very unsafe. The separation of front end and back end is not the same as that without separation. Page requests have to go through the back end. The back end can easily judge the security of each page request

@Controller
public class ViewController {
    @Autowired
    private ResourceService resourceService;
    
       //These logic can be put in the filter to do, here is just for the convenience of demonstration
    @GetMapping("/user/account")
    public String userAccount() {
        //First, retrieve the permission data of the current login user from the cache or database
        List<String> menus = resourceService.getCurrentUserMenus();
        
        //Judge whether you have authority or not
        if (list.contains("/user/account")) {
             //If you have permission, you can return to the normal page
            return "user-account";
        }
        //No permission to return to 404 page
        return "404";
    }
    
}

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

In this way, is it necessary to recognize the front and back end separation mode? Of course not. In fact, when the front end initiates a back-end request, the back-end will return the latest permission data to the front end. In this way, the above problem can be avoided. However, this method will bring great pressure to the network transmission, which is neither elegant nor wise, so it is generally not done in this way. The compromise is to get permission data again when the user enters a page, such as the home page. However, this is not very safe, after all, as long as the user does not enter the home page that is still useless.

So what’s the elegant, wise and safe way? That’s what we’re going to talk about next!

Operation authority

The operation authority is tooperationAs a resource, such as deletion, some people can, some people can’t. In the back end, operation is an interface. For the front end, the operation is often a button, so the operation permission is also called “operation permission”Button permissionsIt’s a kind ofFine grained permissions

The more intuitive embodiment on the page is that people who do not have the delete permission will not display the button, or the button will be disabled

[project practice] takes you through page permissions, button permissions and data permissions

The front-end button permissions are the same as the previous navigation menu rendering. Compare the current user’s permission resource ID with the permission resource dictionary. If you have permission, it will be rendered. If you don’t have permission, it won’t be rendered

The front-end logic about permissions is the same as before. How can operation permissions be more secure than page permissions? This security is mainly reflected in the back end. Page rendering does not go through the back end, but the interface must go through the back end. As long as we go through the back end, it is easy to do. We only need to make a permission judgment on each interface!

Basic realization

We used to design for page permissions, now we need to expand the operation permissions on the existingresourceResource table for a small expansion, add atypeField to distinguish page permissions and operation permissions

[project practice] takes you through page permissions, button permissions and data permissions

We use it here0To represent page permissions, use1To represent the operation permission.

After the table expansion, we will add the data of operation permission type. As I said just now, for the back end, operation is an interface, so we need toTake the interface path as our permission resourceAs soon as you look at it, you can see:

[project practice] takes you through page permissions, button permissions and data permissions

DELETE:/API/userIt consists of two parts,DELETE:Represents the request mode of the interface, such asGETPOSTAnd so on,/API/userIs the interface path, the combination of the two can determine an interface request!

Now that the data is available, we will judge the authority security in the code, and pay attention to the comments

@RestController
@RequestMapping("/API/user")
public class UserController {
    ... omit the auto injected service code

    @DeleteMapping
    public String deleteUser(Long[] ids) {
        //Get all the permission paths and the permission paths owned by the current user
        Set<String> allPaths = resourceService.getAllPaths();
        Set<String> userPaths = resourceService.getPathsByUserId(UserContext.getCurrentUserId());
        
        //The first judgment: if all permission paths contain this interface, it means that the interface needs permission processing, so this is a prerequisite,
        //Second judgment: judge whether the interface belongs to the permission scope of the current user. If not, it means that the user of the interface has no permission
        if (allPaths.contains("DELETE:/API/user") && !userPaths.contains("DELETE:/API/user")) {
            throw new ApiException(ResultCode.FORBIDDEN);
        }
        
        //This means that the user of the interface has permission, and normal business logic processing is performed
        userService.removeByIds(Arrays.asList(ids));
        Return "operation successful";
    }
    
    ... omit other interface declarations
}

After joint debugging with the front end, the front end hides the corresponding operation button according to the authority

[project practice] takes you through page permissions, button permissions and data permissions

The button is hidden, but what if the user tampers with the local permission data, causing the button that should not be displayed to be displayed to be displayed, or if the user knows that the interface bypasses the page and calls itself? Anyway, he will call our interface in the end. Let’s call the interface to try the effect

[project practice] takes you through page permissions, button permissions and data permissions

As you can see, it is useless to bypass the front-end security judgment!

Then there is another question we mentioned before. If the current user’s permission is modified, how can it be synchronized with the front end in real time? For example, in the beginning, the role of user a wasDelete permissionsThen an administrator removed his permission. At this time, user a can still see the delete button if he doesn’t log in again.

In fact, with the operation permission, even if the user can see the button that does not belong to himself, it does not damage the security. After he clicks it, he will still prompt that he has no permission, just saying that the user experience is a little poor! It’s the same with pages,A page is just a container for carrying data, which is called through an interfaceFor example, in the pagination data shown in the figure, we can also manage the permissions of the pagination query interface, so that the user can bypass the page permissions and come to the pageAccount managementPlate, still can not see the slightest data!

So far, we have completed the button level operation permission, isn’t it very simple? Once again wordy: as long as you master the core idea, the implementation is really simple, don’t think about the complexity.

Readers who know my style will know that I’m going to upgrade next! That’s right. Now our way of implementation is too simple and cumbersome. Now we all add resource data manually. I need to add a data manually when I write an interface. If you know that hundreds of interfaces in a system are too normal, can’t I add them manually? What can I do to automatically generate resource data when I write an interface? That’s what I’m going to talk about next: interface scanning!

Interface scan

SpringMVCProvides a very convenient classRequestMappingInfoHandlerMapping, this class can get all the web interface information you declare. After getting this, the rest is not very simple. It is to add the interface information to the database in batches through the code! But we’re not really going toAllInterfaces are added to permission resources. What we want is for those interfaces that need permission processing to generate permission resources. Some interfaces do not need permission processing, so they will not be generated naturally. So we have to find a way to mark whether the interface needs permission management!

Our interfaces are declared by methods. The most convenient way to mark methods is annotation! Let’s customize an annotation first

@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, unique
     */
    long id();
    /**
     *Permission name
     */
    String name();
}

Why is this annotation designed like this? I’ll talk about it later. Now we just need to know that as long as this annotation is added to the interface method, we will be regarded as requiring permission management

@RestController
@RequestMapping("/API/user")
@Auth (id = 1000, name = user management)
public class UserController {
     ... omit the auto injected service code

    @PostMapping
    @Auth (id = 1, name = new user)
    public String createUser(@RequestBody UserParam param) {
           ... omit business code
        Return "operation successful";
    }

    @DeleteMapping
    @Auth (id = 2, name = delete user)
    public String deleteUser(Long[] ids) {
        ... omit business code
        Return "operation successful";
    }

    @PutMapping
    @Auth (id = 3, name = edit user)
    public String updateRoles(@RequestBody UserParam param) {
        ... omit business code
        Return "operation successful";
    }
    
    @GetMapping("/test/{id}")
    @Auth (id = 4, name = used to demonstrate path parameters)
    public String testInterface(@PathVariable("id") String id) {
        ... omit business code
        Return "operation successful";
    }

    ... omit other interface declarations
}

Before talking about interface scanning and annotation design, let’s take a look at the final effect. After seeing the effect, we can understand it with half effort

[project practice] takes you through page permissions, button permissions and data permissions

As you can see, in the above code, I added our customAuthAnnotation, and set theidandnameThe value of thisnameIt’s easy to understand. It’s the resource name in the resource data. Why design in annotationsidWell, database primary keyidIt’s not usually self increasing. This is because we artificially control the primary key of resourcesidThere are many benefits.

First of allidThe mapping with interface path is particularly stable. If you want to use auto increment, I will give you the permissions of an interface at the beginningidyes4, a lot of roles are bound to this resource4As mentioned above, I don’t need this interface for permission management for a period of time according to my business requirements, so I use this resource4Delete for a period of time, then add back, but when the data is added backidIt becomes5, the previously bound roles have to reset the resources, which is very troublesome! If thisidIf it is fixed, I will add back the permissions of this interface. All the permissions set before can take effect without perception, which is very convenient. So,idAnd interface path mapping from the beginning to stabilize, do not easily change!

As for the class plusAuthAnnotation is a convenient module to manage interface permissionsControllerClass is regarded as a set of interface modules, and the final interface permissions areidIt’s the moduleid+Methodsid. Let’s think about it. If we don’t do this, I want to guarantee the permission of each interfaceidThe only thing I have to remember is the number of methods in each classid, one by one to set the new oneid. For example, I set the last method to101And then I’m going to set it up102103If you don’t pay attention, it’s reset. But according toControllerClasses are easy to manage after they are grouped. This class is1000The next class is2000Then all methods in the class can independently follow the123To set, greatly avoid the mental burden!

After introducing the design of annotation for such a long time, we will explain the specific implementation of interface scanning again! This scan must have happened when I finished writing the new interface, recompiled, packaged and restarted the program! And only do a scan when the program starts, it is impossible to repeat the scan during the follow-up run, repeated scan is meaningless! Since it is a logical operation when the program starts, we can use theApplicationRunnerInterface, and the method that rewrites the interface will be executed when the program starts. (there are many ways to execute the specified logic when the program starts, not limited to this one. The specific use depends on the demand.)

Let’s now create a class to implement the interface and override therunMethod to write our interface scan logic.Note that you don’t need to understand every line of the following code logic now. It’s OK to know such a writing method. The key point is to understand the general meaning by looking at the comments, and we will study it slowly 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 need permission processing (the logic of this method is written below)
        List<Resource> list = getAuthResources();
        //Delete all permission resources of operation permission type first, and add new resources later to realize full update (Note: don't set foreign keys in the database, otherwise the deletion will fail)
        resourceService.deleteResourceByType(1);
        //If the permission resource is empty, there is no need to follow the steps of data insertion
        if (Collections.isEmpty(list)) {
            return;
        }
        //Batch adding resource data to database
        resourceService.insertResources(list);
    }
    
    /**
     *Scan and return all interface resources that need permission processing
     */
    private List<Resource> getAuthResources() {
        //Next, add the resource to the database
        List<Resource> list = new LinkedList<>();
        //Get all the interface information and start traversing
        Map<RequestMappingInfo, HandlerMethod> handlerMethods = requestMappingInfoHandlerMapping.getHandlerMethods();
        handlerMethods.forEach((info, handlerMethod) -> {
            //Get permission annotation on class (module)
            Auth moduleAuth = handlerMethod.getBeanType().getAnnotation(Auth.class);
            //Get the permission annotation on the interface method
            Auth methodAuth = handlerMethod.getMethod().getAnnotation(Auth.class);
            //The absence of either module annotation or method annotation means that permission processing will not be performed
            if (moduleAuth == null || methodAuth == null) {
                return;
            }

            //Get the request mode of the interface method (get, post, etc.)
            Set<RequestMethod> methods = info.getMethodsCondition().getMethods();
            //If an interface method is marked with multiple request methods, the permission ID cannot be recognized and will not be processed
            if (methods.size() != 1) {
                return;
            }
                //The request mode and path are spliced with ':' to distinguish interfaces. For example: get / user / {ID}, post / user / {ID}
                String path = methods.toArray()[0] + ":" + info.getPatternsCondition().getPatterns().toArray()[0];
                //The permission name, resource path and resource type are assembled into resource objects and added to the collection
                Resource resource = new Resource();
                resource.setType(1)
                        .setPath(path)
                        .setName(methodAuth.name())
                        .setId(moduleAuth.id() + methodAuth.id());
                list.add(resource);
        });
        return list;
    }
}

In this way, we have completed the interface scan! In the future, as long as you write a new interface and need permission processing, just addAuthAnnotation is OK! The final inserted data is the data rendering shown before!

You think it’s over here. As an old routine person, how can it end so easily? I want to continue to optimize!

Now we’re doing core logic + interface scanning, but it’s not enough. Now every permission security judgment is written in the method, and the logic judgment code is the same. I have to write as many duplicate codes as many interfaces need permission processing, 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 new ApiException(ResultCode.FORBIDDEN);
    }
    ... omit business logic code
    Return "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 new ApiException(ResultCode.FORBIDDEN);
    }
    ... omit business logic code
    Return "operation successful";
}

This kind of repetitive code has been mentioned before. Of course, interceptors should be used for unified processing!

Interceptor

The code in the interceptor is roughly the same as the logic judgment written in the previous interface method, or is it the same

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

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

        //Get the best matching path of the request, which means the / API / user / test / {ID} path parameter in my previous data demonstration
        //If you use URI to judge, it is / API / user / test / 100, which can't match the path parameter, so you need to get it in this way
        String pattern = (String)request.getAttribute(
                HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
        //The request method (get, post, etc.) and the request path are spliced with: to make a good judgment. The final string is like this: delete / API / user
        String path = request.getMethod() + ":" + pattern;

        //Get all the permission paths and the permission paths owned by the current user
        Set<String> allPaths = resourceService.getAllPaths();
        Set<String> userPaths = resourceService.getPathsByUserId(UserContext.getCurrentUserId());
        
        //The first judgment: if all permission paths contain this interface, it means that the interface needs permission processing, so this is a prerequisite,
        //Second judgment: judge whether the interface belongs to the permission scope of the current user. If not, it means that the user of the interface has no permission
        if (allPaths.contains(path) && !userPaths.contains(path)) {
            throw new ApiException(ResultCode.FORBIDDEN);
        }
        //Release if you have permission
        return true;
    }
}

After the interceptor class is written, don’t forget to make it effective. Let’s make it workSpringBootStart class implementationWevMvcConfigurerInterface

@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, remember to put the permission interceptor behind the login interceptor)
        registry.addInterceptor(authInterceptor()).excludePathPatterns("/API/login");
    }
    
    //The interceptor must be created in this way, otherwise the automatic injection in the interceptor will not take effect
    @Bean
    public AuthInterceptor authInterceptor() {return new AuthInterceptor();};
}

In this way, we can remove the relevant code of permission judgment in the interface method before!

So far, we have a good implementation of page level permission + button level permission!

Note that getting permission data in interceptor is now a direct query database, which is in actual developmentSure, sureThe permission data should be stored in the cache (such as redis), otherwise each interface has to access the database once, which is too much pressure! In order to reduce the mental burden here, I don’t integrate redis

Data rights

The page permissions and operation permissions described above belong toFunction authorityWhat we’re going to talk about next is quite differentData rights

The biggest difference between function permission and Data permission is that the former is judgmentIs there anyThe latter is judgmentHow many?jurisdiction. There are only yes and no results for function permissions to judge the security of resources. Either you have this permission or you don’t have it. What resource permissions require is thatSame data requestAccording to the scope of different permissionsDifferent data sets

Take the simplest example of Data permission: now there are ten pieces of data in the list, four of which I don’t have permission, so I can only query six pieces of data. Next, I will take you to realize this function!

Hard coding

Now let’s simulate a business scenario: a company has set up branches in various places, and each branch has its own order data, which can’t be seen without corresponding permissions. Everyone can only view orders with their own permissions, just like this:

[project practice] takes you through page permissions, button permissions and data permissions

[project practice] takes you through page permissions, button permissions and data permissions

It’s all the same paged list page, different people find out different results.

This paging query function has nothing to say. The design of the database table is also very simple. Let’s build a data tabledataAnd a company tablecompanydataThe other fields in the data table are not the key. The main thing is to have onecompany_idFields are used to associatecompanyIn this way, the data can be classified and the permissions can be divided subsequently

[project practice] takes you through page permissions, button permissions and data permissions

Our authority division is also very simple, just like before, just build an intermediate table. Here, for demonstration, the user and the company are directly linked to build auser_companyTable to show which company data rights users have:

[project practice] takes you through page permissions, button permissions and data permissions

The above data shows that ID is1User with ID1、2、3、4、5The company data authority of, ID is2User with ID4、5Company data rights.

I believe that after the study of functional permissions, this table design has been handy. After the table design and data preparation, the next step is the implementation of our key permission functions.

First of all, we have to sort out the general paging query. We need to be rightdataPaging query,SQLThe statement is written as follows:

--Sort by creation time in descending order
SELECT * FROM `data` ORDER BY create_time DESC LIMIT ?,?

This has nothing to say, normal query data and thenlimitLimit to achieve paging effect. So we need to add the data filtering function,All you need to do isSQLIt’s done by filtering on the Internet

--Only query the data of the specified company
SELECT * FROM `data` where company_id in (?, ?, ?...) ORDER BY create_time DESC LIMIT ?,?

We just need to change the company to which the user belongsidFind out all of them, and then put them in the paging statementinThe effect can be achieved in the process.

We don’t have toinThe effect can also be achieved by using linked tables

--Connect 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 DESC 
LIMIT ?,?

Of course, you don’t need to use subquery to implement it. Here, you just need to expand it. In a word, it can achieve the filtering effectSQLThere are many statements, just optimize them according to the business characteristics.

So far, I have actually introduced a very simple and crude way to realize data permission: hard coding! That is, to directly modify our originalSQLSentence, naturally achieve the effect~

However, this method is too invasive to the original code. I have to modify every interface that needs permission filtering, which seriously affects the open close principle. Is there any way to modify the original interface? Of course, there are. That’s what I’m going to introduce nextMybatisIntercept plug-ins.

Mybatis interception plug in

MybatisProvides aInterceptorInterface. By implementing this interface, we can define our own interceptor, which canSQLStatement, and then extend / modify. Many plug-ins such as paging, database and table, encryption and decryption are completed through this interface!

We just need to intercept the original oneSQLStatement, add our additional statements, not just as hard coding to achieve the same effect? Here I’ll show you the interceptor effect I’ve written:

[project practice] takes you through page permissions, button permissions and data permissions

As you can see, the red framed part is in the originalSQLStatement added on! This interception is not limited to paging queries. As long as we write the statement extension rules, other statements can be intercepted and extended!

Next, I’ll post the interceptor code. Pay attentionWe don’t have to worry too much about this code. We probably know that there is such a thing at a glance, because now our focus is on the overall thinking. First, follow my thinking. There is plenty of time for the code

@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 of mybatis, and then operate them
        StatementHandler statementHandler = PluginUtils.realTarget(invocation.getTarget());
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");

        //ID is the full pathname of the executed mapper method, such as com.rudecrab.mapper . UserMapper.insertUser
        String id = mappedStatement.getId();
        log.info("mapper: ==> {}", id);
        //If it is not the specified method, the interception will be terminated directly
        //If more than one method can be saved in a collection, then judge whether the current intercepted method exists in the collection. Here, only one mapper method is intercepted
        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) ";
        //Parses and returns a new SQL statement
        sql = getSql(sql);
        //Modify SQL
        metaObject.setValue("delegate.boundSql.sql", sql);
        log.info ("intercepted SQL statement: = = > {}", SQL) ";

        return invocation.proceed();
    }

    /**
     *Parse the SQL statement and return the new SQL statement
     *Note that this method uses jsqlparser to operate SQL, and the dependency package mybatis plus has been integrated. If you want to use it alone, please import the dependency first
     *
     *@ param SQL original SQL
     *@ return new SQL
     */
    private String getSql(String sql) {
        try {
            //Parsing statement
            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 join condition of join table
            Join join = new Join();
            join.setInner(true);
            join.setRightItem(new Table("user_company uc"));
            //The first one: two tables pass through company_ ID connection
            EqualsTo joinExpression = new EqualsTo();
            joinExpression.setLeftExpression(new Column(mainTable + ".company_id"));
            joinExpression.setRightExpression(new Column("uc.company_id"));
            //Second condition: match with the current login user ID
            EqualsTo userIdExpression = new EqualsTo();
            userIdExpression.setLeftExpression(new Column("uc.user_id"));
            userIdExpression.setRightExpression(new LongValue(UserContext.getCurrentUserId()));
            //Put the two conditions together
            join.setOnExpression(new AndExpression(joinExpression, userIdExpression));
            joins.add(join);
            ps.setJoins(joins);

            //Modify the original sentence
            sql = ps.toString();
        } catch (JSQLParserException e) {
            e.printStackTrace();
        }
        return sql;
    }
}

SQLAfter the interceptor is written, it will be very convenient. The previously written code does not need to be modified. It can be directly processed with the interceptor! In this way, we have completed a simple data permission function! Do you think it’s a little too simple to introduce data permissions in a short time?

Simple is really simple. The core sentence can show that:yesSQLIntercept and then achieve the effect of data filtering.But! I’m just demonstrating a very simple case here. There are very few aspects to be considered. If the requirements become complicated, it’s difficult for me to add several times more content to this article.

Data permission is highly related to business. There are many dimensions of permission division with industry characteristics, such as transaction amount, transaction time, region, age, user label, etc. we only demonstrate the division of a department dimension. Some data permissions even need to cross multiple dimensions, and they also need to be able to filter data in a certain field (for example, administrator a can see the mobile phone number and transaction amount, but administrator B can’t). The difficulty and complexity are far beyond the functional permissions.

So for data permissions, it must be the demand first, and then the technical means to keep up. As for you are going to useMybatisOr any other framework, whether you want to use subquery or even table, there is no fixed formula. You must formulate a targeted data filtering scheme according to the specific business needs!

summary

At this point, the explanation of authority is coming to an end. In fact, this article has said so much, and it only expounds the following points:

  1. The essence of authority is to protect resources
  2. The core of authority design is to protect what resources and how to protect resources
  3. After the core is mastered, the plan can be formulated according to the specific business needs, and all changes are inseparable from the case

Recommended Today

How to Build a Cybersecurity Career

Original text:How to Build a Cybersecurity Career How to build the cause of network security Normative guidelines for building a successful career in the field of information security fromDaniel miesslerstayinformation safetyCreated / updated: December 17, 2019 I’ve been doing itinformation safety(now many people call it network security) it’s been about 20 years, and I’ve spent […]