Spring Security8. Use dynamic permission verification

In the previous article, we configured the role required for the corresponding path in the SecurityConfig configuration file, and then set the role owned by the user to determine whether the user can access the path.

In our actual project development, as the system is upgraded and iterated, we develop more and more interfaces, so we have to add many similar codes to the configuration file. This is not only time-consuming and laborious, but also causes certain damage to the original code of the system, which is obviously a big problem.

It would be great if we could dynamically load permissions, just like loading accounts in dynamic mode.

Let's discuss how to dynamically load permissions

In Security, we can configure the ObjectPostProcessor in the configuration authentication and authorization policy. Through it, we can determine how to handle each request url.

1, Object postprocessor

The ObjectPostProcessor is configured in HttpSecurity. ObjectPostProcessor is mainly configured with two parameters, namely, SecurityMetadataSource authorized metadata and AccessDecisionManager permission decision management.

1. SecurityMetadataSource

The The parameter of SecurityMetadataSource is FilterInvocationSecurityMetadataSource. This class is mainly used to obtain the permissions required for the currently accessed address. This is an interface that we can implement to dynamically obtain from the data source.

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Console;
import cn.hutool.core.util.ArrayUtil;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;

import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Set;

/**
 * Customize which permission rules are required to obtain the currently accessed address
 *
 * @author lixingwu
 */
@Component
public class UrlFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    /*** ant Path matching rule */
    private final static AntPathMatcher ANT_PATH_MATCHER = new AntPathMatcher();

    /**
     * This is the main method, which needs to return the list of permissions required by the url
     */
    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        FilterInvocation filterInvocation = (FilterInvocation) object;
        String requestUrl = filterInvocation.getRequestUrl();
        Console.log("Request address is[{}]", requestUrl);
        // Ignore specified url
        Set<String> permitSet = permitAll();
        if (CollUtil.isNotEmpty(permitSet)) {
            for (String matcher : permitSet) {
                // If the current url matches the url that needs to be ignored, it will directly return null and be released directly
                if (ANT_PATH_MATCHER.match(matcher, requestUrl)) {
                    Console.log("Request address is[{}]And ignore rules[{}]Match, direct release.", requestUrl, matcher);
                    return null;
                }
            }
        }
        // Query what permissions are required to access the path according to the path
        String[] permissions = findByPath(requestUrl);
        if (ArrayUtil.isNotEmpty(permissions)) {
            Console.log("Request address is[{}]Permission required[{}]", requestUrl, permissions);
            return createList(permissions);
        }
        // There are no matching resources on the. They are all login accesses
        // Here, a permission login is directly given to identify that you can only access after you log in. It is not handled here
        // Whether the decision manager AccessDecisionManager should release or not is what the decision manager needs to do
        return createList("login");
    }

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

    @Override
    public boolean supports(Class<?> clazz) {
        return FilterInvocation.class.isAssignableFrom(clazz);
    }

    private List<ConfigAttribute> createList(String... attributeNames) {
        return org.springframework.security.access.SecurityConfig.createList(attributeNames);
    }


    /*-----------------------------------------------*/
    /*--------------- The following is the simulated data----------------*/
    /*-----------------------------------------------*/

    /***
     * This method is used to simulate those paths that can be accessed without any permission
     * In the real case, you need to query in the database, so it is easy to modify
     */
    private Set<String> permitAll() {
        return CollUtil.newHashSet("/doLogin", "/code", "/open/**");
    }

    /***
     * This method is used to simulate obtaining the permissions required to access the specified value address
     * In the real case, you need to query in the database, so it is easy to modify
     * @param requestUrl Requested address
     */
    private String[] findByPath(String requestUrl) {
        HashMap<String, String[]> map = new HashMap<>(5);
        map.put("/admin/**", new String[]{"admin"});
        map.put("/guest/**", new String[]{"admin", "guest"});
        map.put("/loginUser", new String[]{"login"});
        for (String key : map.keySet()) {
            if (ANT_PATH_MATCHER.match(key, requestUrl)) {
                return map.get(key);
            }
        }
        return new String[0];
    }
}

2.AccessDecisionManager

Permission decision management AccessDecisionManager is mainly used to determine whether the permission data provided by the current user and SecurityMetadataSource is passed or blocked. We can implement the AccessDecisionManager interface to customize the policies for these decisions.

import cn.hutool.core.lang.Console;
import cn.hutool.json.JSONUtil;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;

import java.util.Collection;
import java.util.Iterator;

/**
 * The decision manager determines whether the requested url is passed or blocked
 *
 * @author lixin
 */
@Component
public class UrlAccessDecisionManager implements AccessDecisionManager {
    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
        Console.log("The permissions required for the current path are{}", configAttributes);
        Console.log("Current login user{}", JSONUtil.toJsonStr(authentication));
        // What user roles are required for the currently accessed address
        for (ConfigAttribute configAttribute : configAttributes) {
            String needRole = configAttribute.getAttribute();
            // If you need login permission, but you have already logged in, you can directly use the
            if ("login".equals(needRole)) {
                if (authentication instanceof AnonymousAuthenticationToken) {
                    throw new AccessDeniedException("No login or login failure");
                }else {
                    return;
                }
            }
            //The permissions of the current user. If the user has the permissions required by the path, it can pass the
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
            for (GrantedAuthority authority : authorities) {
                if (authority.getAuthority().equals(needRole)) {
                    return;
                }
            }
        }
        throw new AccessDeniedException("No access");
    }

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

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

2, Configure ObjectPostProcessor to HttpSecurity

The First configure the SecurityMetadataSource and AccessDecisionManager to ObjectPostProcessor, and then configure the ObjectPostProcessor to HttpSecurity. See the code for details:

import com.miaopasi.securitydemo.config.security.handler.*;
import com.miaopasi.securitydemo.config.security.impl.UrlAccessDecisionManager;
import com.miaopasi.securitydemo.config.security.impl.UrlFilterInvocationSecurityMetadataSource;
import com.miaopasi.securitydemo.config.security.impl.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;

/**
 * Security Configuration class, which will overwrite the contents of the yml configuration file
 *
 * @author lixin
 */
@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final JsonSuccessHandler successHandler;
    private final JsonFailureHandler failureHandler;
    private final JsonAccessDeniedHandler accessDeniedHandler;
    private final JsonAuthenticationEntryPoint authenticationEntryPoint;
    private final JsonLogoutSuccessHandler logoutSuccessHandler;
    private final UserDetailsServiceImpl userDetailsService;
    private final UrlFilterInvocationSecurityMetadataSource filterInvocationSecurityMetadataSource;
    private final UrlAccessDecisionManager accessDecisionManager;

    @Autowired
    public SecurityConfig(JsonSuccessHandler successHandler, JsonFailureHandler failureHandler, JsonAccessDeniedHandler accessDeniedHandler, JsonAuthenticationEntryPoint authenticationEntryPoint, JsonLogoutSuccessHandler logoutSuccessHandler, UserDetailsServiceImpl userDetailsService, UrlFilterInvocationSecurityMetadataSource filterInvocationSecurityMetadataSource, UrlAccessDecisionManager accessDecisionManager) {
        this.successHandler = successHandler;
        this.failureHandler = failureHandler;
        this.accessDeniedHandler = accessDeniedHandler;
        this.authenticationEntryPoint = authenticationEntryPoint;
        this.logoutSuccessHandler = logoutSuccessHandler;
        this.userDetailsService = userDetailsService;
        this.filterInvocationSecurityMetadataSource = filterInvocationSecurityMetadataSource;
        this.accessDecisionManager = accessDecisionManager;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            	// Configure the processor for the request object
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O object) {
                        // Configure url metadata
                        object.setSecurityMetadataSource(filterInvocationSecurityMetadataSource);
                        // Decision maker for configuring url permissions
                        object.setAccessDecisionManager(accessDecisionManager);
                        return object;
                    }
                })
                .anyRequest().authenticated()
                .and().formLogin()
                .usernameParameter("username")
                .passwordParameter("password")
                .loginProcessingUrl("/doLogin")
                .successHandler(successHandler)
                .failureHandler(failureHandler)
                .and().logout().logoutUrl("/doLogout")
                .logoutSuccessHandler(logoutSuccessHandler)
                .and().exceptionHandling()
                .accessDeniedHandler(accessDeniedHandler)
                .authenticationEntryPoint(authenticationEntryPoint)
                .and().cors()
                .and().csrf().disable();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

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

3, Set permissions for logged in users

The In the above steps, we set permissions for the url resource. Only users with specified permissions can access it. Now we need to assign the specified permission to the user, otherwise the user will not be able to access without permission. Now we only need to set the user's permission information into the user object when logging in. The user-defined decision manager UrlAccessDecisionManager can obtain the user's permission, and then execute the logic in the decision manager.

The The user will call the loadUserByUsername method in our UserDetailsServiceImpl class when logging in. We can query the user's permissions here and set them to the logged in user. The complete code is as follows:

import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.lang.Console;
import com.miaopasi.securitydemo.config.security.SysUser;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.util.*;

/**
 * Custom query UserDetails
 *
 * @author lixin
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Console.log("Query by user name UserDetails´╝î{}", username);
        Optional<UserDetails> first = userDetailsList().stream()
                .filter(userDetails -> Objects.equals(userDetails.getUsername(), username))
                .findFirst();
        if (first.isPresent()) {
            return first.get();
        }
        throw new BadCredentialsException("[" + username + "]user does not exist");
    }

    /**
     * Under normal circumstances, we should query the data in the database, but for the convenience of display, we use the simulated query data here
     * Of course, in addition to database query, we can also crawl through files, memory, or even the network to obtain user information, as long as we can provide data sources.
     * Simulate the list of user information queried from the database,
     */
    private List<UserDetails> userDetailsList() {
        List<UserDetails> userDetails = new ArrayList<>(10);
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        SysUser user;
        for (int i = 0; i < 10; i++) {
            user = new SysUser();
            user.setId(Convert.toLong(i));
            user.setGmtCreate(DateTime.now());
            user.setOperator("administrators");
            user.setIsDelete(false);
            user.setSort(BigDecimal.valueOf(0));
            user.setStatus(0);
            user.setRemarks("Test user" + i);
            user.setUsername("user" + i);
            user.setPassword(passwordEncoder.encode("pwd_" + i));

            // Set user permission information
            user.setAuthorities(listByUserId(user.getId()));
            userDetails.add(user);
        }

        userDetails.forEach(Console::log);
        return userDetails;
    }

    /**
     * [Data simulation: query the permissions required by the user according to the user id
     * Here we simulate user0 - > [admin, guest]
     * Other users user[0|9] - > [guest]
     *
     * @param userId User id
     * @return the list
     */
    private List<GrantedAuthority> listByUserId(Long userId) {
        HashMap<Long, List<GrantedAuthority>> map = new HashMap<>();
        for (int i = 0; i < 10; i++) {
            List<GrantedAuthority> list = new ArrayList<>();
            // Only 0 user has the permission of admin
            if (userId == 0) {
                list.add(new SimpleGrantedAuthority("admin"));
            }
            list.add(new SimpleGrantedAuthority("guest"));
            map.put(userId, list);
        }
        return map.get(userId);
    }
}

4, Testing

(1) According to our simulated data, we set some addresses that can be accessed without any permission:

/doLogin, /code, /open/**, we do not log in to access these interfaces in turn and find that the returned data is normal.

# /doLogin
{
  "msg": "Login successful",
  "code": 0,
  "data": {
    "authenticated": true,
    "authorities": [
      {}
    ],
    "principal": {
      "isDelete": false,
      "sort": 0,
      "gmtCreate": 1594827663999,
      "operator": "administrators",
      "authorities": [
        {}
      ],
      "id": 1,
      "remarks": "Test user 1",
      "username": "user1",
      "status": 0
    },
    "details": {
      "remoteAddress": "127.0.0.1"
    }
  }
}

# /code
QXAN8A

# /open/get
open get

If we access an interface that is not ignored, such as: /admin/get, the JSON string is returned:

{
  "msg": "No login or login failure",
  "code": 1001,
  "data": "Full authentication is required to access this resource"
}

(2) The permission policy we assign to the path when simulating data is:

expression authority
/admin/** admin
/guest/** admin, guest
[other or unspecified] login

The user assigned permission policy is:

Account number authority
user0 admin, guest
user[1|9] guest

According to the assigned strategy, we can conclude that:

user0 can access all interfaces after logging in. user1 to user9 can access all interfaces except /admin/** interface.

We now log in to user0 and access /admin/get, /guest/get, /open/get and test/get to return JSON strings normally.

Then we log in to user1 and access /guest/get, /open/get and test/get to return JSON strings normally. Access /admin/get to return JSON characters:

{
  "msg": "No access",
  "code": 1002,
  "data": "No access"
}

5, To be brief

Dynamic permission uses simulated data in this article. The project should load data according to its own business. In fact, it is mainly to query the url permission list and the user's permission list, so that we can dynamically operate the permission list and the user's permission list by ourselves.

For the spring security series, please click here View.
This is the code Code cloud address .
Attention attention!!! The project uses the branch method to submit the code for each test. Please switch the branch according to the chapter.

Tags: Spring Security

Posted by serenade2 on Mon, 30 May 2022 03:14:56 +0530