深刻理解Spring Security受權機制原理

原創/朱季謙java

在Spring Security權限框架裏,若要對後端http接口實現權限受權控制,有兩種實現方式。程序員

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

@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 }

 

2、一種基於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 }

這兩種方式各有各的特色,在平常開發當中,普通程序員接觸比較多的,則是註解方式的接口權限控制。springboot

那麼問題來了,咱們配置這些註解或者類,其security框是如何幫作到能針對具體的後端API接口作權限控制的呢?app

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

若要對這個過程作一個總的概述,筆者總體以本身的思考稍做了總結,能夠簡單幾句話說明其總體實現,以該接口爲例:框架

  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進行過濾處理操做,執行過程以下:

image

過濾器鏈像一條鐵鏈,把相關的過濾器連接起來,請求線程如螞蟻同樣,會沿着這條鏈一直爬過去-----即,經過chain.doFilter(request, response)方法,一層嵌套一層地傳遞下去,當傳遞到該請求對應的最後一個過濾器,就會將處理完成的請求轉發返回。所以,經過過濾器鏈,可實如今不一樣的過濾器當中對請求request作處理,且過濾器之間彼此互不干擾。

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

 

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

 

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

image

這些過濾器都在同一條過濾器鏈上,即經過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)的實現主要包括如下步驟:

 

1、獲取需訪問的接口權限,這裏debug的例子是調用了前文提到的「/save」接口,其權限設置是@PreAuthorize("hasAuthority('sys:user:add') AND hasAuthority('sys:user:edit')"),根據下面截圖,可知變量attributes獲取了到該請求接口的權限:

image

2、獲取認證經過以後保存在 SecurityContextHolder的用戶信息,其中,authorities是一個保存用戶所擁有所有權限的集合;

image

這裏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()獲取到保存的用戶信息;

 

3、嘗試受權,用戶信息authenticated、請求攜帶對象信息object、所訪問接口的權限信息attributes,傳入到decide方法;

image

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三個子類;

image

AffirmativeBased表示一票經過,這是AccessDecisionManager默認類;

ConsensusBased表示少數服從多數;

UnanimousBased表示一票反對;

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

點進去AffirmativeBased類裏,能夠看到裏面有一行代碼int result = voter.vote(authentication, object, configAttributes):

image

這裏的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,該接口的實現以下圖所示:

image

這裏簡單介紹兩個經常使用的:

1. RoleVoter:這是用來判斷url請求是否具有接口須要的角色,這種主要用於使用註解@Secured處理的權限;
2. PreInvocationAuthorizationAdviceVoter:針對相似註解@PreAuthorize("hasAuthority('sys:user:add') AND hasAuthority('sys:user:edit')")處理的權限;

image

到這一步,代碼就開始難懂了,這部分封裝地過於複雜,整體的邏輯,是將用戶信息所具備的權限與該接口的權限表達式作匹配,若能匹配成功,返回true,在三目運算符中,

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

image

image

到此爲止,本文就結束了,筆者仍存在不足之處,歡迎各位讀者可以給予珍貴的反饋,也算是對筆者寫做的一種鼓勵。

相關文章
相關標籤/搜索