Spring security - user dynamic authorization and dynamic role permissions

Posted by MyWebAlias on Sun, 09 Jan 2022 10:45:05 +0100

1, Spring security dynamic authorization

In the last article, we introduced the dynamic authentication of spring security. The last article said that the two main functions of spring security are authentication and authorization. Since authentication and learning, this article learned the dynamic authorization of spring security together.

Last article address: https://blog.csdn.net/qq_43692950/article/details/122393435

2, Spring security authorization

We continue to modify the project in the previous article. In the previous article, it was said that the configure(HttpSecurity http) method in our WebSecurityConfig configuration class is used for authorization. Now we can experience it. For example, when we modify the interface starting with admin, admin is required in permissions or roles:

 @Override
 protected void configure(HttpSecurity http) throws Exception {
     http.authorizeRequests()
             .antMatchers("/admin/**").hasAuthority("admin")
             .antMatchers("/**").fullyAuthenticated()
             .and()
             .formLogin()
             .permitAll()
             .and()
             .csrf().disable();
 }

Next, use the admin user to access the admin/test interface:

403 no permission error is reported because we have set the admin / * * interface. You must have admin permission. You can see the UserService class written in the previous article:

Here, an admin ROLE is directly set for users. There is a problem here. What is the difference between permissions and roles? In fact, permissions and roles are put together in spring security. It can be said that they are the same conceptually, but roles are based on roles_ At the beginning.

It should also be noted that if the authorization role can use hasRole() and hasAnyRole(), if it is the authorization permission, use hasAuthority() and hasAnyAuthority()

ROLE authorization: the authorization code needs to be added with ROLE_ Prefix. Do not add prefix when using on controller.
Permission authorization: when setting and using, the name can be kept for one to.

Therefore, you can modify the UserService class:

Request interface here:

Now you have permission to access, but writing is definitely not the effect we want, so you can put the role in the database and dynamically obtain the user's role by querying the database.

Next, you need to create a role table in the database:

CREATE TABLE `role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `role` varchar(255) NOT NULL,
  `role_describe` varchar(255) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

Roles must be related to people, and sometimes have many to many relationships. Therefore, according to the relationship model, we need to extract a role user relationship table:

CREATE TABLE `user_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `userid` int(11) NOT NULL,
  `roleid` int(11) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;


The addition of roles and associated users are nothing more than the addition, deletion and modification of the database. This is not demonstrated here. You can directly create a table, add several roles to the table and associate users:


Add RoleEntity entity

@Data
@TableName("/role")
public class RoleEntity {
    private Long id;
    private String role;
    @TableField("role_describe")
    private String roleDescribe;
}

RoleMapper class, and write the interface to query all roles according to user id:

@Mapper
@Repository
public interface RoleMapper extends BaseMapper<RoleEntity> {

    @Select("SELECT r.id,r.role,r.role_describe FROM user_role u,role r where u.roleid = r.id AND u.userid = #{userId}")
    List<RoleEntity> getAllRoleByUserId(@Param("userId") Integer userId);
}

Modify UserService class:

@Service
public class UserService implements UserDetailsService {
    @Autowired
    UserMapper userMapper;

    @Autowired
    RoleMapper roleMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        LambdaQueryWrapper<UserEntity> wrapper = new LambdaQueryWrapper<UserEntity>()
                .eq(UserEntity::getUsername, username);
        UserEntity userEntity = userMapper.selectOne(wrapper);
        if (userEntity == null) {
            throw new UsernameNotFoundException("user does not exist!");
        }
        List<GrantedAuthority> auths = roleMapper.getAllRoleByUserId(userEntity.getId())
                .stream()
                .map(r -> new SimpleGrantedAuthority(r.getRole()))
                .collect(Collectors.toList());
        userEntity.setRoles(auths);
        return userEntity;
    }

    public boolean register(String userName, String password) {
        UserEntity entity = new UserEntity();
        entity.setUsername(userName);
        entity.setPassword(new BCryptPasswordEncoder().encode(password));
        entity.setEnabled(true);
        entity.setLocked(false);
        return userMapper.insert(entity) > 0;
    }
}

Now you can test and access the above interface again in the browser:

However, it was found that it was 403. The reason is that we set the permission admin for admin, not the role. The role is stored in the database_ Admin, here is to make you more profound about the difference between the two. Modify the database to admin

Restart access again:

It's already accessible. You should have a certain understanding of permissions and roles. The following describes the methods of authorizing and granting roles:

  • hasRole :
    If the user has a given ROLE, access is allowed, otherwise 403 appears. There is no need to write ROLE when authorizing the interface_ At the beginning, because the underlying code will be automatically added to match it, users must write ROLE_whenadding roles.

  • hasAnyRole
    It means that the user can access any condition.

  • hasAuthority :
    Returns true if the current principal has the specified permission; otherwise, returns false

  • hasAnyAuthority
    Returns true if the current principal has any provided roles (given as a comma separated string list)

Now we know how to authorize users and how to grant permissions to interfaces, but there is still a problem:

It's not appropriate to write this in the code. In fact, there are two schemes. One is that in the scenario where the fixed address and role change little, you can read it from the database and map the role through the HttpSecurity object, but this scheme is not good for dynamically adding roles during project operation. Another solution is to implement the FilterInvocationSecurityMetadataSource interface, which returns all the roles of the url according to the currently accessed url. Obviously, the latter is more flexible, but every time you access the interface, you get all the roles, which will certainly lose performance.

The following two cases are implemented respectively:

3, Database reading is authorized through HttpSecurity

The role table has been created above. Now we need to associate URLs with roles, so add a menu table to store URLs:

CREATE TABLE `menu` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `pattern` varchar(255) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

Menu and role are also many to many relationships, so you also need to create a menu_role relationship table:

CREATE TABLE `menu_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `menu_id` int(11) NOT NULL,
  `role_id` int(11) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;

Or add some data to the table:


Create MeunEntity entity class:

@Data
@TableName("menu")
public class MeunEntity {
    @TableId(type = IdType.AUTO)
    private Integer id;
    private String pattern;
}

MeunMapper inherits BaseMapper

@Mapper
@Repository
public interface MeunMapper extends BaseMapper<MeunEntity> {
}

Modify RoleMapper:

@Mapper
@Repository
public interface RoleMapper extends BaseMapper<RoleEntity> {

    @Select("SELECT r.id,r.role,r.role_describe FROM user_role u,role r where u.roleid = r.id AND u.userid = #{userId}")
    List<RoleEntity> getAllRoleByUserId(@Param("userId") Integer userId);

    @Select("SELECT r.id,r.role,r.role_describe FROM menu_role m,role r where m.role_id = r.id AND m.menu_id = #{menuId}")
    List<RoleEntity> getAllRoleByMenuId(@Param("menuId") Integer menuId);
}

To modify WebSecurityConfig:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserService userService;

    @Autowired
    MeunMapper meunMapper;

    @Autowired
    RoleMapper roleMapper;

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService).passwordEncoder(password());
    }

    @Bean
    PasswordEncoder password() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry authorizeRequests = http
                .authorizeRequests();
        List<MeunEntity> meunEntities = meunMapper.selectList(null);
        meunEntities.forEach(m -> {
            authorizeRequests.antMatchers(m.getPattern()).hasAnyAuthority(roleMapper.getAllRoleByMenuId(m.getId())
                    .stream()
                    .map(RoleEntity::getRole).toArray(String[]::new));
        });
        authorizeRequests.antMatchers("/**").fullyAuthenticated()
                .and()
                .formLogin()
                .permitAll()
                .and()
                .csrf().disable();
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/register/**");
    }
}

Restart the project and then access the test interface again. The same effect as above has been achieved:

4, Dynamic role through FilterInvocationSecurityMetadataSource

The first scheme has been implemented above. Next, continue to implement the second scheme. Next, create a class to implement the FilterInvocationSecurityMetadataSource interface:

@Component
public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    @Autowired
    MeunMapper meunMapper;

    @Autowired
    RoleMapper roleMapper;

    //Used to implement ant style Url matching
    AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        //Gets the Url of the current request
        String requestUrl = ((FilterInvocation) object).getRequestUrl();
        List<MeunEntity> list = meunMapper.selectList(null);
        List<ConfigAttribute> roles = new ArrayList<>();
        list.forEach(m -> {
            if (antPathMatcher.match(m.getPattern(), requestUrl)) {
                List<ConfigAttribute> allRoleByMenuId = roleMapper.getAllRoleByMenuId(m.getId())
                        .stream()
                        .map(r -> new SecurityConfig(r.getRole()))
                        .collect(Collectors.toList());
                roles.addAll(allRoleByMenuId);
            }
        });
        if (!roles.isEmpty()) {
            return roles;
        }
        return SecurityConfig.createList("ROLE_LOGIN");
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

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

You also need to create a CustomAccessDecisionManager to implement the AccessDecisionManager:

@Component
public class CustomAccessDecisionManager implements AccessDecisionManager {

    @Override
    public void decide(Authentication auth, Object object, Collection<ConfigAttribute> ca) throws AccessDeniedException, InsufficientAuthenticationException {
        for (ConfigAttribute configAttribute : ca) {
        	//If the role required for the request Url is ROLE_LOGIN, indicating that the current Url user can access after logging in
            if ("ROLE_LOGIN".equals(configAttribute.getAttribute()) && auth instanceof UsernamePasswordAuthenticationToken){ 
                return;
            }
            Collection<? extends GrantedAuthority> auths = auth.getAuthorities(); //Gets the role that the login user has
            for (GrantedAuthority grantedAuthority : auths) {
                if (configAttribute.getAttribute().equals(grantedAuthority.getAuthority())){
                    return;
                }
            }
        }
        throw new AccessDeniedException("Insufficient permissions");
    }

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

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

Modify WebSecurityConfig

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserService userService;

    @Autowired
    CustomAccessDecisionManager customAccessDecisionManager;

    @Autowired
    CustomFilterInvocationSecurityMetadataSource customFilterInvocationSecurityMetadataSource;

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService).passwordEncoder(password());
    }

    @Bean
    PasswordEncoder password() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O o) {
                        o.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource);
                        o.setAccessDecisionManager(customAccessDecisionManager);
                        return o;
                    }
                })
                .antMatchers("/**").fullyAuthenticated()
                .and()
                .formLogin()
                .permitAll()
                .and()
                .csrf().disable();
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/register/**");
    }

    @Bean
    RoleHierarchy roleHierarchy() {
        RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
        String hierarchy = "ROLE_admin > ROLE_user > ROLE_common";
        roleHierarchy.setHierarchy(hierarchy);
        return roleHierarchy;
    }
}

When the above test interface is tested again, it can be found that the same effect is achieved:

However, it is a dynamic role at this time. We can create a new user, give the new user a new role, and then give the role the permission of admin / *.

Create user adc

Add role:

Role bound user:

Role binding menu:


The following is to clear the browser cache and log in with abc user:


Successful access to the interface indicates that the dynamic role permission has taken effect.


Love little buddy can pay attention to my personal WeChat official account and get more learning materials.

Topics: Microservices security