最新版 Spring Security,該如何實現動態許可權管理?

張哥說技術發表於2023-11-16

來源:江南一點雨


在松哥之前的教程中,曾經和小夥伴們聊過動態許可權管理的問題,大家在公眾號江南一點雨後臺回覆 s用不了在松哥之前的教程中,曾經和小夥伴們聊過動態許可權管理的問題,大家在公眾號江南一點雨後臺回覆 ss 有lss 有oDF 教程。

但是,不知道小夥伴們是否有留意過,進入到 Spring Boot3 之後,Spring Security 現在也進化到 Spring Security6 了,Spring Security6 的用法跟之前比起來還是有很大差異,松哥之前寫了篇文章和小夥伴們介紹 Spring Security6 中的一些變化:Spring Security6 全新寫法,大變樣!。

最近有小夥伴說松哥你之前講的動態許可權定義的方式,新版中用不了了,我抽空看了下,今天就和小夥伴們聊聊這個話題。

1. 許可權開發思路

先來說許可權開發的思路,當我們設計好 RBAC 許可權之後,具體到程式碼層面,我們有兩種實現思路:

  1. 直接在介面/Service 層方法上新增許可權註解,這樣做的好處是實現簡單,但是有一個問題就是許可權硬編碼,每一個方法需要什麼許可權都是程式碼中配置好的,後期如果想透過管理頁面修改是不可能的,要修改某一個方法所需要的許可權只能改程式碼。
  2. 將請求和許可權的關係透過資料庫來描述,每一個請求需要什麼許可權都在資料庫中配置好,當請求到達的時候,動態查詢,然後判斷許可權是否滿足,這樣做的好處是比較靈活,將來需要修改介面和許可權之間的關係時,可以透過管理頁面點選幾下,問題就解決了,不用修改程式碼,松哥之前的 vhr 中就是這樣做的。

有的小夥伴覺得第二種方案無法做到按鈕級別的許可權控制,這其實是一個誤解。想要做到按鈕級別的許可權控制,只需要資料庫中細化配置即可。

2. 具體實踐

2.1 舊方案回顧

在 vhr 中,松哥是透過重寫兩個類來和實現動態許可權的。

第一個類是收集許可權後設資料的類:

@Component
public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        //...
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }
}

在 getAttributes 方法中,根據當前請求的 URL 地址(從引數 Object 中可提取出來),然後根據許可權表中的配置,分析出來當前請求需要哪些許可權並返回。

另外我還重寫了一個決策器,其實決策器也可以不重寫,就看你自己的需求,如果 Spring Security 自帶的決策器無法滿足你的需求,那麼可以自己寫一個決策器:

@Component
public class CustomUrlDecisionManager implements AccessDecisionManager {
    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
        //...
    }

    @Override
    public boolean supports(ConfigAttribute attribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }
}

decide 方法就是做決策的地方,第一個引數中可以提取出當前使用者具備什麼許可權,第三個引數是當前請求需要什麼許可權,比較一下就行了,如果當前使用者不具備需要的許可權,則直接丟擲 AccessDeniedException 異常即可。

最後,透過 Bean 的後置處理器 BeanPostProcessor,將這兩個配置類放到 Spring Security 的 FilterSecurityInterceptor 攔截器中:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                @Override
                public <O extends FilterSecurityInterceptor> postProcess(O object) {
                    object.setAccessDecisionManager(customUrlDecisionManager);
                    object.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource);
                    return object;
                }
            })
            .and()
            //...
}

大致上的邏輯就是如此,以上類完整程式碼小夥伴們可以參考 。

2.2 新方案

不過以上程式碼在目前最新的 Spring Security6 中用不了了,不是因為類過期了,而是因為類被移除了!哪個類被移除了?FilterSecurityInterceptor。

FilterSecurityInterceptor 這個過濾器以前是做許可權處理的,但是在新版的 Spring Security6 中,這個攔截器被 AuthorizationFilter 代替了。

老實說,新版的方案其實更合理一些,傳統的方案感覺帶有很多前後端不分的影子,現在就往更純粹的前後端分離奔去。

由於新版中連 FilterSecurityInterceptor 都不用了,所以舊版的方案顯然行不通了,新版的方案實際上更加簡單。

雖然新舊寫法不同,但是核心思路是一模一樣。

我們來看下新版的配置:

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests(register -> register.anyRequest().access((authentication, object) -> {
                //表示請求的 URL 地址和資料庫的地址是否匹配上了
                boolean isMatch = false;
                //獲取當前請求的 URL 地址
                String requestURI = object.getRequest().getRequestURI();
                List<MenuWithRoleVO> menuWithRole = menuService.getMenuWithRole();
                for (MenuWithRoleVO m : menuWithRole) {
                    if (antPathMatcher.match(m.getUrl(), requestURI)) {
                        isMatch = true;
                        //說明找到了請求的地址了
                        //這就是當前請求需要的角色
                        List<Role> roles = m.getRoles();
                        //獲取當前登入使用者的角色
                        Collection<? extends GrantedAuthority> authorities = authentication.get().getAuthorities();
                        for (GrantedAuthority authority : authorities) {
                            for (Role role : roles) {
                                if (authority.getAuthority().equals(role.getName())) {
                                    //說明當前登入使用者具備當前請求所需要的角色
                                    return new AuthorizationDecision(true);
                                }
                            }
                        }
                    }
                }
                if (!isMatch) {
                    //說明請求的 URL 地址和資料庫的地址沒有匹配上,對於這種請求,統一隻要登入就能訪問
                    if (authentication.get() instanceof AnonymousAuthenticationToken) {
                        return new AuthorizationDecision(false);
                    } else {
                        //說明使用者已經認證了
                        return new AuthorizationDecision(true);
                    }
                }
                return new AuthorizationDecision(false);
            }))
            .formLogin(form -> 
            //...
            )
            .csrf(csrf -> 
            //...
            )
            .exceptionHandling(e -> 
            //...
            )
            .logout(logout ->
            //...
            );
    return http.build();
}

核心思路還是和之前一樣,只不過現在的工作都在 access 方法中完成。

access 方法的回撥中有兩個引數,第一個引數是 authentication,很明顯,這就是當前登入成功的使用者物件,從這裡我們就可以提取出來當前使用者所具備的許可權。

第二個引數 object 實際上是一個 RequestAuthorizationContext,從這個裡邊可以提取出來當前請求物件 HttpServletRequest,進而提取出來當前請求的 URL 地址,然後依據許可權表中的資訊,判斷出當前請求需要什麼許可權,再和 authentication 中提取出來的當前使用者所具備的許可權進行對比即可。

如果當前登入使用者具備請求所需要的許可權,則返回 new AuthorizationDecision(true);,否則返回 new AuthorizationDecision(false); 即可。

其實無論什麼框架,只要能把其中一個版本掌握個 70%,以後無論它怎麼升級,你都能快速上手!

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024923/viewspace-2995586/,如需轉載,請註明出處,否則將追究法律責任。

相關文章