编程知识 cdmana.com

深入理解Spring Security授權機制原理

原創/朱季謙

在Spring Security許可權框架裡,若要對後端http介面實現許可權授權控制,有兩種實現方式。

一、一種是基於註解方法級的鑑權,其中,註解方式又有@Secured和@PreAuthorize兩種。

@Secured如:

  1 @PostMapping("/test")  2  @Secured({WebResRole.ROLE_PEOPLE_W})  3  public void test(){  4  ......  5  return null;  6  }

@PreAuthorize如:

  1 @PostMapping("save")  2 @PreAuthorize("hasAuthority('sys:user:add') AND hasAuthority('sys:user:edit')")  3 public RestResponse save(@RequestBody @Validated SysUser sysUser, BindingResult result) {  4     ValiParamUtils.ValiParamReq(result);  5     return sysUserService.save(sysUser);  6 }

 

二、一種基於config配置類,需在對應config類配置@EnableGlobalMethodSecurity(prePostEnabled = true)註解才能生效,其許可權控制方式如下:

  1 @Override  2 protected void configure(HttpSecurity httpSecurity) throws Exception {  3     //使用的是JWT,禁用csrf  4     httpSecurity.cors().and().csrf().disable()  5             //設定請求必須進行許可權認證  6             .authorizeRequests()  7             //首頁和登入頁面  8             .antMatchers("/").permitAll()  9             .antMatchers("/login").permitAll() 10             // 其他所有請求需要身份認證 11             .anyRequest().authenticated(); 12     //退出登入處理 13     httpSecurity.logout().logoutSuccessHandler(...); 14     //token驗證過濾器 15     httpSecurity.addFilterBefore(...); 16 }

這兩種方式各有各的特點,在日常開發當中,普通程式設計師接觸比較多的,則是註解方式的介面許可權控制。

那麼問題來了,我們配置這些註解或者類,其security框是如何幫做到能針對具體的後端API介面做許可權控制的呢?

單從一行@PreAuthorize("hasAuthority('sys:user:add') AND hasAuthority('sys:user:edit')")註解上看,是看不出任何頭緒來的,若要回答這個問題,還需深入到原始碼層面,方能對security授權機制有更好理解。

若要對這個過程做一個總的概述,筆者整體以自己的思考稍作了總結,可以簡單幾句話說明其整體實現,以該介面為例:

  1 @PostMapping("save")  2 @PreAuthorize("hasAuthority('sys:user:add')")  3 public RestResponse save(@RequestBody @Validated SysUser sysUser, BindingResult result) {  4     ValiParamUtils.ValiParamReq(result);  5     return sysUserService.save(sysUser);  6 }

即,認證通過的使用者,發起請求要訪問“/save”介面,若該url請求在配置類裡設定為必須進行許可權認證的,就會被security框架使用filter攔截器對該請求進行攔截認證。攔截過程主要一個動作,是把該請求所擁有的許可權集與@PreAuthorize設定的許可權字元“sys:user:add”進行匹配,若能匹配上,說明該請求是擁有呼叫“/save”介面的許可權,那麼,就可以被允許執行該介面資源。

 

在springboot+security+jwt框架中,通過一系列內建或者自行定義的過濾器Filter來達到許可權控制,如何設定自定義的過濾器Filter呢?例如,可以通過設定httpSecurity.addFilterBefore(new JwtFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class)來自定義一個基於JWT攔截的過濾器JwtFilter,這裡的addFilterBefore方法將在下一篇文詳細分析,這裡暫不展開,該方法大概意思就是,將自定義過濾器JwtFilter加入到Security框架裡,成為其中的一個優先安全Filter,程式碼層面就是將自定義過濾器新增到List<Filter> filters。

 

設定增加自行定義的過濾器Filter虛擬碼如下:

  1 @Configuration  2 @EnableWebSecurity  3 @EnableGlobalMethodSecurity(prePostEnabled = true)  4 public class SecurityConfig extends WebSecurityConfigurerAdapter {  5     ......  6     @Override  7     protected void configure(HttpSecurity httpSecurity) throws Exception {  8         //使用的是JWT,禁用csrf  9         httpSecurity.cors().and().csrf().disable() 10                 //設定請求必須進行許可權認證 11                 .authorizeRequests() 12                 ...... 13                 //首頁和登入頁面 14                 .antMatchers("/").permitAll() 15                 .antMatchers("/login").permitAll() 16                 // 其他所有請求需要身份認證 17                 .anyRequest().authenticated(); 18         ...... 19         //token驗證過濾器 20         httpSecurity.addFilterBefore(new JwtFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class); 21     } 22 }

該過濾器類extrends繼承BasicAuthenticationFilter,而BasicAuthenticationFilter是繼承OncePerRequestFilter,該過濾器確保在一次請求只通過一次filter,而不需要重複執行。這樣配置後,當請求過來時,會自動被JwtFilter類攔截,這時,將執行重寫的doFilterInternal方法,在SecurityContextHolder.getContext().setAuthentication(authentication)認證通過後,會執行過濾器鏈FilterChain的方法chain.doFilter(request, response);

  1 public class JwtFilter  extends BasicAuthenticationFilter {  2   3     @Autowired  4     public JwtFilter(AuthenticationManager authenticationManager) {  5         super(authenticationManager);  6     }  7   8    @Override  9    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { 10        // 獲取token, 並檢查登入狀態 11        // 獲取令牌並根據令牌獲取登入認證資訊 12        Authentication authentication = JwtTokenUtils.getAuthenticationeFromToken(request); 13        // 設定登入認證資訊到上下文 14        SecurityContextHolder.getContext().setAuthentication(authentication); 15  16        chain.doFilter(request, response); 17    } 18  19 }

那麼,問題來了,過濾器鏈FilterChain究竟是什麼?

這裡,先點進去看下其類原始碼:

  1 package javax.servlet;  2   3 import java.io.IOException;  4   5 public interface FilterChain {  6     void doFilter(ServletRequest var1, ServletResponse var2) throws IOException, ServletException;  7 }

FilterChain只有一個 doFilter方法,這個方法的作用就是將請求request轉發到下一個過濾器filter進行過濾處理操作,執行過程如下:

過濾器鏈像一條鐵鏈,把相關的過濾器連結起來,請求執行緒如螞蟻一樣,會沿著這條鏈一直爬過去-----即,通過chain.doFilter(request, response)方法,一層巢狀一層地傳遞下去,當傳遞到該請求對應的最後一個過濾器,就會將處理完成的請求轉發返回。因此,通過過濾器鏈,可實現在不同的過濾器當中對請求request做處理,且過濾器之間彼此互不干擾。

這其實是一種責任鏈的設計模式。在這種模式當中,通常每個接受者都包含對另一個接收者的引用。如果一個物件不能處理該請求,那麼,它就會把相同的請求傳給下一個接收者,以此類推。

 

Spring Security框架上過濾器鏈上都有哪些過濾器呢?

 

可以在DefaultSecurityFilterChain類根據輸出相關log或者debug來檢視Security都有哪些過濾器,如在DefaultSecurityFilterChain類中的構造器中打斷點,如圖所示,可以看到,自定義的JwtFilter過濾器也包含其中:

這些過濾器都在同一條過濾器鏈上,即通過chain.doFilter(request, response)可將請求一層接一層轉發,處理請求介面是否授權的主要過濾器是FilterSecurityInterceptor,其主要作用如下:

1. 獲取到需訪問介面的許可權資訊,即@Secured({WebResRole.ROLE_PEOPLE_W}) 或@PreAuthorize定義的許可權資訊;

2. 根據SecurityContextHolder中儲存的authentication使用者資訊,來判斷是否包含與需訪問介面的許可權資訊,若包含,則說明擁有該介面許可權;

3. 主要授權功能在父類AbstractSecurityInterceptor中實現;

  

我們將從FilterSecurityInterceptor這裡開始重點分析Security授權機制原理的實現。

過濾器鏈將請求傳遞轉發FilterSecurityInterceptor時,會執行FilterSecurityInterceptor的doFilter方法:

  1 public void doFilter(ServletRequest request, ServletResponse response,  2       FilterChain chain) throws IOException, ServletException {  3    FilterInvocation fi = new FilterInvocation(request, response, chain);  4    invoke(fi);  5 }

在這段程式碼當中,FilterInvocation類是一個有意思的存在,其實它的功能很簡單,就是將上一個過濾器傳遞過濾的request,response,chain複製儲存到FilterInvocation裡,專門供FilterSecurityInterceptor過濾器使用。它的有意思之處在於,是將多個引數統一歸納到一個類當中,其到統一管理作用,你想,若是N多個引數,傳進來都分散到類的各個地方,引數多了,程式碼多了,方法過於分散時,可能就很容易造成閱讀過程中,弄糊塗這些個引數都是哪裡來了。但若統一歸納到一個類裡,就能很快定位其來源,方便程式碼閱讀。網上有人提到該FilterInvocation類還起到解耦作用,即避免與其他過濾器使用同樣的引用變數。

總而言之,這個地方的設定雖簡單,但很值得我們學習一番,將其思想運用到實際開發當中,不外乎也是一種能簡化程式碼的方法。

FilterInvocation主要原始碼如下:

  1 public class FilterInvocation {  2   3    private FilterChain chain;  4    private HttpServletRequest request;  5    private HttpServletResponse response;  6   7   8    public FilterInvocation(ServletRequest request, ServletResponse response,  9          FilterChain chain) { 10       if ((request == null) || (response == null) || (chain == null)) { 11          throw new IllegalArgumentException("Cannot pass null values to constructor"); 12       } 13  14       this.request = (HttpServletRequest) request; 15       this.response = (HttpServletResponse) response; 16       this.chain = chain; 17    } 18    ...... 19 }

FilterSecurityInterceptor的doFilter方法裡呼叫invoke(fi)方法:

  1 public void invoke(FilterInvocation fi) throws IOException, ServletException {  2    if ((fi.getRequest() != null)  3          && (fi.getRequest().getAttribute(FILTER_APPLIED) != null)  4          && observeOncePerRequest) {  5      //篩選器已應用於此請求,每個請求處理一次,所以不需重新進行安全檢查   6       fi.getChain().doFilter(fi.getRequest(), fi.getResponse());  7    }  8    else {  9       // 第一次呼叫此請求時,需執行安全檢查 10       if (fi.getRequest() != null && observeOncePerRequest) { 11          fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE); 12       } 13        //1.授權具體實現入口 14       InterceptorStatusToken token = super.beforeInvocation(fi); 15       try { 16        //2.授權通過後執行的業務 17          fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); 18       } 19       finally { 20          super.finallyInvocation(token); 21       } 22        //3.後續處理 23       super.afterInvocation(token, null); 24    } 25 }

授權機制實現的入口是super.beforeInvocation(fi),其具體實現在父類AbstractSecurityInterceptor中實現,beforeInvocation(Object object)的實現主要包括以下步驟:

 

一、獲取需訪問的介面許可權,這裡debug的例子是呼叫了前文提到的“/save”介面,其許可權設定是@PreAuthorize("hasAuthority('sys:user:add') AND hasAuthority('sys:user:edit')"),根據下面截圖,可知變數attributes獲取了到該請求介面的許可權:

二、獲取認證通過之後儲存在 SecurityContextHolder的使用者資訊,其中,authorities是一個儲存使用者所擁有全部許可權的集合;

這裡authenticateIfRequired()方法核心實現:

  1 private Authentication authenticateIfRequired() {  2    Authentication authentication = SecurityContextHolder.getContext()  3          .getAuthentication();  4    if (authentication.isAuthenticated() && !alwaysReauthenticate) {  5      ......  6       return authentication;  7    }  8    authentication = authenticationManager.authenticate(authentication);  9    SecurityContextHolder.getContext().setAuthentication(authentication); 10    return authentication; 11 }

在認證過程通過後,執行SecurityContextHolder.getContext().setAuthentication(authentication)將使用者資訊儲存在Security框架當中,之後可通過SecurityContextHolder.getContext().getAuthentication()獲取到儲存的使用者資訊;

 

三、嘗試授權,使用者資訊authenticated、請求攜帶物件資訊object、所訪問介面的許可權資訊attributes,傳入到decide方法;

decide()是決策管理器AccessDecisionManager定義的一個方法。

  1 public interface AccessDecisionManager {  2    void decide(Authentication authentication, Object object,  3          Collection<ConfigAttribute> configAttributes) throws AccessDeniedException,  4          InsufficientAuthenticationException;  5    boolean supports(ConfigAttribute attribute);  6    boolean supports(Class<?> clazz);  7 }

AccessDecisionManager是一個interface介面,這是授權體系的核心。FilterSecurityInterceptor 在鑑權時,就是通過呼叫AccessDecisionManager的decide()方法來進行授權決策,若能通過,則可訪問對應的介面。

AccessDecisionManager類的方法具體實現都在子類當中,包含AffirmativeBased、ConsensusBased、UnanimousBased三個子類;

AffirmativeBased表示一票通過,這是AccessDecisionManager預設類;

ConsensusBased表示少數服從多數;

UnanimousBased表示一票反對;

如何理解這個投票機制呢?

點進去AffirmativeBased類裡,可以看到裡面有一行程式碼int result = voter.vote(authentication, object, configAttributes):

這裡的AccessDecisionVoter是一個投票器,用到委託設計模式,即AffirmativeBased類會委託投票器進行選舉,然後將選舉結果返回賦值給result,然後判斷result結果值,若為1,等於ACCESS_GRANTED值時,則表示可一票通過,也就是,允許訪問該介面的許可權。

這裡,ACCESS_GRANTED表示同意、ACCESS_DENIED表示拒絕、ACCESS_ABSTAIN表示棄權:

  1 public interface AccessDecisionVoter<S> {  2    int ACCESS_GRANTED = 1;//表示同意  3    int ACCESS_ABSTAIN = 0;//表示棄權  4    int ACCESS_DENIED = -1;//表示拒絕  5    ......  6    }

那麼,什麼情況下,投票結果result為1呢?

這裡需要研究一下投票器介面AccessDecisionVoter,該介面的實現如下圖所示:

這裡簡單介紹兩個常用的:

1. RoleVoter:這是用來判斷url請求是否具備介面需要的角色,這種主要用於使用註解@Secured處理的許可權;
2. PreInvocationAuthorizationAdviceVoter:針對類似註解@PreAuthorize("hasAuthority('sys:user:add') AND hasAuthority('sys:user:edit')")處理的許可權;

到這一步,程式碼就開始難懂了,這部分封裝地過於複雜,總體的邏輯,是將使用者資訊所具有的許可權與該介面的許可權表示式做匹配,若能匹配成功,返回true,在三目運算子中,

allowed ? ACCESS_GRANTED : ACCESS_DENIED,就會返回ACCESS_GRANTED ,即表示通過,這樣,返回給result的值就為1了。

到此為止,本文就結束了,筆者仍存在不足之處,歡迎各位讀者能夠給予珍貴的反饋,也算是對筆者寫作的一種鼓

版权声明
本文为[itread01]所创,转载请带上原文链接,感谢
https://www.itread01.com/content/1608816483.html

Scroll to Top