spring security 授權方式(自定義)及原始碼跟蹤

尋找的路上發表於2021-12-02

spring security 授權方式(自定義)及原始碼跟蹤

​ 這節我們來看看spring security的幾種授權方式,及簡要的原始碼跟蹤。在初步接觸spring security時,為了實現它的授權,特別是它的自定義授權,在網上找了特別多的文章以及例子,覺得好難,但是現在自己嘗試結合官方文件及demo來學習,頗有收穫。

基於表示式Spel的訪問控制

​ Spring Security 使用 Spring EL 進行表示式支援,不瞭解Spring EL的童鞋自行學習。根據文件https://docs.spring.io/spring-security/site/docs/5.4.9/reference/html5/#el-access 在IDEA中檢視SecurityExpressionRoot類的上下繼承關係,SecurityExpressionOperations宣告瞭各個表示式介面,最終由WebSecurityExpressionRoot、MethodSecurityExpressionRoot實現各個具體的表示式邏輯。繼承關係如下所示:

​ 在這裡我們能夠知道,最常用的應該就是基於Web\Method這兩種方式來進行授權我們的應用。再來看下 SecurityExpressionRoot 類中定義的最基本的 SpEL 有哪些:

​ 我們簡單介紹幾個表示式介面:

Expression Description
hasRole(String role) 如果當前主體具有指定的角色,則返回 true
hasAnyRole(String… roles) 如果當前主體具有任何提供的角色,則返回 true
hasAuthority(String authority) 如果當前主體具有指定的許可權,則返回 true。
hasAnyAuthority(String… authorities) 如果當前主體具有任何提供的許可權,則返回 true
authentication 允許直接訪問從 SecurityContext 獲得的當前 Authentication 物件
principal 允許直接訪問代表當前使用者的主體物件

授權方式

基於Web/Url的安全表示式

這種方式可以對單個Url進行安全驗證,也可以對批量的Url進行安全驗證,比如

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
            .authorizeRequests()
        	// 針對/admin/api/hello 這個介面要有p1許可權
            .antMatchers("/admin/api/hello").hasAuthority("p1")
        	// 對於訪問/user/api/** 的介面要有user許可權
            .antMatchers("/user/api/**").hasRole("USER")
            .antMatchers("/app/api/**").permitAll()
            .antMatchers("/css/**", "/index").permitAll()
            .antMatchers("/user/**").hasRole("USER")
            .and()
            .formLogin()
            .loginPage("/login")
            .failureUrl("/login-error")
            .permitAll();
}

基於Method的安全表示式

Method Security Expressions

Method security is a bit more complicated than a simple allow or deny rule. Spring Security 3.0 introduced some new annotations in order to allow comprehensive support for the use of expressions.

Spring Security 3.0 引入了一些新的註解,以便全面支援表示式的使用,分別是@PreAuthorize, @PreFilter, @PostAuthorize and @PostFilter相信大家有web開發的基礎,不難知道這幾個註解的意思。

  • @PreAuthorize:在訪問方法前進行鑑權

  • @PreFilter:同上

  • @PostAuthorize:在訪問方法後進行鑑權

  • @PostFiltert:同上

    public class AdminController {
    
        @GetMapping("hello")
        @PostAuthorize("hasRole('User')")
        public String hello() {
            return "hello, admin";
        }
    
        @GetMapping("p1")
        @PreAuthorize("hasAuthority('p1')")
        public String p1() {
            return "hello, p1";
        }
    }
    

但是基於方法的需要事先在配置類新增註解,表示開啟方法驗證。

@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)

根據官網介紹還有其他2中方式(基於AOP 、spring security原生的註釋@Secure),有興趣的小夥伴可以自行參閱https://docs.spring.io/spring-security/site/docs/5.4.9/reference/html5/#secure-object-impls

授權原理

​ 根據文件https://docs.spring.io/spring-security/site/docs/5.4.9/reference/html5/#secure-object-impls指出,spring security提供了攔截器來控制安全物件的訪問,例如方法呼叫、web請求。AccessDecisionManager 做出關於是否允許呼叫繼續進行的呼叫前決定。

​ 檢視AccessDecisionManager 介面:

decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)

引數說明:

  • authentication:當前登入物件主體
  • object: 當前安全保護物件
  • configAttributes:訪問當前物件必須要有的許可權屬性

再看看看它的三個實現類,預設的實現是根據各個實現類的投票機制來決定是否能夠訪問當前安全保護物件的:

AffirmativeBased:只要configAttributes中有一個許可權滿足就可以訪問當前保護物件

ConsensusBased:滿足超過一半的許可權就能夠訪問當前保護物件

UnanimousBased:configAttributes中所有的許可權都滿足才能訪問當前保護物件

由於我們不知道預設是哪個實現類,所以我們在三個類上的decide方法都打上斷點,這樣我們就能知道預設是哪個實現類了,

內部的投票實現有興趣的小夥伴自行探索,到這樣我們大概就明白spring security預設的授權實現機制了。接著我們根據該機制去實現我們的自定義授權方式。

給出官網的一張原理圖

  1. 首先,FilterSecurityInterceptor 從 SecurityContextHolder 獲得一個 Authentication
  2. 其次,FilterSecurityInterceptor 從傳入 FilterSecurityInterceptor 的 HttpServletRequest、HttpServletResponse 和 FilterChain 建立一個 FilterInvocation
  3. 接下來,它將 FilterInvocation 傳遞給 SecurityMetadataSource 以獲取 ConfigAttributes
  4. 最後,它將 Authentication、FilterInvocation 和 ConfigAttributes 傳遞給 AccessDecisionManager。
    1. 如果授權被拒絕,則丟擲 AccessDeniedException。在這種情況下,ExceptionTranslationFilter 處理 AccessDeniedException
    2. 如果訪問被授予,FilterSecurityInterceptor 繼續使用允許應用程式正常處理的 FilterChain。

自定義授權方式

​ 根據葫蘆畫瓢,我們首先需要

1、自定義一個AccessDecisionManager實現類,讓它確定到底是否能夠鑑權通過,能夠訪問保護物件;

@Component
public class CustomUrlDecisionManager implements AccessDecisionManager {
    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
        for (ConfigAttribute configAttribute : configAttributes) {
            String needRole = configAttribute.getAttribute();
            if ("ROLE_LOGIN".equals(needRole)) {
                if (authentication instanceof AnonymousAuthenticationToken) {
                    throw new AccessDeniedException("尚未登入,請登入!");
                }else {
                    return;
                }
            }
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
            for (GrantedAuthority authority : authorities) {
                if (authority.getAuthority().equals(needRole)) {
                    return;
                }
            }
        }
        throw new AccessDeniedException("許可權不足,請聯絡管理員!");
    }

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

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

2、接著實現一個FilterInvocationSecurityMetadataSource實現類,這個類給出訪問保護物件具體需要的哪些許可權。

/**
 * 這個類的作用,主要是根據使用者傳來的請求地址,分析出請求需要的角色
 */
@Component
public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    @Autowired
    MenuService menuService;
    AntPathMatcher antPathMatcher = new AntPathMatcher();
    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        String requestUrl = ((FilterInvocation) object).getRequestUrl();
        List<Menu> menus = menuService.getAllMenusWithRole();
        for (Menu menu : menus) {
            if (antPathMatcher.match(menu.getUrl(), requestUrl)) {
                List<Role> roles = menu.getRoles();
                String[] str = new String[roles.size()];
                for (int i = 0; i < roles.size(); i++) {
                    str[i] = roles.get(i).getName();
                }
                return SecurityConfig.createList(str);
            }
        }
        return SecurityConfig.createList("ROLE_LOGIN");
    }

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

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

3、將上面2個物件新增到攔截器中,給FilterSecurityInterceptor重新設定它的這2個屬性

http.authorizeRequests()
        .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
            @Override
            public <O extends FilterSecurityInterceptor> O postProcess(O object) {
                object.setAccessDecisionManager(customUrlDecisionManager);
                object.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource);
                return object;
            }
        })

​ 相信到這裡,小夥伴也能根據自己實際專案需要怎樣的授權方式去進行實現了,如果是AOP/@secure方式的則需要再看一下文件說明。好了,spring security的章節就到這裡,後面繼續學習spring security oauth2的章節。

相關文章