欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

SpringSecurity實現(xiàn)RBAC權限管理

 更新時間:2025年08月13日 09:43:50   作者:未必完美  
本文介紹了在Spring Boot中用 Spring Security實現(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);
}
  1. 應用啟動時,會自動注冊 user:get 權限。
  2. 調(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 中,可以使用 hasRolehasAuthority 等方法,而 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 注解也就能正常工作了。

如果想要擴展其他功能,也可以采用類似的思路:

  1. 自定義一個 Root,擴充屬性或者添加方法。
  2. 自定義 Handler,使用自定義的 Root。
  3. 解決各種可見性問題。

總結

本文介紹了在 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使用問題小結

    這篇文章主要介紹了Java8 中使用Stream 讓List 轉 Map使用總結,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2021-06-06
  • Java創(chuàng)建student類詳細代碼例子

    Java創(chuàng)建student類詳細代碼例子

    這篇文章主要給大家介紹了關于Java創(chuàng)建student類的相關資料,學生類(Student)是一種面向對象的編程概念,其主要用于描述學生的屬性和行為,文中通過代碼介紹的非常詳細,需要的朋友可以參考下
    2023-11-11
  • Java JDBC連接數(shù)據(jù)庫常見操作總結

    Java JDBC連接數(shù)據(jù)庫常見操作總結

    這篇文章主要介紹了Java JDBC連接數(shù)據(jù)庫常見操作,結合實例形式總結分析了java基于jdbc連接mysql、Oracle數(shù)據(jù)庫及連接池相關操作技巧,需要的朋友可以參考下
    2019-03-03
  • Java Socket編程詳解及示例代碼

    Java Socket編程詳解及示例代碼

    本文主要講解Java Socket編程,這里整理了詳細的技術資料及簡單的示例代碼幫助大家學習參考,有需要的小伙伴可以參考下本文內(nèi)容
    2016-09-09
  • MyBatis insert語句返回主鍵和selectKey標簽方式

    MyBatis insert語句返回主鍵和selectKey標簽方式

    這篇文章主要介紹了MyBatis insert語句返回主鍵和selectKey標簽方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2021-09-09
  • Spring 父類變量注入失敗的解決

    Spring 父類變量注入失敗的解決

    這篇文章主要介紹了Spring 父類變量注入失敗的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2021-09-09
  • Jmeter測試時遇到的各種亂碼問題及解決

    Jmeter測試時遇到的各種亂碼問題及解決

    這篇文章主要介紹了Jmeter測試時遇到的各種亂碼問題及解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2023-03-03
  • java中實現(xiàn)視頻處理以及播放功能代碼詳解

    java中實現(xiàn)視頻處理以及播放功能代碼詳解

    這篇文章主要給大家介紹了關于java中實現(xiàn)視頻處理以及播放功能的相關資料,最近要實現(xiàn)一套音視頻播放程序,所以這里給大家總結下,需要的朋友可以參考下
    2023-09-09
  • Java實現(xiàn)簡單堆棧代碼

    Java實現(xiàn)簡單堆棧代碼

    這篇文章主要為大家詳細介紹了Java實現(xiàn)簡單堆棧代碼,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2016-12-12
  • Java Web學習教程之Hibernate And MyBatis的理解

    Java Web學習教程之Hibernate And MyBatis的理解

    這篇文章主要給大家介紹了關于Java Web學習教程之Hibernate And MyBatis的相關資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們來一起學習學習吧。
    2018-04-04

最新評論