Spring?Security權(quán)限管理實現(xiàn)接口動態(tài)權(quán)限控制
SpringBoot實戰(zhàn)電商項目mall(30k+star)地址:https://github.com/macrozheng/mall
摘要
權(quán)限控管理作為后臺管理系統(tǒng)中必要的功能,mall項目中結(jié)合Spring Security實現(xiàn)了基于路徑的動態(tài)權(quán)限控制,可以對后臺接口訪問進行細粒度的控制,今天我們來講下它的后端實現(xiàn)原理。
前置知識
學習本文需要一些Spring Security的知識,對Spring Security不太了解的朋友可以看下以下文章。
- mall整合SpringSecurity和JWT實現(xiàn)認證和授權(quán)(一)
- mall整合SpringSecurity和JWT實現(xiàn)認證和授權(quán)(二)
- 僅需四步,整合SpringSecurity+JWT實現(xiàn)登錄認證!
數(shù)據(jù)庫設(shè)計
權(quán)限管理相關(guān)表已經(jīng)重新設(shè)計,將原來的權(quán)限拆分成了菜單和資源,菜單管理用于控制前端菜單的顯示和隱藏,資源管理用來控制后端接口的訪問權(quán)限。
數(shù)據(jù)庫表結(jié)構(gòu)
其中ums_admin
、ums_role
、ums_admin_role_relation
為原來的表,其他均為新增表。
數(shù)據(jù)庫表介紹
接下來我們將對每張表的用途做個詳細介紹。
ums_admin
后臺用戶表,定義了后臺用戶的一些基本信息。
create table ums_admin ( id bigint not null auto_increment, username varchar(64) comment '用戶名', password varchar(64) comment '密碼', icon varchar(500) comment '頭像', email varchar(100) comment '郵箱', nick_name varchar(200) comment '昵稱', note varchar(500) comment '備注信息', create_time datetime comment '創(chuàng)建時間', login_time datetime comment '最后登錄時間', status int(1) default 1 comment '帳號啟用狀態(tài):0->禁用;1->啟用', primary key (id) );
ums_role
后臺用戶角色表,定義了后臺用戶角色的一些基本信息,通過給后臺用戶分配角色來實現(xiàn)菜單和資源的分配。
create table ums_role ( id bigint not null auto_increment, name varchar(100) comment '名稱', description varchar(500) comment '描述', admin_count int comment '后臺用戶數(shù)量', create_time datetime comment '創(chuàng)建時間', status int(1) default 1 comment '啟用狀態(tài):0->禁用;1->啟用', sort int default 0, primary key (id) );
ums_admin_role_relation
后臺用戶和角色關(guān)系表,多對多關(guān)系表,一個角色可以分配給多個用戶。
create table ums_admin_role_relation ( id bigint not null auto_increment, admin_id bigint, role_id bigint, primary key (id) );
ums_menu
后臺菜單表,用于控制后臺用戶可以訪問的菜單,支持隱藏、排序和更改名稱、圖標。
create table ums_menu ( id bigint not null auto_increment, parent_id bigint comment '父級ID', create_time datetime comment '創(chuàng)建時間', title varchar(100) comment '菜單名稱', level int(4) comment '菜單級數(shù)', sort int(4) comment '菜單排序', name varchar(100) comment '前端名稱', icon varchar(200) comment '前端圖標', hidden int(1) comment '前端隱藏', primary key (id) );
ums_resource
后臺資源表,用于控制后臺用戶可以訪問的接口,使用了Ant路徑的匹配規(guī)則,可以使用通配符定義一系列接口的權(quán)限。
create table ums_resource ( id bigint not null auto_increment, category_id bigint comment '資源分類ID', create_time datetime comment '創(chuàng)建時間', name varchar(200) comment '資源名稱', url varchar(200) comment '資源URL', description varchar(500) comment '描述', primary key (id) );
ums_resource_category
后臺資源分類表,在細粒度進行權(quán)限控制時,可能資源會比較多,所以設(shè)計了個資源分類的概念,便于給角色分配資源。
create table ums_resource_category ( id bigint not null auto_increment, create_time datetime comment '創(chuàng)建時間', name varchar(200) comment '分類名稱', sort int(4) comment '排序', primary key (id) );
ums_role_menu_relation
后臺角色菜單關(guān)系表,多對多關(guān)系,可以給一個角色分配多個菜單。
create table ums_role_menu_relation ( id bigint not null auto_increment, role_id bigint comment '角色ID', menu_id bigint comment '菜單ID', primary key (id) );
ums_role_resource_relation
后臺角色資源關(guān)系表,多對多關(guān)系,可以給一個角色分配多個資源。
create table ums_role_resource_relation ( id bigint not null auto_increment, role_id bigint comment '角色ID', resource_id bigint comment '資源ID', primary key (id) );
結(jié)合Spring Security實現(xiàn)
實現(xiàn)動態(tài)權(quán)限是在原mall-security
模塊的基礎(chǔ)上進行改造完成的,原實現(xiàn)有不清楚的可以自行參照前置知識
中的文檔來學習。
以前的權(quán)限控制
以前的權(quán)限控制是采用Spring Security的默認機制實現(xiàn)的,下面我們以商品模塊的代碼為例來講講實現(xiàn)原理。
首先我們在需要權(quán)限的接口上使用@PreAuthorize
注解定義好需要的權(quán)限;
/** * 商品管理Controller * Created by macro on 2018/4/26. */ @Controller @Api(tags = "PmsProductController", description = "商品管理") @RequestMapping("/product") public class PmsProductController { @Autowired private PmsProductService productService; @ApiOperation("創(chuàng)建商品") @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 { return CommonResult.failed(); } } }
然后將該權(quán)限值存入到權(quán)限表中,當用戶登錄時,將其所擁有的權(quán)限查詢出來;
/** * UmsAdminService實現(xiàn)類 * Created by macro on 2018/4/26. */ @Service public class UmsAdminServiceImpl implements UmsAdminService { @Override public UserDetails loadUserByUsername(String username){ //獲取用戶信息 UmsAdmin admin = getAdminByUsername(username); if (admin != null) { List<UmsPermission> permissionList = getPermissionList(admin.getId()); return new AdminUserDetails(admin,permissionList); } throw new UsernameNotFoundException("用戶名或密碼錯誤"); } }
之后Spring Security把用戶擁有的權(quán)限值和接口上注解定義的權(quán)限值進行比對,如果包含則可以訪問,反之就不可以訪問;
但是這樣做會帶來一些問題,我們需要在每個接口上都定義好訪問該接口的權(quán)限值,而且只能挨個控制接口的權(quán)限,無法批量控制。其實每個接口都可以由它的訪問路徑唯一確定,我們可以使用基于路徑的動態(tài)權(quán)限控制來解決這些問題。
基于路徑的動態(tài)權(quán)限控制
接下來我們詳細介紹下如何使用Spring Security實現(xiàn)基于路徑的動態(tài)權(quán)限。
首先我們需要創(chuàng)建一個過濾器,用于實現(xiàn)動態(tài)權(quán)限控制,這里需要注意的是doFilter
方法,對于OPTIONS請求直接放行,否則前端調(diào)用會出現(xiàn)跨域問題。對于配置在IgnoreUrlsConfig
中的白名單路徑我也需要直接放行,所有的鑒權(quán)操作都會在super.beforeInvocation(fi)
中進行。
/** * 動態(tài)權(quán)限過濾器,用于實現(xiàn)基于路徑的動態(tài)權(quán)限過濾 * 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請求直接放行 if(request.getMethod().equals(HttpMethod.OPTIONS.toString())){ fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); return; } //白名單請求直接放行 PathMatcher pathMatcher = new AntPathMatcher(); for (String path : ignoreUrlsConfig.getUrls()) { if(pathMatcher.match(path,request.getRequestURI())){ fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); return; } } //此處會調(diào)用AccessDecisionManager中的decide方法進行鑒權(quán)操作 InterceptorStatusToken token = super.beforeInvocation(fi); try { fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { super.afterInvocation(token, null); } } @Override public void destroy() { } @Override public Class<?> getSecureObjectClass() { return FilterInvocation.class; } @Override public SecurityMetadataSource obtainSecurityMetadataSource() { return dynamicSecurityMetadataSource; } }
在DynamicSecurityFilter中調(diào)用super.beforeInvocation(fi)方法時會調(diào)用AccessDecisionManager中的decide方法用于鑒權(quán)操作,而decide方法中的configAttributes參數(shù)會通過SecurityMetadataSource中的getAttributes方法來獲取,configAttributes其實就是配置好的訪問當前接口所需要的權(quán)限,下面是簡化版的beforeInvocation源碼。
public abstract class AbstractSecurityInterceptor implements InitializingBean, ApplicationEventPublisherAware, MessageSourceAware { protected InterceptorStatusToken beforeInvocation(Object object) { //獲取元數(shù)據(jù) Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource() .getAttributes(object); Authentication authenticated = authenticateIfRequired(); //進行鑒權(quán)操作 try { this.accessDecisionManager.decide(authenticated, object, attributes); } catch (AccessDeniedException accessDeniedException) { publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, accessDeniedException)); throw accessDeniedException; } } }
知道了鑒權(quán)的原理,接下來我們需要自己實現(xiàn)SecurityMetadataSource接口的getAttributes方法,用于獲取當前訪問路徑所需資源。
/** * 動態(tài)權(quán)限數(shù)據(jù)源,用于獲取動態(tài)權(quán)限規(guī)則 * 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() { configAttributeMap = dynamicSecurityService.loadDataSource(); } public void clearDataSource() { configAttributeMap.clear(); configAttributeMap = null; } @Override public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException { if (configAttributeMap == null) this.loadDataSource(); List<ConfigAttribute> configAttributes = new ArrayList<>(); //獲取當前訪問的路徑 String url = ((FilterInvocation) o).getRequestUrl(); String path = URLUtil.getPath(url); PathMatcher pathMatcher = new AntPathMatcher(); Iterator<String> iterator = configAttributeMap.keySet().iterator(); //獲取訪問該路徑所需資源 while (iterator.hasNext()) { String pattern = iterator.next(); if (pathMatcher.match(pattern, path)) { configAttributes.add(configAttributeMap.get(pattern)); } } // 未設(shè)置操作請求權(quán)限,返回空集合 return configAttributes; } @Override public Collection<ConfigAttribute> getAllConfigAttributes() { return null; } @Override public boolean supports(Class<?> aClass) { return true; } }
由于我們的后臺資源規(guī)則被緩存在了一個Map對象之中,所以當后臺資源發(fā)生變化時,我們需要清空緩存的數(shù)據(jù),然后下次查詢時就會被重新加載進來。
這里我們需要修改UmsResourceController類,注入DynamicSecurityMetadataSource,當修改后臺資源時,需要調(diào)用clearDataSource方法來清空緩存的數(shù)據(jù)。
/** * 后臺資源管理Controller * Created by macro on 2020/2/4. */ @Controller @Api(tags = "UmsResourceController", description = "后臺資源管理") @RequestMapping("/resource") public class UmsResourceController { @Autowired private UmsResourceService resourceService; @Autowired private DynamicSecurityMetadataSource dynamicSecurityMetadataSource; @ApiOperation("添加后臺資源") @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 { return CommonResult.failed(); } } }
之后我們需要實現(xiàn)AccessDecisionManager接口來實現(xiàn)權(quán)限校驗,對于沒有配置資源的接口我們直接允許訪問,對于配置了資源的接口,我們把訪問所需資源和用戶擁有的資源進行比對,如果匹配則允許訪問。
/** * 動態(tài)權(quán)限決策管理器,用于判斷用戶是否有訪問權(quán)限 * Created by macro on 2020/2/7. */ public class DynamicAccessDecisionManager implements AccessDecisionManager { @Override public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException { // 當接口未被配置資源時直接放行 if (CollUtil.isEmpty(configAttributes)) { return; } Iterator<ConfigAttribute> iterator = configAttributes.iterator(); while (iterator.hasNext()) { ConfigAttribute configAttribute = iterator.next(); //將訪問所需資源或用戶擁有資源進行比對 String needAuthority = configAttribute.getAttribute(); for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) { if (needAuthority.trim().equals(grantedAuthority.getAuthority())) { return; } } } throw new AccessDeniedException("抱歉,您沒有訪問權(quán)限"); } @Override public boolean supports(ConfigAttribute configAttribute) { return true; } @Override public boolean supports(Class<?> aClass) { return true; } }
我們之前在DynamicSecurityMetadataSource中注入了一個DynamicSecurityService對象,它是我自定義的一個動態(tài)權(quán)限業(yè)務(wù)接口,其主要用于加載所有的后臺資源規(guī)則。
/** * 動態(tài)權(quán)限相關(guān)業(yè)務(wù)類 * Created by macro on 2020/2/7. */ public interface DynamicSecurityService { /** * 加載資源ANT通配符和資源對應MAP */ Map<String, ConfigAttribute> loadDataSource(); }
接下來我們需要修改Spring Security的配置類SecurityConfig,當有動態(tài)權(quán)限業(yè)務(wù)類時在FilterSecurityInterceptor過濾器前添加我們的動態(tài)權(quán)限過濾器。
這里在創(chuàng)建動態(tài)權(quán)限相關(guān)對象時,還使用了@ConditionalOnBean這個注解,當沒有動態(tài)權(quán)限業(yè)務(wù)類時就不會創(chuàng)建動態(tài)權(quán)限相關(guān)對象,實現(xiàn)了有動態(tài)權(quán)限控制和沒有這兩種情況的兼容。
/** * 對SpringSecurity的配置的擴展,支持自定義白名單資源路徑和查詢用戶邏輯 * 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(); //有動態(tài)權(quán)限配置時添加動態(tài)權(quán)限校驗過濾器 if(dynamicSecurityService!=null){ registry.and().addFilterBefore(dynamicSecurityFilter(), FilterSecurityInterceptor.class); } } @ConditionalOnBean(name = "dynamicSecurityService") @Bean public DynamicAccessDecisionManager dynamicAccessDecisionManager() { return new DynamicAccessDecisionManager(); } @ConditionalOnBean(name = "dynamicSecurityService") @Bean public DynamicSecurityFilter dynamicSecurityFilter() { return new DynamicSecurityFilter(); } @ConditionalOnBean(name = "dynamicSecurityService") @Bean public DynamicSecurityMetadataSource dynamicSecurityMetadataSource() { return new DynamicSecurityMetadataSource(); } }
這里還有個問題需要提下,當前端跨域訪問沒有權(quán)限的接口時,會出現(xiàn)跨域問題,只需要在沒有權(quán)限訪問的處理類RestfulAccessDeniedHandler中添加允許跨域訪問的響應頭即可。
/** * 自定義返回結(jié)果:沒有權(quán)限訪問時 * 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(); } }
當我們其他模塊需要動態(tài)權(quán)限控制時,只要創(chuàng)建一個DynamicSecurityService對象就行了,比如在mall-admin
模塊中我們啟用了動態(tài)權(quán)限功能。
/** * mall-security模塊相關(guān)配置 * 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() { //獲取登錄用戶信息 return username -> adminService.loadUserByUsername(username); } @Bean public DynamicSecurityService dynamicSecurityService() { return new DynamicSecurityService() { @Override public Map<String, ConfigAttribute> loadDataSource() { 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())); } return map; } }; } }
權(quán)限管理功能演示
具體參考:大家心心念念的權(quán)限管理功能,這次安排上了!
mall項目更新源碼地址 https://github.com/macrozheng/mall
以上就是Spring Security權(quán)限管理實現(xiàn)接口動態(tài)權(quán)限控制的詳細內(nèi)容,更多關(guān)于Spring Security接口動態(tài)權(quán)限控制的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Spring實現(xiàn)定時任務(wù)的幾種方式總結(jié)
Spring Task 是 Spring 框架提供的一種任務(wù)調(diào)度和異步處理的解決方案,可以按照約定的時間自動執(zhí)行某個代碼邏輯它可以幫助開發(fā)者在 Spring 應用中輕松地實現(xiàn)定時任務(wù)、異步任務(wù)等功能,提高應用的效率和可維護性,需要的朋友可以參考下本文2024-07-07SpringBoot整合Mybatis簡單實現(xiàn)增刪改查
這篇文章主要介紹了SpringBoot整合Mybatis簡單實現(xiàn)增刪改查,文章為圍繞主題展開詳細的內(nèi)容介紹,具有一定的參考價值,需要的小伙伴可以參考一下2022-08-08MyBatis?實現(xiàn)多對多中間表插入數(shù)據(jù)
這篇文章主要介紹了MyBatis?實現(xiàn)多對多中間表插入數(shù)據(jù),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-02-02Spring boot JPA實現(xiàn)分頁和枚舉轉(zhuǎn)換代碼示例
這篇文章主要介紹了Spring boot JPA實現(xiàn)分頁和枚舉轉(zhuǎn)換代碼示例,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-09-09一分鐘掌握Java?ElasticJob分布式定時任務(wù)
ElasticJob?是面向互聯(lián)網(wǎng)生態(tài)和海量任務(wù)的分布式調(diào)度解決方案,本文主要通過簡單的示例帶大家深入了解ElasticJob分布式定時任務(wù)的相關(guān)知識,需要的可以參考一下2023-05-05mybatis注解之@Mapper和@MapperScan的使用
這篇文章主要介紹了mybatis注解之@Mapper和@MapperScan的使用,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-10-10Quarkus集成open api接口使用swagger ui展示
這篇文章主要為大家介紹了Quarkus集成open?api接口使用swagger?ui的展示示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步2022-02-02