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
@PreAuthorize
Annotations 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.