Spring Security 動(dòng)態(tài)權(quán)限與RBAC模型實(shí)戰(zhàn)
權(quán)限控制是我們幾乎每個(gè)項(xiàng)目都要面對(duì)的問(wèn)題, 而Spring Security作為Spring生態(tài)中的安全框架, 提供了強(qiáng)大的支持. 但很多同學(xué)在使用時(shí)會(huì)遇到一些困惑, 特別是如何實(shí)現(xiàn)動(dòng)態(tài)權(quán)限控制, 今天我們就來(lái)詳細(xì)講一講.
一、權(quán)限控制概念
1) 什么是權(quán)限控制?
簡(jiǎn)單來(lái)說(shuō), 權(quán)限控制就是決定"誰(shuí)能在什么情況下對(duì)什么資源做什么操作". 比如:
- 普通用戶只能查看自己的訂單
- 管理員可以查看所有訂單
- 只有財(cái)務(wù)人員才能導(dǎo)出財(cái)務(wù)報(bào)表
2) 常見(jiàn)的權(quán)限模型
ACL, ACL是最直接的權(quán)限模型, 它直接維護(hù)了"主體-資源-操作"的對(duì)應(yīng)關(guān)系. 比如:
用戶 | 資源 | 操作 |
張三 | /order | 查看 |
李四 | /report | 導(dǎo)出 |
這種模型簡(jiǎn)單直接, 但當(dāng)用戶和資源數(shù)量增多時(shí), 維護(hù)成本會(huì)很高.
RBAC(Role-Based Access Control)引入了"角色"這一中間層, 是目前最流行的權(quán)限模型. 它的核心思想是:
- 用戶關(guān)聯(lián)角色
- 角色關(guān)聯(lián)權(quán)限
- 權(quán)限決定能否訪問(wèn)資源
下面是ACL和RBAC的對(duì)比圖:
圖片
二、Spring Security中的RBAC實(shí)現(xiàn)
2.1 我們先看一個(gè)簡(jiǎn)單一點(diǎn)的實(shí)現(xiàn), 是基于配置的權(quán)限控制
1) 先來(lái)添加數(shù)據(jù)庫(kù)表:
--用戶表
CREATE TABLE user (
id BIGINT PRIMARY KEY,
username VARCHAR(50) NOT NULL,
password VARCHAR(100) NOT NULL
);
--角色表
CREATE TABLE role (
id BIGINT PRIMARY KEY,
name VARCHAR(50) NOT NULL
);
--用戶-角色關(guān)聯(lián)表
CREATE TABLE user_role (
user_id BIGINT,
role_id BIGINT,
PRIMARY KEY (user_id, role_id)
);
--權(quán)限表
CREATE TABLE permission (
id BIGINT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
url VARCHAR(255) NOT NULL,
description VARCHAR(200)
);
--角色-權(quán)限關(guān)聯(lián)表
CREATE TABLE role_permission (
role_id BIGINT,
permission_id BIGINT,
PRIMARY KEY (role_id, permission_id)
);2) 接著是配置Spring Security:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/user/**").hasAnyRole("USER", "ADMIN")
.antMatchers("/public/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.logout().permitAll();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
}這種方式的優(yōu)點(diǎn)是簡(jiǎn)單直接, 但缺點(diǎn)是權(quán)限規(guī)則硬編碼在配置類中, 不夠靈活.
2.2 實(shí)現(xiàn)動(dòng)態(tài)的權(quán)限控制
要實(shí)現(xiàn)真正的動(dòng)態(tài)權(quán)限(從數(shù)據(jù)庫(kù)加載權(quán)限規(guī)則), 我們需要自定義權(quán)限決策邏輯. 下面是實(shí)現(xiàn)步驟:
1) 自定義FilterInvocationSecurityMetadataSource:
@Component
public class DynamicSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
@Autowired
private PermissionService permissionService;
private Map<String, ConfigAttribute> permissionMap = null;
/**
* 加載所有權(quán)限規(guī)則
*/
public void loadDataSource() {
permissionMap = permissionService.getAllPermissionMap();
}
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
if (permissionMap == null) {
this.loadDataSource();
}
HttpServletRequest request = ((FilterInvocation) object).getRequest();
String url = request.getRequestURI();
String method = request.getMethod();
//去掉URL中的參數(shù)部分
String path = url.split("\\?")[0];
//嘗試直接匹配URL
ConfigAttribute configAttribute = permissionMap.get(path + ":" + method);
if (configAttribute != null) {
return Collections.singletonList(configAttribute);
}
//嘗試通配符匹配
for (String pattern : permissionMap.keySet()) {
if (pathMatcher.match(pattern.split(":")[0], path)
&& method.equalsIgnoreCase(pattern.split(":")[1])) {
return Collections.singletonList(permissionMap.get(pattern));
}
}
// 如果沒(méi)有匹配到, 返回一個(gè)標(biāo)記, 表示需要登錄但不需要特定權(quán)限
return SecurityConfig.createList("ROLE_LOGIN");
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}2) 自定義AccessDecisionManager:
@Component
public class DynamicAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
//如果沒(méi)有權(quán)限規(guī)則,直接放行
if (CollectionUtils.isEmpty(configAttributes)) {
return;
}
//檢查每個(gè)需要的權(quán)限
for (ConfigAttribute configAttribute : configAttributes) {
String needRole = configAttribute.getAttribute();
//只需要登錄的情況
if ("ROLE_LOGIN".equals(needRole)) {
if (authentication instanceof AnonymousAuthenticationToken) {
throw new AccessDeniedException("尚未登錄,請(qǐng)登錄");
} else {
return;
}
}
//檢查用戶是否有該角色
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities) {
if (authority.getAuthority().equals(needRole)) {
return;
}
}
}
throw new AccessDeniedException("抱歉,您沒(méi)有訪問(wèn)權(quán)限");
}
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}3) 更新Security配置:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private DynamicSecurityMetadataSource dynamicSecurityMetadataSource;
@Autowired
private DynamicAccessDecisionManager dynamicAccessDecisionManager;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
object.setSecurityMetadataSource(dynamicSecurityMetadataSource);
object.setAccessDecisionManager(dynamicAccessDecisionManager);
return object;
}
})
.and()
.formLogin()
.and()
.logout().permitAll();
}
// 其他配置...
}































