SpringSecurity實現(xiàn)RBAC權限管理
上一篇文章 Spring Boot 實現(xiàn) JWT 認證,介紹了 Spring Boot 實現(xiàn) JWT 認證的流程,本文將關注架構安全性的另一個重要概念——授權,也就是權限控制。
RBAC 模型
權限控制有不同的模型,常用的一種是 RBAC。RBAC 是基于角色的訪問控制(Role-Based Access Control)的縮寫。
簡單來講,RBAC 模型大致結構為:
用戶(User)-> 角色(Role)-> 權限(Permission)-> 資源(Resource)
用戶擁有角色,角色被賦予權限,權限關聯(lián)資源。同一個用戶可能擁有多個角色,同一個角色也可能被賦予多個權限。訪問資源需要一種或多種權限。用戶訪問資源時,對比用戶擁有的權限和資源需要的權限。
Spring Security 支持 RBAC 模型,并做了一些簡化,將角色和權限合并為 Authority。在資源端,角色是 Authority,隸屬角色的權限也是 Authority,檢查權限就是在需要的權限和用戶擁有的 Authority 之間做對比。
具體實現(xiàn)上,Spring Security 的安全上下文保存了用戶信息(UserDetails),用戶信息中包含了用戶擁有的權限(GrantedAuthority)。在 Spring Security 體系中,UserDetailsService 接口的 loadUserByUsername 方法用于從數(shù)據(jù)庫獲取 UserDetails 信息,也包括用戶擁有的權限信息。
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
// 省略其他方法
}
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
依托 GrantedAuthority 抽象,Spirng Security 的權限控制分為兩部分:
- 用戶權限管理,由
UserDetailsService接口的loadUserByUsername方法實現(xiàn)。獲取 UserDetails 時,實現(xiàn) getAuthorities() 方法獲取權限。至于權限從何而來,自己實現(xiàn)。通常存儲在數(shù)據(jù)庫中。 - 資源權限控制,為資源分配需要的權限。
訪問資源時,Spring Security 會調(diào)用 AuthorizationManager 接口的 check 方法檢查權限,對比需要的權限和用戶擁有的權限。
實現(xiàn)用戶權限管理
用戶權限管理,實現(xiàn)權限-角色-用戶的層級結構。通常是關系型數(shù)據(jù)的多對多關系表。
class User {
private Long id;
private String username;
private String password;
private Set<Role> roles;
}
class Role {
private Long id;
private String name;
private Set<Permission> permissions;
}
class Permission {
private Long id;
private String code;
}
總共需要三張實體表,外加兩張關系表。
實現(xiàn) UserDetailsService 接口的 loadUserByUsername 方法。
@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.selectByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("User not found");
}
user.setAuthorities(getAuthorities());
return user;
}
private Set<SimpleGrantedAuthority> getAuthorities(User user) {
Set<String> authorities = new HashSet<>();
for (Role role : user.getRoles()) {
authorities.add("ROLE_" + role.getCode());
for (Permission permission : role.getPermissions()) {
authorities.add(permission.getCode());
}
}
return authorities.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toSet());
}
}
Spring Security 處理角色時會自動加 ROLE_ 前綴,在處理 Authority 時不會,所以定義角色時,角色名不加 ROLE_ 前綴。
實現(xiàn)資源權限控制
這部分講述如何為資源指定需要的權限。
先理解什么是資源。資源可以有不同的粒度,比如方法、類、模塊、系統(tǒng)等。但對后端應用而言,最直觀的劃分是 API,一個 API 就是一個資源,訪問 API 需要權限。這也是 Spring Security 的默認粒度。
API 與 Controller 的方法存在一一對應的關系,因此,為 API 指定權限,也包括為 Controller 的方法指定權限。Spring Security 可以直接為 API 指定權限,也可以基于注解為 Controller 的方法指定權限。
直接為 API 指定權限
直接為 API 指定權限,通過在配置類定義 SecurityFilterChain 來指定。
@EnableWebSecurity // 用于支持 SecurityFilterChain
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> {
auth.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/users/edit").hasAuthority("user:edit")
.anyRequest().authenticated();
})
.build();
}
}
對以 /api/auth 為前綴的 API 和以 /api/public 為前綴的 API,不進行權限檢查。對以 /api/admin 為前綴的 API,需要用戶具有 ADMIN 角色。對 /api/users/edit API,需要用戶具有 user:edit 權限。其他 API 需要認證,不需要額外權限。
在 SecurityFilterChain 中,通常進行比較粗粒度的權限控制,比如以前綴來指定 API 權限。如果進行細粒度的權限控制,如果 API 很多,配置會非常繁瑣,也不便于維護。此時,可以基于注解來指定 API 權限。
基于注解指定 API 權限
在 Controller 的方法上添加注解,可以間接地為 API 指定權限。
有多種注解支持在方法上控制權限,大致可以分為三類:
- Spring Security 內(nèi)置注解,@PreAuthorize、@PostAuthorize、@PreFilter、@PostFilter
- 基于 JSR-250 規(guī)范的注解,@RolesAllowed、@PermitAll、@DenyAll
- Spring Security 的遺留注解 @Secured,文檔介紹這是一個 Service 層注解。
要使用這些注解,需要在配置中開啟支持。
@EnableMethodSecurity(prePostEnabled = true, jsr250Enabled = true, securedEnabled = true)
@Configuration
public class SecurityConfig {
// 省略其他配置
}
三類注解中,Spring Security 內(nèi)置注解 prePost 的功能最強大,提供了基于表達式的權限定義和數(shù)據(jù)過濾的功能,推薦使用。
- PreAuthorize:指定方法需要的權限
- PostAuthorize:權限 + 數(shù)據(jù)過濾,不滿足條件時拋出 AccessDeniedException 異常
- PreFilter:授權 + 列表過濾,不滿足條件的數(shù)據(jù)會被過濾,不會拋出異常
- PostFilter:授權 + 列表過濾,不滿足條件的數(shù)據(jù)會被過濾,不會拋出異常
使用 PreFilter 和 PostFilter 時,需要考慮數(shù)據(jù)過濾的性能問題。如果數(shù)據(jù)量很大,過濾會非常耗時,不如直接在 SQL 中限制過濾條件。
資源權限控制的原理
通過 SecurityFilterChain 配置的 API 權限,在 AuthorizationFilter 中檢查。內(nèi)部存在 AuthorizationFilter -> RequestMatcherDelegatingAuthorizationManager 的調(diào)用鏈。RequestMatcherDelegatingAuthorizationManager 類如其名,會根據(jù) API 的路徑來選擇實際的 AuthorizationManager。
如果在 SecurityFilterChain 中沒有指定 API 權限,只是開啟了 authenticated 認證檢查,則根據(jù)路徑匹配到 AuthenticatedAuthorizationManager,調(diào)用鏈為 AuthenticatedAuthorizationManager -> AuthenticationTrustResolverImpl。AuthenticationTrustResolverImpl 只會檢查 Authentication 是否存在用戶 UserDetails,用戶狀態(tài)是否存在,不涉及權限檢查的邏輯。
如果在 SecurityFilterChain 中用 hasRole hasAuthority 為 API 指定了權限,路徑匹配到 AuthoritiesAuthorizationManager。這又構成了一條新的調(diào)用鏈 AuthorityAuthorizationManager -> AuthoritiesAuthorizationManager。最終,在 AuthoritiesAuthorizationManager 中,會調(diào)用 isAuthorized 方法,遍歷所有注冊的 GrantedAuthority,檢查用戶是否擁有權限。
// AuthoritiesAuthorizationManager
private boolean isAuthorized(Authentication authentication, Collection<String> authorities) {
for (GrantedAuthority grantedAuthority : getGrantedAuthorities(authentication)) {
if (authorities.contains(grantedAuthority.getAuthority())) {
return true;
}
}
return false;
}
對于方法注解配置的權限,無法直接在 AuthorizationFilter 中處理。在 Filter-Servlet 洋蔥圈中,F(xiàn)ilter 只能從 Request 中獲取信息,無法獲取具體處理請求的方法信息。只有到了 Servlet 中,才有可能接觸到方法信息。
Spring MVC 使用 DispatcherServlet 作為 Controller 和外部 Servlet 容器的橋梁,請求穿過重重 Filter 后,到達 DispatcherServlet 的剎那,才真正進入 Spring MVC 的世界。DispatcherServlet 內(nèi)部,也有一個類似的洋蔥圈,外層是重重疊疊的 Interceptor 攔截器,最內(nèi)層才是 Controller 方法。

在 Dispatcher 內(nèi)部,能獲取到 Controller 方法信息。因此,注解式的方法權限控制,都是用攔截器 Interceptor 實現(xiàn)。
對于注解 @PreAuthorize("hasAuthority('user:edit')"),調(diào)用鏈大致如下:
AuthorizationManagerBeforeMethodInterceptor -> PreAuthorizeAuthorizationManager -> ExpressionUtils
AuthorizationManagerBeforeMethodInterceptor 是前置攔截器,在 Controller 方法執(zhí)行前,調(diào)用 AuthorizationManager 檢查權限。如果是 @PostAuthorize 注解,則會使用 AuthorizationManagerAfterMethodInterceptor 后置攔截器。
PreAuthorizeAuthorizationManager 是具體的權限檢查邏輯,與注解 @PreAuthorize 一一對應。調(diào)用 ExpressionUtils 的 evaluate 方法,解析 SpEL 表達式 hasAuthority('user:edit'),檢查用戶是否擁有權限。如果是 @PostAuthorize 注解,則會使用 PostAuthorizeAuthorizationManager。
解析表達式時,會使用 MethodSecurityEvaluationContext 提供的 Root 對象。最終執(zhí)行 hasAuthority() 方法的,是 MethodSecurityExpressionRoot 類。
// MethodSecurityExpressionRoot
private boolean hasAnyAuthorityName(String prefix, String... roles) {
Set<String> roleSet = getAuthoritySet();
for (String role : roles) {
String defaultedRole = getRoleWithDefaultPrefix(prefix, role);
if (roleSet.contains(defaultedRole)) {
return true;
}
}
return false;
}
getAuthoritySet() 方法會從 SecurityContext 中獲取當前用戶的權限,roles 參數(shù)則代表注解中指定的權限。
可以看到,不管實現(xiàn)方式如何,最終的權限檢查邏輯,仍然是對比用戶權限和注解權限。掌握這一點,在對 Spirng Security 進行擴展時就可以靈活變通。
自定義注解控制方法權限
基于注解的權限控制,除了 Spring Security 提供的注解,還可以使用自定義注解。自定義注解的優(yōu)點在于實現(xiàn)權限控制的同時,還可以實現(xiàn)自動注冊權限的功能。
@RequirePermission(code = "user:get", name = "獲取用戶信息")
@GetMapping("/{id}")
public User getUserById(@PathVariable Long id) {
return userRepository.selectByPrimaryKey(id);
}
- 應用啟動時,會自動注冊 user:get 權限。
- 調(diào)用 getUserById 方法時,也會像 @PreAuthorize 一樣,檢查用戶是否擁有 user:get 權限。
要實現(xiàn)上述功能,首先需要一個自定義注解,比如 @RequirePermission。然后,基于這個注解實現(xiàn)如下功能:
- 應用啟動時,掃描注解,注冊權限。
- 調(diào)用方法時,檢查權限。
基于注解自動注冊權限
在應用啟動時,掃描所有被 @RequirePermission 注解的方法,注冊權限。將方法權限限制在 Controller 層是一個比較合適的粒度。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequirePermission {
String code();
String name() default "";
String description() default "";
}
@Component
@RequiredArgsConstructor
public class PermissionRegistrar implements ApplicationListener<ContextRefreshedEvent> {
private final PermissionService permissionService;
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
Map<String, Object> beans = event.getApplicationContext().getBeansWithAnnotation(Controller.class);
for (Object bean : beans.values()) {
registerPermissionsForBean(bean);
}
}
private void registerPermissionsForBean(Object bean) {
// 代理對象無法獲取 method 注解,需要用原對象
Class<?> clazz = AopUtils.getTargetClass(bean);
for (Method method : clazz.getDeclaredMethods()) {
RequirePermission annotation = method.getAnnotation(RequirePermission.class);
if (annotation != null) {
registerPermission(annotation);
}
}
RequirePermission requirePermission = clazz.getDeclaredAnnotation(RequirePermission.class);
if (requirePermission != null) {
registerPermission(requirePermission);
}
}
private void registerPermission(RequirePermission requirePermission) {
String code = requirePermission.code();
String name = StringUtils.defaultIfBlank(requirePermission.name(), code);
String description = StringUtils.defaultIfBlank(requirePermission.description(), name);
permissionService.createPermissionIfNotExists(code, name, description);
}
}
AopUtils.getTargetClass 用于獲取代理對象的原對象。因為無法直接從代理對象獲取方法注解。此外,可以用 CommandLineRunner 結合 ApplicationContext 來替換 ApplicationListener。性能方面,可以先掃描,然后一次性注冊,合并數(shù)據(jù)庫寫操作。
基于自定義注解檢查權限
要為 @RequirePermission 實現(xiàn)類似 @PreAuthorize 的功能,最簡單的辦法是基于 AOP 實現(xiàn),用切面來處理。優(yōu)點是不會與框架耦合,只要有 Spring Boot 就行。缺點是侵入性太強,無法直接使用 Spring Security 提供的功能。
這里介紹兩種用元注解為自定義注解添加權限檢查功能的方法。
所謂元注解(meta-annotation),就是修飾注解的注解。比如想實現(xiàn)一個限制 ADMIN 角色的注解,可以這么寫:
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('ADMIN')")
public @interface IsAdmin {}
@PreAuthorize 是元注解,@IsAdmin 就是被修飾的注解,在 @PreAuthorize 修飾下,@IsAdmin 注解就具有了 @PreAuthorize 的功能。在方法上使用 @IsAdmin 注解,就可以實現(xiàn)權限檢查。
@GetMapping("/admin")
@IsAdmin
public String admin() {
return "admin";
}
@IsAdmin 的功能比較簡單,權限固定,如果要想實現(xiàn) @IsUser 的功能,還得另起爐灶,提供一個新的注解。@RequirePermission 則不同,權限不固定,由屬性值 code 決定。由于 Java 語言本身不支持在元注解獲取被修飾注解的屬性值,@PreAuthorize 無法直接獲取 code 的值。想要 @RequirePermission 能檢查權限,還得另想辦法,解決元注解無法獲取被修飾注解屬性值的問題。

使用擴展 SpEL 表達式
一個簡單的解決方案是使用 Spring Security 6.3 擴展的 SpEL 表達式,通過 '[code]' 獲取注解的屬性值。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAuthority('[code]')")
public @interface RequirePermission {
String code();
String name() default "";
String description() default "";
}
執(zhí)行 hasAuthority('[code]') 表達式時,能自動獲取 RequirePermission.code() 的值,填充進 [code] 占位符中。
但要啟用這種使用大括號的表達式,需要向 Spring 容器注冊 PrePostTemplateDefaults 類型的 Bean。
@EnableMethodSecurity(prePostEnabled = true)
@Configuration
public class SecurityConfig {
@Bean
static PrePostTemplateDefaults prePostTemplateDefaults() {
return new PrePostTemplateDefaults();
}
}
自己擴展 SpEL 表達式
在 Spring Security 6.3 之前,不支持大括號 [code] 獲取注解屬性的寫法,無法直接將注解的屬性傳遞給元注解。我們只能自己擴展 SpEL 表達式,繞點遠路,先利用反射獲取注解的屬性值,再將屬性值傳給元注解。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAuthority(@permissionExpressionEvaluator.getPermission(#root.method))")
public @interface RequirePermission {
String code();
String name() default "";
String description() default "";
}
在 @PreAuthorize 中,仍然使用了 hasAuthority 表達式,@permissionExpressionEvaluator.getPermission() 表示調(diào)用名為 permissionExpressionEvaluator Bean 的 getPermission 方法來獲取權限,#root.method 表示獲取被注解修飾方法的反射對象 Method。
PermissionExpressionEvaluator 是一個自定義的類,根據(jù)傳入的反射對象 Method,利用反射獲取方法上的注解信息,從而得到方法需要的權限,再從安全上下文 Authentication 中獲取分配給當前用戶的權限,兩相比較,實現(xiàn)權限檢查。
@Component
public class PermissionExpressionEvaluator {
private final Map<String, String> permissionCache = new ConcurrentHashMap<>(64);
public String getPermission(Method method) {
return permissionCache.computeIfAbsent(keyOf(method), k -> getPermissionCode(method));
}
private String getPermissionCode(Method method) {
RequirePermission annotation = method.getAnnotation(RequirePermission.class);
if (annotation != null) {
return annotation.code();
}
// 方法注解優(yōu)先于類注解
annotation = method.getDeclaringClass().getAnnotation(RequirePermission.class);
if (annotation != null) {
return annotation.code();
}
return "";
}
private String keyOf(Method method) {
Class<?> clazz = method.getDeclaringClass();
String methodName = clazz.getName() + "." + method.getName();
StringJoiner sj = new StringJoiner(",", methodName + "(", ")");
for (Class<?> parameterType : method.getParameterTypes()) {
sj.add(parameterType.getSimpleName());
}
return sj.toString();
}
public void clear() {
this.permissionCache.clear();
}
}
PermissionExpressionEvaluator 實現(xiàn) getPermission 方法時,按照先方法注解再類注解的順序獲取權限,保證方法上的注解優(yōu)先于類上的注解。這一點與 @PreAuthorize 的行為一致。同時在類和方法使用 @PreAuthorize 注解時,方法注解會覆蓋類注解。此外,為了提高性能,PermissionExpressionEvaluator 還使用了 Map 來緩存方法的權限,避免每次都要靠反射獲取。
上述實現(xiàn)需要使用 #root.method 獲取方法反射。遺憾的是,Spring Security 的 SpEL 表達式不支持這種用法。Spring 體系大量使用 SpEL 表達式,但不同的模塊會提供不同的 Context。Context 不同,表達式可以獲取的信息也不同。比如 Spring Security 的 Context 中,可以使用 hasRole、hasAuthority 等方法,而 Web 模塊的 Context 中,可以使用 #request 獲取 HttpServletRequest 對象。Spring Security 解析注解的 SpEL 使用的 Context 為 MethodSecurityEvaluationContext,內(nèi)部有一個 MethodSecurityExpressionRoot 類型的屬性。Root 決定了 SpEL 表達式可以獲取的信息。用表達式可以直接調(diào)用 Root 的方法,比如 hasRole、hasAuthority;用 #root.property 表達式可以訪問 property 屬性,比如 #root.method,就需要 Root 提供了名為 method 的屬性。
現(xiàn)在的 MethodSecurityExpressionRoot 并沒有 method 屬性,但可以通過擴展 Root 來實現(xiàn)。我們可以繼承 MethodSecurityExpressionRoot,添加 method 屬性,再將新的 Root 傳遞給 Context。
具體代碼如下:
public class CustomMethodSecurityExpressionRoot extends SecurityExpressionRoot implements MethodSecurityExpressionOperations {
private Object filterObject;
private Object returnObject;
private Object target;
/**
* 僅僅添加了 method 屬性,其他都與 MethodSecurityExpressionRoot 保持一致
*/
@Getter
@Setter
private Method method;
public CustomMethodSecurityExpressionRoot(Authentication a) {
super(a);
}
public CustomMethodSecurityExpressionRoot(Supplier<Authentication> authentication) {
super(authentication);
}
@Override
public void setFilterObject(Object filterObject) {
this.filterObject = filterObject;
}
@Override
public Object getFilterObject() {
return this.filterObject;
}
@Override
public void setReturnObject(Object returnObject) {
this.returnObject = returnObject;
}
@Override
public Object getReturnObject() {
return this.returnObject;
}
void setThis(Object target) {
this.target = target;
}
@Override
public Object getThis() {
return this.target;
}
}
/**
* 重寫 MethodSecurityExpressionHandler,用于設置自定義 Root 對象
*/
public class CustomMethodSecurityExpressionHandler extends DefaultMethodSecurityExpressionHandler {
/**
* 重寫以避免調(diào)用父類的 createSecurityExpressionRoot 方法
*/
@Override
public EvaluationContext createEvaluationContext(Supplier<Authentication> authentication, MethodInvocation mi) {
/*
createSecurityExpressionRoot 是 private 方法,由 invokespecial 指令調(diào)用,采用解析方式來確定方法版本。
解析會直接根據(jù)外觀類型來確定方法,因此如果不重寫 createEvaluationContext 方法,
就會直接調(diào)用 DefaultMethodSecurityExpressionHandler 的 createSecurityExpressionRoot 方法。
*/
MethodSecurityExpressionOperations root = createSecurityExpressionRoot(authentication, mi);
// 為了解決 MethodSecurityEvaluationContext 不可見問題
CustomMethodSecurityEvaluationContext ctx = new CustomMethodSecurityEvaluationContext(root, mi,
getParameterNameDiscoverer());
ctx.setBeanResolver(getBeanResolver());
return ctx;
}
@Override
protected MethodSecurityExpressionOperations createSecurityExpressionRoot(
Authentication authentication, MethodInvocation invocation) {
return createSecurityExpressionRoot(() -> authentication, invocation);
}
private MethodSecurityExpressionOperations createSecurityExpressionRoot(Supplier<Authentication> authentication,
MethodInvocation invocation) {
CustomMethodSecurityExpressionRoot root = new CustomMethodSecurityExpressionRoot(authentication);
root.setThis(invocation.getThis());
root.setPermissionEvaluator(getPermissionEvaluator());
root.setTrustResolver(getTrustResolver());
root.setRoleHierarchy(getRoleHierarchy());
root.setDefaultRolePrefix(getDefaultRolePrefix());
root.setMethod(invocation.getMethod());
return root;
}
}
/**
* MethodSecurityEvaluationContext 是 default 可見,為了在 CustomMethodSecurityExpressionHandler 中使用,復制過來
*/
public class CustomMethodSecurityEvaluationContext extends MethodBasedEvaluationContext {
public CustomMethodSecurityEvaluationContext(MethodSecurityExpressionOperations root, MethodInvocation mi,
ParameterNameDiscoverer parameterNameDiscoverer) {
super(root, getSpecificMethod(mi), mi.getArguments(), parameterNameDiscoverer);
}
private static Method getSpecificMethod(MethodInvocation mi) {
return AopUtils.getMostSpecificMethod(mi.getMethod(), AopProxyUtils.ultimateTargetClass(mi.getThis()));
}
}
重寫的類型比較多,主要邏輯如下:
- 由于 MethodSecurityExpressionRoot 是 private 可見,無法直接繼承,所以 CustomMethodSecurityExpressionRoot 復制了 MethodSecurityExpressionRoot 的代碼,添加了 method 屬性。
- 為了使用自定義的 Root,還需要重寫 DefaultMethodSecurityExpressionHandler 的 createSecurityExpressionRoot 方法,返回自定義的 Root。
- 僅僅重寫 createSecurityExpressionRoot 方法還不夠,由于框架直接調(diào)用 DefaultMethodSecurityExpressionHandler 的 createEvaluationContext 方法來獲取 Context,而這個方法內(nèi)部調(diào)用了 private 版的 createSecurityExpressionRoot,為了避免解析到父類,還需要重寫 createEvaluationContext 方法。
- 重寫 createEvaluationContext 方法時,由于默認的 MethodSecurityEvaluationContext 對外不可見,所以又復制了一個一模一樣的 CustomMethodSecurityEvaluationContext 類。
總結下來,關鍵是為 Root 添加 method 屬性,以及使 MethodSecurityExpressionHandler 使用自定義的 Root,其他都是為了解決可見性問題而復制了一堆代碼。
現(xiàn)在,我們還需要將一切組合起來,就可以使用 @RequirePermission 注解來實現(xiàn)權限控制了。
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
@Configuration
class SecurityConfig {
@Bean
MethodSecurityExpressionHandler expressionHandler() {
// 用自定義的 Handler 替換默認的 DefaultMethodSecurityExpressionHandler
return new CustomMethodSecurityExpressionHandler();
}
}
此時,@PreAuthorize 等 Method Security 注解的表達式中,就可以使用 #root.method 就可以獲取到方法反射對象。@RequirePermission 注解也就能正常工作了。
如果想要擴展其他功能,也可以采用類似的思路:
- 自定義一個 Root,擴充屬性或者添加方法。
- 自定義 Handler,使用自定義的 Root。
- 解決各種可見性問題。
總結
本文介紹了在 Spring Boot 中用 Spring Security 實現(xiàn) RBAC 權限管理的方案,并提供了自定義注解來簡化權限管理的思路。想要實現(xiàn)自定義注解,需要解決元注解無法獲取被修飾注解屬性值的問題。Spring Security 6.3 之后可以直接使用大括號表達式獲取注解屬性,而之前的版本需要自己擴展 SpEL 表達式來實現(xiàn)。
參考文章
[1] Method Security :: Spring Security 文檔
到此這篇關于SpringSecurity實現(xiàn)RBAC權限管理的文章就介紹到這了,更多相關SpringSecurity RBAC權限內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Java8 中使用Stream 讓List 轉 Map使用問題小結
這篇文章主要介紹了Java8 中使用Stream 讓List 轉 Map使用總結,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-06-06
MyBatis insert語句返回主鍵和selectKey標簽方式
這篇文章主要介紹了MyBatis insert語句返回主鍵和selectKey標簽方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-09-09
Java Web學習教程之Hibernate And MyBatis的理解
這篇文章主要給大家介紹了關于Java Web學習教程之Hibernate And MyBatis的相關資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們來一起學習學習吧。2018-04-04

