SpringBoot實戰電商項目mall(30k+star)地址:github.com/macrozheng/…前端
權限控管理做爲後臺管理系統中必要的功能,mall項目中結合Spring Security實現了基於路徑的動態權限控制,能夠對後臺接口訪問進行細粒度的控制,今天咱們來說下它的後端實現原理。java
學習本文須要一些Spring Security的知識,對Spring Security不太瞭解的朋友能夠看下如下文章。git
權限管理相關表已經從新設計,將原來的權限拆分紅了菜單和資源,菜單管理用於控制前端菜單的顯示和隱藏,資源管理用來控制後端接口的訪問權限。github
其中
ums_admin
、ums_role
、ums_admin_role_relation
爲原來的表,其餘均爲新增表。spring
接下來咱們將對每張表的用途作個詳細介紹。sql
後臺用戶表,定義了後臺用戶的一些基本信息。數據庫
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 '建立時間',
login_time datetime comment '最後登陸時間',
status int(1) default 1 comment '賬號啓用狀態:0->禁用;1->啓用',
primary key (id)
);
複製代碼
後臺用戶角色表,定義了後臺用戶角色的一些基本信息,經過給後臺用戶分配角色來實現菜單和資源的分配。json
create table ums_role
(
id bigint not null auto_increment,
name varchar(100) comment '名稱',
description varchar(500) comment '描述',
admin_count int comment '後臺用戶數量',
create_time datetime comment '建立時間',
status int(1) default 1 comment '啓用狀態:0->禁用;1->啓用',
sort int default 0,
primary key (id)
);
複製代碼
後臺用戶和角色關係表,多對多關係表,一個角色能夠分配給多個用戶。後端
create table ums_admin_role_relation
(
id bigint not null auto_increment,
admin_id bigint,
role_id bigint,
primary key (id)
);
複製代碼
後臺菜單表,用於控制後臺用戶能夠訪問的菜單,支持隱藏、排序和更更名稱、圖標。跨域
create table ums_menu
(
id bigint not null auto_increment,
parent_id bigint comment '父級ID',
create_time datetime comment '建立時間',
title varchar(100) comment '菜單名稱',
level int(4) comment '菜單級數',
sort int(4) comment '菜單排序',
name varchar(100) comment '前端名稱',
icon varchar(200) comment '前端圖標',
hidden int(1) comment '前端隱藏',
primary key (id)
);
複製代碼
後臺資源表,用於控制後臺用戶能夠訪問的接口,使用了Ant路徑的匹配規則,可使用通配符定義一系列接口的權限。
create table ums_resource
(
id bigint not null auto_increment,
category_id bigint comment '資源分類ID',
create_time datetime comment '建立時間',
name varchar(200) comment '資源名稱',
url varchar(200) comment '資源URL',
description varchar(500) comment '描述',
primary key (id)
);
複製代碼
後臺資源分類表,在細粒度進行權限控制時,可能資源會比較多,因此設計了個資源分類的概念,便於給角色分配資源。
create table ums_resource_category
(
id bigint not null auto_increment,
create_time datetime comment '建立時間',
name varchar(200) comment '分類名稱',
sort int(4) comment '排序',
primary key (id)
);
複製代碼
後臺角色菜單關係表,多對多關係,能夠給一個角色分配多個菜單。
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)
);
複製代碼
後臺角色資源關係表,多對多關係,能夠給一個角色分配多個資源。
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)
);
複製代碼
實現動態權限是在原
mall-security
模塊的基礎上進行改造完成的,原實現有不清楚的能夠自行參照前置知識
中的文檔來學習。
之前的權限控制是採用Spring Security的默認機制實現的,下面咱們以商品模塊的代碼爲例來說講實現原理。
@PreAuthorize
註解定義好須要的權限;/** * 商品管理Controller * Created by macro on 2018/4/26. */
@Controller
@Api(tags = "PmsProductController", description = "商品管理")
@RequestMapping("/product")
public class PmsProductController {
@Autowired
private PmsProductService productService;
@ApiOperation("建立商品")
@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();
}
}
}
複製代碼
/** * UmsAdminService實現類 * 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把用戶擁有的權限值和接口上註解定義的權限值進行比對,若是包含則能夠訪問,反之就不能夠訪問;
可是這樣作會帶來一些問題,咱們須要在每一個接口上都定義好訪問該接口的權限值,並且只能挨個控制接口的權限,沒法批量控制。其實每一個接口均可以由它的訪問路徑惟一肯定,咱們可使用基於路徑的動態權限控制來解決這些問題。
接下來咱們詳細介紹下如何使用Spring Security實現基於路徑的動態權限。
首先咱們須要建立一個過濾器,用於實現動態權限控制,這裏須要注意的是doFilter
方法,對於OPTIONS請求直接放行,不然前端調用會出現跨域問題。對於配置在IgnoreUrlsConfig
中的白名單路徑我也須要直接放行,全部的鑑權操做都會在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請求直接放行
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;
}
}
//此處會調用AccessDecisionManager中的decide方法進行鑑權操做
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中調用super.beforeInvocation(fi)方法時會調用AccessDecisionManager中的decide方法用於鑑權操做,而decide方法中的configAttributes參數會經過SecurityMetadataSource中的getAttributes方法來獲取,configAttributes其實就是配置好的訪問當前接口所須要的權限,下面是簡化版的beforeInvocation源碼。
public abstract class AbstractSecurityInterceptor implements InitializingBean, ApplicationEventPublisherAware, MessageSourceAware {
protected InterceptorStatusToken beforeInvocation(Object object) {
//獲取元數據
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
.getAttributes(object);
Authentication authenticated = authenticateIfRequired();
//進行鑑權操做
try {
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException accessDeniedException) {
publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
accessDeniedException));
throw accessDeniedException;
}
}
}
複製代碼
知道了鑑權的原理,接下來咱們須要本身實現SecurityMetadataSource接口的getAttributes方法,用於獲取當前訪問路徑所需資源。
/** * 動態權限數據源,用於獲取動態權限規則 * 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));
}
}
// 未設置操做請求權限,返回空集合
return configAttributes;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
複製代碼
因爲咱們的後臺資源規則被緩存在了一個Map對象之中,因此當後臺資源發生變化時,咱們須要清空緩存的數據,而後下次查詢時就會被從新加載進來。這裏咱們須要修改UmsResourceController類,注入DynamicSecurityMetadataSource,當修改後臺資源時,須要調用clearDataSource方法來清空緩存的數據。
/** * 後臺資源管理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();
}
}
}
複製代碼
以後咱們須要實現AccessDecisionManager接口來實現權限校驗,對於沒有配置資源的接口咱們直接容許訪問,對於配置了資源的接口,咱們把訪問所需資源和用戶擁有的資源進行比對,若是匹配則容許訪問。
/** * 動態權限決策管理器,用於判斷用戶是否有訪問權限 * 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("抱歉,您沒有訪問權限");
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
複製代碼
咱們以前在DynamicSecurityMetadataSource中注入了一個DynamicSecurityService對象,它是我自定義的一個動態權限業務接口,其主要用於加載全部的後臺資源規則。
/** * 動態權限相關業務類 * Created by macro on 2020/2/7. */
public interface DynamicSecurityService {
/** * 加載資源ANT通配符和資源對應MAP */
Map<String, ConfigAttribute> loadDataSource();
}
複製代碼
接下來咱們須要修改Spring Security的配置類SecurityConfig,當有動態權限業務類時在FilterSecurityInterceptor過濾器前添加咱們的動態權限過濾器。這裏在建立動態權限相關對象時,還使用了@ConditionalOnBean這個註解,當沒有動態權限業務類時就不會建立動態權限相關對象,實現了有動態權限控制和沒有這兩種狀況的兼容。
/** * 對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();
//有動態權限配置時添加動態權限校驗過濾器
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();
}
}
複製代碼
這裏還有個問題須要提下,當前端跨域訪問沒有權限的接口時,會出現跨域問題,只須要在沒有權限訪問的處理類RestfulAccessDeniedHandler中添加容許跨域訪問的響應頭便可。
/** * 自定義返回結果:沒有權限訪問時 * 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();
}
}
複製代碼
當咱們其餘模塊須要動態權限控制時,只要建立一個DynamicSecurityService對象就好了,好比在mall-admin
模塊中咱們啓用了動態權限功能。
/** * mall-security模塊相關配置 * 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;
}
};
}
}
複製代碼
mall項目全套學習教程連載中,關注公衆號第一時間獲取。