<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          深入理解Spring Security授權(quán)機(jī)制原理

          共 17361字,需瀏覽 35分鐘

           ·

          2021-05-11 17:58

          點(diǎn)擊上方藍(lán)色字體,選擇“標(biāo)星公眾號(hào)”

          優(yōu)質(zhì)文章,第一時(shí)間送達(dá)

            作者 |  朱季謙

          來(lái)源 |  urlify.cn/i2UJRn

          76套java從入門(mén)到精通實(shí)戰(zhàn)課程分享

          在Spring Security權(quán)限框架里,若要對(duì)后端http接口實(shí)現(xiàn)權(quán)限授權(quán)控制,有兩種實(shí)現(xiàn)方式。

          一、一種是基于注解方法級(jí)的鑒權(quán),其中,注解方式又有@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配置類,需在對(duì)應(yīng)config類配置@EnableGlobalMethodSecurity(prePostEnabled = true)注解才能生效,其權(quán)限控制方式如下:

            1 @Override
            2 protected void configure(HttpSecurity httpSecurity) throws Exception {
            3     //使用的是JWT,禁用csrf
            4     httpSecurity.cors().and().csrf().disable()
            5             //設(shè)置請(qǐng)求必須進(jìn)行權(quán)限認(rèn)證
            6             .authorizeRequests()
            7             //首頁(yè)和登錄頁(yè)面
            8             .antMatchers("/").permitAll()
            9             .antMatchers("/login").permitAll()
           10             // 其他所有請(qǐng)求需要身份認(rèn)證
           11             .anyRequest().authenticated();
           12     //退出登錄處理
           13     httpSecurity.logout().logoutSuccessHandler(...);
           14     //token驗(yàn)證過(guò)濾器
           15     httpSecurity.addFilterBefore(...);
           16 } 

          這兩種方式各有各的特點(diǎn),在日常開(kāi)發(fā)當(dāng)中,普通程序員接觸比較多的,則是注解方式的接口權(quán)限控制。

          那么問(wèn)題來(lái)了,我們配置這些注解或者類,其security框是如何幫做到能針對(duì)具體的后端API接口做權(quán)限控制的呢?

          單從一行@PreAuthorize("hasAuthority('sys:user:add') AND hasAuthority('sys:user:edit')")注解上看,是看不出任何頭緒來(lái)的,若要回答這個(gè)問(wèn)題,還需深入到源碼層面,方能對(duì)security授權(quán)機(jī)制有更好理解。

          若要對(duì)這個(gè)過(guò)程做一個(gè)總的概述,筆者整體以自己的思考稍作了總結(jié),可以簡(jiǎn)單幾句話說(shuō)明其整體實(shí)現(xiàn),以該接口為例:

            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 } 

          即,認(rèn)證通過(guò)的用戶,發(fā)起請(qǐng)求要訪問(wèn)“/save”接口,若該url請(qǐng)求在配置類里設(shè)置為必須進(jìn)行權(quán)限認(rèn)證的,就會(huì)被security框架使用filter攔截器對(duì)該請(qǐng)求進(jìn)行攔截認(rèn)證。攔截過(guò)程主要一個(gè)動(dòng)作,是把該請(qǐng)求所擁有的權(quán)限集與@PreAuthorize設(shè)置的權(quán)限字符“sys:user:add”進(jìn)行匹配,若能匹配上,說(shuō)明該請(qǐng)求是擁有調(diào)用“/save”接口的權(quán)限,那么,就可以被允許執(zhí)行該接口資源。

           

          在springboot+security+jwt框架中,通過(guò)一系列內(nèi)置或者自行定義的過(guò)濾器Filter來(lái)達(dá)到權(quán)限控制,如何設(shè)置自定義的過(guò)濾器Filter呢?例如,可以通過(guò)設(shè)置httpSecurity.addFilterBefore(new JwtFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class)來(lái)自定義一個(gè)基于JWT攔截的過(guò)濾器JwtFilter,這里的addFilterBefore方法將在下一篇文詳細(xì)分析,這里暫不展開(kāi),該方法大概意思就是,將自定義過(guò)濾器JwtFilter加入到Security框架里,成為其中的一個(gè)優(yōu)先安全Filter,代碼層面就是將自定義過(guò)濾器添加到List<Filter> filters。

           

          設(shè)置增加自行定義的過(guò)濾器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                 //設(shè)置請(qǐng)求必須進(jìn)行權(quán)限認(rèn)證
           11                 .authorizeRequests()
           12                 ......
           13                 //首頁(yè)和登錄頁(yè)面
           14                 .antMatchers("/").permitAll()
           15                 .antMatchers("/login").permitAll()
           16                 // 其他所有請(qǐng)求需要身份認(rèn)證
           17                 .anyRequest().authenticated();
           18         ......
           19         //token驗(yàn)證過(guò)濾器
           20         httpSecurity.addFilterBefore(new JwtFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class);
           21     }
           22 }

          該過(guò)濾器類extrends繼承BasicAuthenticationFilter,而B(niǎo)asicAuthenticationFilter是繼承OncePerRequestFilter,該過(guò)濾器確保在一次請(qǐng)求只通過(guò)一次filter,而不需要重復(fù)執(zhí)行。這樣配置后,當(dāng)請(qǐng)求過(guò)來(lái)時(shí),會(huì)自動(dòng)被JwtFilter類攔截,這時(shí),將執(zhí)行重寫(xiě)的doFilterInternal方法,在SecurityContextHolder.getContext().setAuthentication(authentication)認(rèn)證通過(guò)后,會(huì)執(zhí)行過(guò)濾器鏈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, 并檢查登錄狀態(tài)
           11        // 獲取令牌并根據(jù)令牌獲取登錄認(rèn)證信息
           12        Authentication authentication = JwtTokenUtils.getAuthenticationeFromToken(request);
           13        // 設(shè)置登錄認(rèn)證信息到上下文
           14        SecurityContextHolder.getContext().setAuthentication(authentication);
           15 
           16        chain.doFilter(request, response);
           17    }
           18 
           19 } 

          那么,問(wèn)題來(lái)了,過(guò)濾器鏈FilterChain究竟是什么?

          這里,先點(diǎn)進(jìn)去看下其類源碼:

            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只有一個(gè) doFilter方法,這個(gè)方法的作用就是將請(qǐng)求request轉(zhuǎn)發(fā)到下一個(gè)過(guò)濾器filter進(jìn)行過(guò)濾處理操作,執(zhí)行過(guò)程如下:

          過(guò)濾器鏈像一條鐵鏈,把相關(guān)的過(guò)濾器鏈接起來(lái),請(qǐng)求線程如螞蟻一樣,會(huì)沿著這條鏈一直爬過(guò)去-----即,通過(guò)chain.doFilter(request, response)方法,一層嵌套一層地傳遞下去,當(dāng)傳遞到該請(qǐng)求對(duì)應(yīng)的最后一個(gè)過(guò)濾器,就會(huì)將處理完成的請(qǐng)求轉(zhuǎn)發(fā)返回。因此,通過(guò)過(guò)濾器鏈,可實(shí)現(xiàn)在不同的過(guò)濾器當(dāng)中對(duì)請(qǐng)求request做處理,且過(guò)濾器之間彼此互不干擾。

          這其實(shí)是一種責(zé)任鏈的設(shè)計(jì)模式。在這種模式當(dāng)中,通常每個(gè)接受者都包含對(duì)另一個(gè)接收者的引用。如果一個(gè)對(duì)象不能處理該請(qǐng)求,那么,它就會(huì)把相同的請(qǐng)求傳給下一個(gè)接收者,以此類推。

          Spring Security框架上過(guò)濾器鏈上都有哪些過(guò)濾器呢?

          可以在DefaultSecurityFilterChain類根據(jù)輸出相關(guān)log或者debug來(lái)查看Security都有哪些過(guò)濾器,如在DefaultSecurityFilterChain類中的構(gòu)造器中打斷點(diǎn),如圖所示,可以看到,自定義的JwtFilter過(guò)濾器也包含其中:

          這些過(guò)濾器都在同一條過(guò)濾器鏈上,即通過(guò)chain.doFilter(request, response)可將請(qǐng)求一層接一層轉(zhuǎn)發(fā),處理請(qǐng)求接口是否授權(quán)的主要過(guò)濾器是FilterSecurityInterceptor,其主要作用如下:

          1. 獲取到需訪問(wèn)接口的權(quán)限信息,即@Secured({WebResRole.ROLE_PEOPLE_W}) 或@PreAuthorize定義的權(quán)限信息;

          2. 根據(jù)SecurityContextHolder中存儲(chǔ)的authentication用戶信息,來(lái)判斷是否包含與需訪問(wèn)接口的權(quán)限信息,若包含,則說(shuō)明擁有該接口權(quán)限;

          3. 主要授權(quán)功能在父類AbstractSecurityInterceptor中實(shí)現(xiàn);  

          我們將從FilterSecurityInterceptor這里開(kāi)始重點(diǎn)分析Security授權(quán)機(jī)制原理的實(shí)現(xiàn)。

          過(guò)濾器鏈將請(qǐng)求傳遞轉(zhuǎn)發(fā)FilterSecurityInterceptor時(shí),會(huì)執(zhí)行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 }

          在這段代碼當(dāng)中,F(xiàn)ilterInvocation類是一個(gè)有意思的存在,其實(shí)它的功能很簡(jiǎn)單,就是將上一個(gè)過(guò)濾器傳遞過(guò)濾的request,response,chain復(fù)制保存到FilterInvocation里,專門(mén)供FilterSecurityInterceptor過(guò)濾器使用。它的有意思之處在于,是將多個(gè)參數(shù)統(tǒng)一歸納到一個(gè)類當(dāng)中,其到統(tǒng)一管理作用,你想,若是N多個(gè)參數(shù),傳進(jìn)來(lái)都分散到類的各個(gè)地方,參數(shù)多了,代碼多了,方法過(guò)于分散時(shí),可能就很容易造成閱讀過(guò)程中,弄糊涂這些個(gè)參數(shù)都是哪里來(lái)了。但若統(tǒng)一歸納到一個(gè)類里,就能很快定位其來(lái)源,方便代碼閱讀。網(wǎng)上有人提到該FilterInvocation類還起到解耦作用,即避免與其他過(guò)濾器使用同樣的引用變量。

          總而言之,這個(gè)地方的設(shè)定雖簡(jiǎn)單,但很值得我們學(xué)習(xí)一番,將其思想運(yùn)用到實(shí)際開(kāi)發(fā)當(dāng)中,不外乎也是一種能簡(jiǎn)化代碼的方法。

          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方法里調(diào)用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      //篩選器已應(yīng)用于此請(qǐng)求,每個(gè)請(qǐng)求處理一次,所以不需重新進(jìn)行安全檢查 
            6       fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
            7    }
            8    else {
            9       // 第一次調(diào)用此請(qǐng)求時(shí),需執(zhí)行安全檢查
           10       if (fi.getRequest() != null && observeOncePerRequest) {
           11          fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
           12       }
           13        //1.授權(quán)具體實(shí)現(xiàn)入口
           14       InterceptorStatusToken token = super.beforeInvocation(fi);
           15       try {
           16        //2.授權(quán)通過(guò)后執(zhí)行的業(yè)務(wù)
           17          fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
           18       }
           19       finally {
           20          super.finallyInvocation(token);
           21       }
           22        //3.后續(xù)處理
           23       super.afterInvocation(token, null);
           24    }
           25 }

           權(quán)機(jī)制實(shí)現(xiàn)的入口是super.beforeInvocation(fi),其具體實(shí)現(xiàn)在父類AbstractSecurityInterceptor中實(shí)現(xiàn),beforeInvocation(Object object)的實(shí)現(xiàn)主要包括以下步驟:

          一、獲取需訪問(wèn)的接口權(quán)限,這里debug的例子是調(diào)用了前文提到的“/save”接口,其權(quán)限設(shè)置是@PreAuthorize("hasAuthority('sys:user:add') AND hasAuthority('sys:user:edit')"),根據(jù)下面截圖,可知變量attributes獲取了到該請(qǐng)求接口的權(quán)限:

          二、獲取認(rèn)證通過(guò)之后保存在 SecurityContextHolder的用戶信息,其中,authorities是一個(gè)保存用戶所擁有全部權(quán)限的集合;

          這里authenticateIfRequired()方法核心實(shí)現(xiàn):

            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 }

           在認(rèn)證過(guò)程通過(guò)后,執(zhí)行SecurityContextHolder.getContext().setAuthentication(authentication)將用戶信息保存在Security框架當(dāng)中,之后可通過(guò)SecurityContextHolder.getContext().getAuthentication()獲取到保存的用戶信息; 

          三、嘗試授權(quán),用戶信息authenticated、請(qǐng)求攜帶對(duì)象信息object、所訪問(wèn)接口的權(quán)限信息attributes,傳入到decide方法;

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

            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是一個(gè)interface接口,這是授權(quán)體系的核心。FilterSecurityInterceptor 在鑒權(quán)時(shí),就是通過(guò)調(diào)用AccessDecisionManager的decide()方法來(lái)進(jìn)行授權(quán)決策,若能通過(guò),則可訪問(wèn)對(duì)應(yīng)的接口。

          AccessDecisionManager類的方法具體實(shí)現(xiàn)都在子類當(dāng)中,包含AffirmativeBased、ConsensusBased、UnanimousBased三個(gè)子類;

          AffirmativeBased表示一票通過(guò),這是AccessDecisionManager默認(rèn)類;

          ConsensusBased表示少數(shù)服從多數(shù);

          UnanimousBased表示一票反對(duì);

          如何理解這個(gè)投票機(jī)制呢?

          點(diǎn)進(jìn)去AffirmativeBased類里,可以看到里面有一行代碼int result = voter.vote(authentication, object, configAttributes):

          這里的AccessDecisionVoter是一個(gè)投票器,用到委托設(shè)計(jì)模式,即AffirmativeBased類會(huì)委托投票器進(jìn)行選舉,然后將選舉結(jié)果返回賦值給result,然后判斷result結(jié)果值,若為1,等于ACCESS_GRANTED值時(shí),則表示可一票通過(guò),也就是,允許訪問(wèn)該接口的權(quán)限。

          這里,ACCESS_GRANTED表示同意、ACCESS_DENIED表示拒絕、ACCESS_ABSTAIN表示棄權(quán):

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

          那么,什么情況下,投票結(jié)果result為1呢?

          這里需要研究一下投票器接口AccessDecisionVoter,該接口的實(shí)現(xiàn)如下圖所示:

          這里簡(jiǎn)單介紹兩個(gè)常用的:

          1. RoleVoter:這是用來(lái)判斷url請(qǐng)求是否具備接口需要的角色,這種主要用于使用注解@Secured處理的權(quán)限;
          2. PreInvocationAuthorizationAdviceVoter:針對(duì)類似注解@PreAuthorize("hasAuthority('sys:user:add') AND hasAuthority('sys:user:edit')")處理的權(quán)限;

          到這一步,代碼就開(kāi)始難懂了,這部分封裝地過(guò)于復(fù)雜,總體的邏輯,是將用戶信息所具有的權(quán)限與該接口的權(quán)限表達(dá)式做匹配,若能匹配成功,返回true,在三目運(yùn)算符中,

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





          鋒哥最新SpringCloud分布式電商秒殺課程發(fā)布

          ??????

          ??長(zhǎng)按上方微信二維碼 2 秒





          感謝點(diǎn)贊支持下哈 

          瀏覽 47
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美嫩交| 欧美午夜精品一区二区三区 | 黄色电影一区 | 亚洲日韩中文字幕在线 | 麻豆亚洲AV成人无码一区精品 |