SpringBoot actual e-business project mall (30K + STAR) address: github.com/macrozheng/…

Abstract

As a necessary function in the background management system, the Mall project combined with Spring Security realizes the path-based dynamic permission control, which can carry out fine-grained control over the background interface access. Today, we will talk about its back-end implementation principle.

Front knowledge

You will need some knowledge of Spring Security to learn from this article. If you are not familiar with Spring Security, you can read the following article.

  • Mall integrates SpringSecurity and JWT for authentication and authorization
  • Mall integrates SpringSecurity and JWT for authentication and authorization
  • In just four steps, integrate SpringSecurity+JWT for login authentication!

Database design

The permission management table has been redesigned to split the original permissions into menus and resources. Menu management controls the display and hiding of front-end menus, and resource management controls the access permissions of back-end interfaces.

Database table structure

Ums_admin, UMS_ROLE, and ums_ADMIN_ROLE_RELATION are original tables, and other tables are newly added.

Introduction to Database Tables

The purpose of each table is described in detail below.

ums_admin

Background user table, which defines some basic information about background users.

create table ums_admin
(
   id                   bigint not null auto_increment,
   username             varchar(64) comment 'Username'.password             varchar(64) comment 'password',
   icon                 varchar(500) comment 'avatar',
   email                varchar(100) comment 'email',
   nick_name            varchar(200) comment 'nickname',
   note                 varchar(500) comment 'Remarks',
   create_time          datetime comment 'Creation time',
   login_time           datetime comment 'Last Login Time'.status               int(1) default 1 comment 'Account enabled status: 0-> Disabled; 1 - > enable ',
   primary key (id));Copy the code

ums_role

Background user role table, which defines basic information about background user roles and allocates menus and resources by assigning roles to background users.

create table ums_role
(
   id                   bigint not null auto_increment,
   name                 varchar(100) comment 'name',
   description          varchar(500) comment 'description',
   admin_count          int comment 'Number of background users',
   create_time          datetime comment 'Creation time'.status               int(1) default 1 comment 'Enabled status: 0-> Disabled; 1 - > enable '.sort                 int default 0,
   primary key (id));Copy the code

ums_admin_role_relation

Background user and role relationship table, many-to-many relationship table, a role can be assigned to multiple users.

create table ums_admin_role_relation
(
   id                   bigint not null auto_increment,
   admin_id             bigint,
   role_id              bigint,
   primary key (id));Copy the code

ums_menu

Background menu list, used to control the background users can access the menu, support to hide, sort, and change the name, icon.

create table ums_menu
(
   id                   bigint not null auto_increment,
   parent_id            bigint comment 'the parent ID',
   create_time          datetime comment 'Creation time',
   title                varchar(100) comment 'Menu name'.level                int(4) comment 'Menu series'.sort                 int(4) comment 'Menu sort'.name                 varchar(100) comment 'Front end Name',
   icon                 varchar(200) comment 'Front icon',
   hidden               int(1) comment 'Front Hide',
   primary key (id));Copy the code

ums_resource

The background resource table, used to control the interfaces that background users can access, uses matching rules for Ant paths, and can define permissions for a range of interfaces using wildcards.

create table ums_resource
(
   id                   bigint not null auto_increment,
   category_id          bigint comment 'Resource Category ID',
   create_time          datetime comment 'Creation time'.name                 varchar(200) comment 'Resource Name'.url                  varchar(200) comment 'resource URL',
   description          varchar(500) comment 'description',
   primary key (id));Copy the code

ums_resource_category

Background resource classification table. When fine-grained permission control is implemented, resources may be large. Therefore, a resource classification concept is designed to facilitate the allocation of resources to roles.

create table ums_resource_category
(
   id                   bigint not null auto_increment,
   create_time          datetime comment 'Creation time'.name                 varchar(200) comment 'Category name'.sort                 int(4) comment 'order',
   primary key (id));Copy the code

ums_role_menu_relation

Background role menu relationship table, many-to-many relationship, can assign a role to multiple menus.

create table ums_role_menu_relation
(
   id                   bigint not null auto_increment,
   role_id              bigint comment 'character ID',
   menu_id              bigint comment 'menu ids',
   primary key (id));Copy the code

ums_role_resource_relation

Background role resource relationship table, many-to-many relationship, can allocate multiple resources to a role.

create table ums_role_resource_relation
(
   id                   bigint not null auto_increment,
   role_id              bigint comment 'character ID',
   resource_id          bigint comment 'resource ID',
   primary key (id));Copy the code

Implemented with Spring Security

Dynamic permissions are realized on the basis of the original mall-Security module. If the original implementation is not clear, you can learn by referring to the documents in the pre-knowledge.

Previous permission control

The previous permission control was implemented using the default mechanism of Spring Security. Let’s take the code of the commodity module as an example to explain the implementation principle.

  • First we use it on interfaces that require permissions@PreAuthorizeAnnotations define the required permissions;
/** * Controller * Created by macro on 2018/4/26. */
@Controller
@Api(tags = "PmsProductController", description = "Commodity Management")
@RequestMapping("/product")
public class PmsProductController {
    @Autowired
    private PmsProductService productService;

    @ApiOperation("Create goods")
    @RequestMapping(value = "/create", method = RequestMethod.POST)
    @ResponseBody
    @PreAuthorize("hasAuthority('pms:product:create')")
    public CommonResult create(@RequestBody PmsProductParam productParam, BindingResult bindingResult) {
        int count = productService.create(productParam);
        if (count > 0) {
            return CommonResult.success(count);
        } else {
            returnCommonResult.failed(); }}}Copy the code
  • Then, the permission value is saved in the permission table. When the user logs in, the user’s permission is queried.
/** * UmsAdminService implementation * Created by macro on 2018/4/26. */
@Service
public class UmsAdminServiceImpl implements UmsAdminService {
    @Override
    public UserDetails loadUserByUsername(String username){
        // Get user information
        UmsAdmin admin = getAdminByUsername(username);
        if(admin ! =null) {
            List<UmsPermission> permissionList = getPermissionList(admin.getId());
            return new AdminUserDetails(admin,permissionList);
        }
        throw new UsernameNotFoundException("Wrong username or password"); }}Copy the code
  • Spring Security then compares the user’s permission value with the value defined in the interface annotations. If the user has the permission value, the user can access it, and if the user has the permission value, the user cannot access it.

  • However, this can cause some problems. We need to define the access permission value on each interface, and can only control the permission of each interface, not batch control. Each interface can be uniquely identified by its access path, and we can use path-based dynamic permission control to solve these problems.

Path-based dynamic permission control

Let’s take a closer look at implementing path-based dynamic permissions using Spring Security.

First, we need to create a filter for dynamic permission control. Here we need to pay attention to the doFilter method, the OPTIONS request is directly allowed, otherwise the front-end call will have cross-domain problems. I also need to route the white list path configured in IgnoreUrlsConfig, all the authentication is done in super-.beforeInvocation (FI).

** Created by macro on 2020/2/7. */
public class DynamicSecurityFilter extends AbstractSecurityInterceptor implements Filter {

    @Autowired
    private DynamicSecurityMetadataSource dynamicSecurityMetadataSource;
    @Autowired
    private IgnoreUrlsConfig ignoreUrlsConfig;

    @Autowired
    public void setMyAccessDecisionManager(DynamicAccessDecisionManager dynamicAccessDecisionManager) {
        super.setAccessDecisionManager(dynamicAccessDecisionManager);
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {}@Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain);
        //OPTIONS requests permission directly
        if(request.getMethod().equals(HttpMethod.OPTIONS.toString())){
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
            return;
        }
        // Whitelist requests direct release
        PathMatcher pathMatcher = new AntPathMatcher();
        for (String path : ignoreUrlsConfig.getUrls()) {
            if(pathMatcher.match(path,request.getRequestURI())){
                fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
                return; }}// The decide method in AccessDecisionManager is called for authentication
        InterceptorStatusToken token = super.beforeInvocation(fi);
        try {
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        } finally {
            super.afterInvocation(token, null); }}@Override
    public void destroy(a) {}@Override
    publicClass<? > getSecureObjectClass() {return FilterInvocation.class;
    }

    @Override
    public SecurityMetadataSource obtainSecurityMetadataSource(a) {
        returndynamicSecurityMetadataSource; }}Copy the code

The Decide method in AccessDecisionManager is called when the super.beforeInvocation(FI) method is called in DynamicSecurityFilter for authentication, The configAttributes parameter in the Decide method is obtained through the getAttributes method on SecurityMetadataSource. ConfigAttributes is the configured permission to access the current interface. Here is the simplified version of the beforeInvocation source code.

public abstract class AbstractSecurityInterceptor implements InitializingBean.ApplicationEventPublisherAware.MessageSourceAware {
    

protected InterceptorStatusToken beforeInvocation(Object object) {
        
        // Get metadata
		Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
				.getAttributes(object);

		Authentication authenticated = authenticateIfRequired();

		// Perform authentication
		try {
			this.accessDecisionManager.decide(authenticated, object, attributes);
		}
		catch (AccessDeniedException accessDeniedException) {
			publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
					accessDeniedException));

			throwaccessDeniedException; }}}Copy the code

Now that we know how authentication works, we need to implement the getAttributes method of the SecurityMetadataSource interface ourselves to get resources for the current access path.

/** * Created by macro on 2020/2/7. */
public class DynamicSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    private static Map<String, ConfigAttribute> configAttributeMap = null;
    @Autowired
    private DynamicSecurityService dynamicSecurityService;

    @PostConstruct
    public void loadDataSource(a) {
        configAttributeMap = dynamicSecurityService.loadDataSource();
    }

    public void clearDataSource(a) {
        configAttributeMap.clear();
        configAttributeMap = null;
    }

    @Override
    public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
        if (configAttributeMap == null) this.loadDataSource();
        List<ConfigAttribute>  configAttributes = new ArrayList<>();
        // Get the current access path
        String url = ((FilterInvocation) o).getRequestUrl();
        String path = URLUtil.getPath(url);
        PathMatcher pathMatcher = new AntPathMatcher();
        Iterator<String> iterator = configAttributeMap.keySet().iterator();
        // Get the resources needed to access the path
        while (iterator.hasNext()) {
            String pattern = iterator.next();
            if(pathMatcher.match(pattern, path)) { configAttributes.add(configAttributeMap.get(pattern)); }}// Return an empty set
        return configAttributes;
    }

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

    @Override
    public boolean supports(Class
        aClass) {
        return true; }}Copy the code

Since our background resource rules are cached in a Map object, when the background resource changes, we need to clear the cached data and reload it the next time we query it. Here we need to modify UmsResourceController, inject DynamicSecurityMetadataSource, when change the background resources, you need to call clearDataSource method to clear the cache data.

/** * Created by macro on 2020/2/4. */
@Controller
@Api(tags = "UmsResourceController", description = "Background Resource Management")
@RequestMapping("/resource")
public class UmsResourceController {

    @Autowired
    private UmsResourceService resourceService;
    @Autowired
    private DynamicSecurityMetadataSource dynamicSecurityMetadataSource;

    @ApiOperation("Add Background Resources")
    @RequestMapping(value = "/create", method = RequestMethod.POST)
    @ResponseBody
    public CommonResult create(@RequestBody UmsResource umsResource) {
        int count = resourceService.create(umsResource);
        dynamicSecurityMetadataSource.clearDataSource();
        if (count > 0) {
            return CommonResult.success(count);
        } else {
            returnCommonResult.failed(); }}}Copy the code

Then we need to implement the AccessDecisionManager interface to verify permissions. For interfaces without resources, we directly allow access; for interfaces configured with resources, we compare the resources required for access with the resources owned by the user, and allow access if they match.

/** * Created by macro on 2020/2/7. */
public class DynamicAccessDecisionManager implements AccessDecisionManager {

    @Override
    public void decide(Authentication authentication, Object object, Collection
       
         configAttributes)
        throws AccessDeniedException, InsufficientAuthenticationException {
        // If no resource is configured on the interface, the interface is allowed
        if (CollUtil.isEmpty(configAttributes)) {
            return;
        }
        Iterator<ConfigAttribute> iterator = configAttributes.iterator();
        while (iterator.hasNext()) {
            ConfigAttribute configAttribute = iterator.next();
            // Compare access required resources or user-owned resources
            String needAuthority = configAttribute.getAttribute();
            for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
                if (needAuthority.trim().equals(grantedAuthority.getAuthority())) {
                    return; }}}throw new AccessDeniedException("Sorry, you don't have access.");
    }

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

    @Override
    public boolean supports(Class
        aClass) {
        return true; }}Copy the code

Before us in the DynamicSecurityMetadataSource injected a DynamicSecurityService object, it is my custom a dynamic access business interface, it is mainly used for loading all the background resources rules.

/** * Created by macro on 2020/2/7. */
public interface DynamicSecurityService {
    /** * Load resources ANT wildcards and resources correspond to MAP */
    Map<String, ConfigAttribute> loadDataSource(a);
}
Copy the code

Then we need to modify the Spring Security configuration class SecurityConfig, when there is a dynamic access business class add our dynamic permissions before FilterSecurityInterceptor filter filter. ConditionalOnBean (@conditionAlonbean) : ConditionalOnBean (@conditionAlonbean) ConditionalOnBean (@conditionAlonbean) : ConditionalOnBean (@conditionAlonbean) ConditionalOnBean

/** * Created by macro on 2019/11/5. */ Created by macro on 2019/11/5
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired(required = false)
    private DynamicSecurityService dynamicSecurityService;

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity
                .authorizeRequests();
        // Add a dynamic permission check filter when dynamic permission is configured
        if(dynamicSecurityService! =null){ registry.and().addFilterBefore(dynamicSecurityFilter(), FilterSecurityInterceptor.class); }}@ConditionalOnBean(name = "dynamicSecurityService")
    @Bean
    public DynamicAccessDecisionManager dynamicAccessDecisionManager(a) {
        return new DynamicAccessDecisionManager();
    }


    @ConditionalOnBean(name = "dynamicSecurityService")
    @Bean
    public DynamicSecurityFilter dynamicSecurityFilter(a) {
        return new DynamicSecurityFilter();
    }

    @ConditionalOnBean(name = "dynamicSecurityService")
    @Bean
    public DynamicSecurityMetadataSource dynamicSecurityMetadataSource(a) {
        return newDynamicSecurityMetadataSource(); }}Copy the code

There is a question need to ask, no current end cross domain access interface, there will be a cross-domain problem, only need to added in the class don’t have permission to access treatment RestfulAccessDeniedHandler allow cross-domain access response headers.

/** * Created by macro on 2018/4/26. */
public class RestfulAccessDeniedHandler implements AccessDeniedHandler{
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
        response.setHeader("Access-Control-Allow-Origin"."*");
        response.setHeader("Cache-Control"."no-cache");
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json"); response.getWriter().println(JSONUtil.parse(CommonResult.forbidden(e.getMessage()))); response.getWriter().flush(); }}Copy the code

When our other modules need dynamic permissions, we simply create a DynamicSecurityService object. For example, we have dynamic permissions enabled in the mall-admin module.

/** * Created by macro on 2019/11/9. */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MallSecurityConfig extends SecurityConfig {

    @Autowired
    private UmsAdminService adminService;
    @Autowired
    private UmsResourceService resourceService;

    @Bean
    public UserDetailsService userDetailsService(a) {
        // Obtain the login user information
        return username -> adminService.loadUserByUsername(username);
    }

    @Bean
    public DynamicSecurityService dynamicSecurityService(a) {
        return new DynamicSecurityService() {
            @Override
            public Map<String, ConfigAttribute> loadDataSource(a) {
                Map<String, ConfigAttribute> map = new ConcurrentHashMap<>();
                List<UmsResource> resourceList = resourceService.listAll();
                for (UmsResource resource : resourceList) {
                    map.put(resource.getUrl(), new org.springframework.security.access.SecurityConfig(resource.getId() + ":" + resource.getName()));
                }
                returnmap; }}; }}Copy the code

Demonstration of permission management function

Specific reference: we heart and mind of the authority management function, this arrangement!

Project source code address

Github.com/macrozheng/…

The public,

Mall project full set of learning tutorials serialized, attention to the public number the first time access.