Step-by-step learning of spring security Part 11 How to dynamically control URL? How to dynamically add permissions to users?

review

Earlier we introduced that spring security can authorize the permissions of the logged-in user, which resources can be accessed and which resources cannot be accessed. If you don’t know how to configure resource access permissions, you can go back to the previous article "Step-by-Step Learning Spring Security Chapter 7 , How to configure permissions based on user table and permission table? The more you learn, the easier it is ." I don't know if you have noticed that what we have introduced before is to configure the interface directly on the configuration file. This is still not enough for many practical business scenarios. For example, in some enterprises, there will be a Background management, enterprise administrators can configure the menus of the front page in the background, authorize which menus, and which user identities have permissions, can access these menus, otherwise they will not be able to access them. If the administrator needs to change the code configuration every time when configuring the front-end menu permissions, this is undoubtedly unrealistic. Then, is there a way to dynamically realize the configuration? Of course there is, and that is the subject of this article

Before reading this article, please read my previous article to help you understand this article

  1. Don’t say that you are not familiar with spring security in the interview, a demo makes you try to fool the interviewer
  2. Step by step to learn spring security Part 2, how to modify the default user?
  3. Step by step learning spring security Part 3, how to customize the login page? Login callback?
  4. Step by step snow spring security Part 4, what is the login process like? Where is the login user information saved?
  5. Step by step learning spring security chapter 5, how to deal with redirection and server jump? How does login return a JSON string?
  6. Learn spring security step by step the sixth chapter, teach you how to read users from the database for login verification, mybatis integration
  7. How to configure permissions based on user table and permission table in the seventh chapter of spring security step by step? The more you learn, the easier it is
  8. How to configure password encryption in the eighth chapter of spring security step by step? Are multiple encryption schemes supported?
  9. Step-by-step learning of spring security, Chapter 9, supports source code interpretation and sample code of multiple encryption schemes
    10. Step-by-step learning of spring security, Chapter 10 How to log in with token? JWT debut

Why dynamic permissions?

In a word: In order to flexibly meet the business needs of enterprises,
give an example

Most companies are in the process of growing from small to large. At the beginning, there were only 10 people in the company, and Xiao Zhang was the only person in the HR department. Xiao Zhang was responsible for all roles such as finance, recruitment, and front desk. View permissions

Later, the company gradually expanded, and the personnel department also added 2 people, Xiao Li and Xiao Wang. Recruitment, finance, front desk, etc. all permissions, you can check or operate Xiao Li's work items at any time, who is absent from work today, who asked for leave, who has submitted reimbursement for business trips, and the amount of reimbursement; you can also view or operate small Wang recruits and front desk business; and Xiao Li, because he is in charge of finance, can only view and operate financial business, but cannot exceed his authority to view or operate Xiao Wang's business.

The company continues to expand and go public. There are now 1,000 people in the HR department, and there are many branches. People may join and leave every day, and permissions must be modified. If you follow what we introduced before, add the URL in the configuration file The permission configuration is dead, and it will take effect after restarting every time, which will undoubtedly make people crash

How to better manage permissions?

Database Design

If you want to manage permissions well, you must first design the database well, so that it will be very simple to manage permissions

And how to design the database? The most important point is isolation, that is to say, a table only represents one business and should not be mixed with other businesses

First, there must be

user table definition

CREATE TABLE `h_user` (
                          `id` int NOT NULL AUTO_INCREMENT,
                          `username` varchar(50) NOT NULL COMMENT '用户名',
                          `password` varchar(500) NOT NULL COMMENT '密码',
                          `enabled` tinyint(1) NOT NULL COMMENT '是否启动,0-不启用,1-启用',
                          PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COMMENT '用户表';

role table definition

CREATE TABLE `h_role` (
                                 `id` int NOT NULL AUTO_INCREMENT,
                                 `name` varchar(50) NOT NULL COMMENT '角色名称',
                                 `code` varchar(50) NOT NULL COMMENT '角色编码',
                                 PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COMMENT '角色表';

Roles are actually permissions

menu table

CREATE TABLE `h_menu` (
                          `id` int NOT NULL AUTO_INCREMENT,
                          `name` varchar(50) NOT NULL COMMENT '菜单名称',
                          `url` varchar(200) NOT NULL COMMENT '菜单URL',
                          `parent_id` int NOT NULL default 0 COMMENT '上级菜单id',
                          PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COMMENT '菜单表';

The menu table defines which menus need to be handed over to the authority to manage, and the URL is handed over to the authority management interface

Role personnel table

CREATE TABLE `h_role_user` (
                               `role_id` int NOT NULL COMMENT '角色id',
                               `user_id` int NOT NULL COMMENT '菜单id'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COMMENT '角色用户表';

role menu sheet

CREATE TABLE `h_role_menu` (
                               `role_id` int NOT NULL COMMENT '角色id',
                               `menu_id` int NOT NULL COMMENT '菜单id'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COMMENT '角色菜单表';

Why are the relationships between Person, Role, and Menu tables separate? Why not merge the menu table into the role table? Wouldn't it be better to save a user id in the role table?
In fact, if you do this, it is really bad. If a user was in charge of finance before, but now he is transferred to another department, and now he is responsible for accounting and settlement, then the user's role must be deleted, and then Create two new records for roles, refill the role name, role code, menu URL, select people, etc.

But what about this design?
The relationship between roles and menus will basically not change, and after configuration, it will basically not change. The main change is people. Today, 1,000 new employees may be recruited, or 500 employees may leave, but we only need to maintain the role user table, isn’t it easier?

Ok, let's add some initial data to these tables

INSERT INTO `h_user` (`username`, `password`, `enabled`) VALUES ('harry', '{bcrypt}$2a$10$gQv1oUFK/LvbV7p4Nk0xE.Gn8H1lYV1hqVJfReWSUYUQBfCkGq2uy', '1');
INSERT INTO `h_user` (`username`, `password`, `enabled`) VALUES ('mike', '{bcrypt}$2a$10$gQv1oUFK/LvbV7p4Nk0xE.Gn8H1lYV1hqVJfReWSUYUQBfCkGq2uy', '1');


INSERT INTO `h_role` (`name`, `code`) VALUES ('超级管理员', 'admin'),('用户管理员','user');

INSERT INTO `h_menu` (`name`, `url`) VALUES ('后台管理', '/admin'),('用户管理','/user/**');

INSERT INTO `h_role_menu` (`role_id`, `menu_id`) VALUES (1, 1),(1,2),(2,2);

INSERT INTO `h_role_user` (`role_id`, `user_id`) VALUES (1, 1),(2,2);
  • Two users are initialized, harry and mike, with ids 1 and 2 respectively
  • Two roles are initialized, super administrator and user administrator, the code here is actually the authority
  • Two menus are initialized, namely background management and user management
  • Three role menu records are initialized, role: super administrator has the authority of menu "background management and user management", user administrator only has the authority of user management
  • Two role user records are initialized. User: harry has the role of super administrator, and user mike: has the role of user administrator. Therefore, user harry can access the background management and user management menus, while mike can only access User manages this menu, or interface

Well, after talking about the database design, let's introduce how to implement it

How to implement dynamic rights management?

Dynamically obtain url permission configuration

Create the class MenuFilterInvocationSecurityMetadataSource to implement the Collection getAttributes(Object object) method of the FilterInvocationSecurityMetadataSource interface. This implementation class is mainly to read the permissions of the menu in the database and dynamically load the permissions

Judgment of authority

Create a class: MenuAccessDecisionManager to implement the decide(Authentication authentication, Object object, Collection collection) method of the AccessDecisionManager interface, where you can check the permissions of the menu or interface

Ok, let's start building the project and witness the miracle

build project

Create a project: security-mybatis-jwt-perm

This project is based on the previous article " Study spring security step by step, chapter 10 How to log in with token?" JWT debut " project for transformation

Add maven dependency:

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-compress</artifactId>
            <version>1.18</version>
            <scope>test</scope>
        </dependency>

This dependency mainly uses toolkits inside, such as collection and string toolkits

Modify the pom project name and artifactId to be unified as: security-mybatis-jwt-perm

New database operation related classes and mapper

Create entity class

  • Create the Menu class
@Data
public class Menu {
    
    
    //菜单id
    private int id;
    //菜单名称
    private String name;
    //菜单URL
    private String url;
    //上级菜单id
    private int parentId;
}
  • Creating a Role class is actually changing the original Authorities class to a Role class, and then adding a few fields
public class Role implements GrantedAuthority {
    
    
    private int id;

    private String name;
    //权限编码
    private String code;

    @Override
    public String getAuthority() {
    
    
        return "ROLE_"+code;
    }
}
  • Modify the User class
@Data
public class User implements UserDetails {
    
    
    private int id;
    private String password;
    private String username;
    private boolean accountNonExpired=true;
    private boolean accountNonLocked=true;
    private boolean credentialsNonExpired=true;
    private boolean enabled;

    private List<Role>authorities;
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
    
    
        return authorities;
    }
    @Override
    public String getPassword() {
    
    
        return password;
    }
    @Override
    public String getUsername() {
    
    
        return username;
    }
    @Override
    public boolean isAccountNonExpired() {
    
    
        return accountNonExpired;
    }
    @Override
    public boolean isAccountNonLocked() {
    
    
        return accountNonLocked;
    }
    @Override
    public boolean isCredentialsNonExpired() {
    
    
        return credentialsNonExpired;
    }
    @Override
    public boolean isEnabled() {
    
    
        return enabled;
    }
}

Add mapper.xml of mybatis

  • Add RoleMapper.xml
<mapper namespace="com.harry.security.mapper.RoleMapper">

    <select id="findRolesByUserId" resultType="com.harry.security.entity.Role" parameterType="integer">
        select role.id,role.`name`,role.`code` from h_role role
        LEFT JOIN h_role_user ru on ru.role_id=role.id
        WHERE ru.user_id=#{userId}
    </select>

    <select id="findRolesByUrl" resultType="com.harry.security.entity.Role" parameterType="string">
        select role.id,role.`name`,role.`code` from h_role role
        LEFT JOIN h_role_menu rm on rm.role_id=role.id
        LEFT JOIN h_menu m on m.id=rm.menu_id
        WHERE m.url=#{url};
    </select>
</mapper>

Here, two queries are provided,

  1. findRolesByUserId is to query the roles that the user has according to the user id,
  2. findRolesByUrl is to query which roles the menu belongs to according to the menu URL
  • Add MenuMapper.xml
<mapper namespace="com.harry.security.mapper.MenuMapper">

    <select id="findMenusByRoleId" resultType="com.harry.security.entity.Menu" parameterType="integer">
        SELECT m.id,m.`name`,m.parent_id,m.url from h_menu m
        LEFT JOIN h_role_menu rm
        on rm.menu_id=m.id
        WHERE rm.role_id=#{roleId};
    </select>
    <select id="findAllMenus" resultType="com.harry.security.entity.Menu">
        SELECT m.id,m.`name`,m.parent_id,m.url from h_menu m
    </select>

</mapper>

There are also two query interfaces provided here,

  1. The findMenusByRoleId method is to query the management menu under the role according to the incoming role id
  2. The findAllMenus method is to query all menus
  • Modify UserMapper.xml
<mapper namespace="com.harry.security.mapper.UserMapper">

    <resultMap id="BaseUser" type="com.harry.security.entity.User" >
        <id property="id" column="id" ></id>
        <result property="username" column="username" ></result>
        <result property="password" column="password" ></result>
        <result property="enabled" column="enabled" ></result>
    </resultMap>
    <select id="findUserByUsername" resultMap="BaseUser" parameterType="string">
        select u.id,u.username,u.`password`,u.enabled from h_user u where u.username=#{username};
    </select>

</mapper>

The user has only one interface to query user information based on the user name

Add mapper interface

  • Create the RoleMapper interface
@Mapper//指定这是一个操作数据库的mapper
public interface RoleMapper {
    
    
    /**
     * 根据用户id查找角色
     * @param userId
     * @return
     */
    List<Role> findRolesByUserId(int userId);

    /**
     * 根据URL查找角色
     * @param url
     * @return
     */
    List<Role> findRolesByUrl(String url);
}
  • Create a MenuMapper interface through
@Mapper//指定这是一个操作数据库的mapper
public interface MenuMapper {
    
    

    /**
     * 根据角色id查找菜单
     * @param roleId
     * @return
     */
    List<Menu> findMenusByRoleId(int roleId);

    /**
     * 查询所有配置菜单
     * @return
     */
    List<Menu> findAllMenus();
}
  • The UserMapper interface already exists and does not need to be changed

Dynamic permission configuration

Dynamically obtain url permission configuration

Create the class MenuFilterInvocationSecurityMetadataSource and implement the interface FilterInvocationSecurityMetadataSource

public class MenuFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    
    
    @Autowired
    private MenuMapper menuMapper;
    @Autowired
    private RoleMapper roleMapper;
    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
    
    
        Set<ConfigAttribute> set = new HashSet<>();
        // 获取请求地址
        String requestUrl = ((FilterInvocation) object).getRequestUrl();
        log.info("requestUrl >> {}", requestUrl);
        List<Menu> allMenus = menuMapper.findAllMenus();
        if (!CollectionUtils.isEmpty(allMenus)) {
    
    
            List<String> urlList = allMenus.stream().filter(f->f.getUrl().endsWith("**")?requestUrl.startsWith(f.getUrl().substring(0,f.getUrl().lastIndexOf("/"))):requestUrl.equals(f.getUrl())).map(menu -> menu.getUrl()).collect(Collectors.toList());
            for (String url:urlList){
    
    
                List<Role> roles = roleMapper.findRolesByUrl(url); //当前请求需要的权限
                if(!CollectionUtils.isEmpty(roles)){
    
    
                    roles.forEach(role -> {
    
    
                        SecurityConfig securityConfig = new SecurityConfig(role.getAuthority());
                        set.add(securityConfig);
                    });
                }
            }
        }
        if (ObjectUtils.isEmpty(set)) {
    
    
            return SecurityConfig.createList("ROLE_LOGIN");
        }
        return set;
    }
    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
    
    
        return null;
    }
    @Override
    public boolean supports(Class<?> clazz) {
    
    
        return FilterInvocation.class.isAssignableFrom(clazz);
    }
}

Here, it is mainly to implement the Collection getAttributes(Object object) method to dynamically load menu permissions from the database

  • First, query all the menus from the database, and then filter to find the URL that satisfies the current request. The matching method has an exact match, or the menu configuration has ended with **, and identifies the fuzzy matching path. As long as it meets the previous matching requirements access control
  • Then, according to the menu URL that is satisfied after filtering, query its role, and return the role of the menu that needs to be controlled. In this way, the dynamic configuration of the currently accessed request URL is completed.
  • Finally, if the current request URL does not have a corresponding role configured, that is, the set collection is empty, a default role will be returned, which can be customized, mainly to identify the default role of the URL of the current request, if no default role is given , by default, the system will give an anonymous user access to all interfaces, and the request cannot enter the implementation class of permission judgment for permission control. Even if you do not log in, you can still access all interfaces, which is unreasonable. This point needs to be noted
  • The implementation of the other two methods can be written like this, don’t worry about it

It should be noted here that the menu permission is to query the database in full every time. If there is a lot of data, it may affect performance. You can modify the read cache here, but remember to update the cache data when adding and modifying menus.

Dynamic permission judgment

Create a class MenuAccessDecisionManager to implement the interface AccessDecisionManager

public class MenuAccessDecisionManager implements AccessDecisionManager {
    
    

    @Autowired
    private RoleMapper roleMapper;
    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
    
    
// 当前请求需要的权限
        log.info("collection:{}", collection);
        log.info("principal:{} authorities:{}", authentication.getPrincipal().toString());
        Object principal = authentication.getPrincipal();
        if(principal instanceof String){
    
    
            throw new BadCredentialsException("未登录");
        }

        List<Role> roleList=null;
        for (ConfigAttribute configAttribute : collection) {
    
    
            // 当前请求需要的权限
            String needRole = configAttribute.getAttribute();
            if ("ROLE_LOGIN".equals(needRole)) {
    
    
                return;
            }

            // 当前用户所具有的权限
            if(roleList==null){
    
    
                User loginUser= (User) authentication.getPrincipal();
                roleList = roleMapper.findRolesByUserId(loginUser.getId());
            }
            for (GrantedAuthority grantedAuthority : roleList) {
    
    
                // 包含其中一个角色即可访问
                if (grantedAuthority.getAuthority().equals(needRole)) {
    
    
                    return;
                }
            }
        }
        throw new AccessDeniedException("SimpleGrantedAuthority!!");
    }

    @Override
    public boolean supports(ConfigAttribute attribute) {
    
    
        return true;
    }

    @Override
    public boolean supports(Class<?> clazz) {
    
    
        return true;
    }
}

Here, the main implementation method is: decide(Authentication authentication, Object object, Collection collection)

  • collection is the permission collection of the current request URL
  • First, determine whether the user has logged in. If not, it will be an anonymous user by default. authentication.getPrincipal() gets a string, and we should get a User object after logging in. Here, it is judged that if not logged in, throw An exception occurs, telling the front-end that the current user is not logged in, what is the front-end doing?
  • Secondly, loop through the collection and take out the permissions
  • If it is the default permission, it is passed directly without control
  • Query the permissions of the currently logged-in user from the database. The purpose of this is to avoid timely fetching of the latest user permission data when the user permissions change, ensuring the timeliness of permission control
  • If it is a menu configuration permission, it is judged whether the currently logged in user has the permission, and if so, it is skipped
  • If the currently logged-in user does not have permission, an exception will be thrown, indicating that there is no permission for this interface, and access is denied

Configure SecurityConfig

 protected void configure(HttpSecurity http) throws Exception {
    
    
        http.authorizeRequests()
                .anyRequest().authenticated()
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
    
    
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O object) {
    
    
                        object.setSecurityMetadataSource(menuFilterInvocationSecurityMetadataSource); //动态获取url权限配置
                        object.setAccessDecisionManager(menuAccessDecisionManager); //权限判断
                        return object;
                    }
                })
                .and().formLogin()
                .successHandler(authenticationSuccessHandler)
                .failureHandler(authenticationFailureHandler)
        .permitAll()
                .and().exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint)
                .and().logout().logoutSuccessHandler(logoutSuccessHandler)
                .and().csrf().disable()
        ;
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

    }

This is mainly to configure the associated dynamic permission implementation class

In this way, we have basically completed all the configurations. The other interface definitions are still in the previous article " How to log in with tokens in the tenth chapter of step-by-step learning spring security?" JWT Shining Debut " is the same, the changed part has been explained above, and then we will start testing to see the effect

start test

Start the project, log in with mike, access the authorized interface, and it can be accessed normally
insert image description here

Access to an interface without permission cannot be accessed normally
insert image description here

At this time, we simulate the adjustment of the company's employees' positions. Adding a role user record in the database is to add super administrator authority to Mike
INSERT INTO h_role_user( role_id, user_id) VALUES (1, 2);

Then come to visit again, the visit is successful, which means that we have completed the control of dynamic permissions
insert image description here
and then delete the above added role user records, after deletion, there will be no permissions, and then log in with the harry user, which is consistent with the expected effect

In fact, you will find that the articles I wrote are mainly biased towards the separation of the front and back ends. Regarding the dynamic addition, deletion and modification of data, the data is manually inserted into the database. After all, there is no front-end support. Come? You can add a registration interface, combined with the registration page interaction, to complete the addition of user data, menus, roles, etc. are the same, these are not in the content of this article, please add it yourself based on actual business needs

OK, that's it, we have completed the control of dynamic permissions. Even if it is a listed company, thousands of employees join and thousands of employees leave every day, we only need to maintain the role user table to complete the permission controlled

Source code download

Guess you like

Origin blog.csdn.net/huangxuanheng/article/details/119302215