SpringSecurity系列學習(五):授權流程和源碼分析


系列導航

SpringSecurity系列

SpringSecurityOauth2系列

授權

看到標題了吧?這一節咱們不上號哈,先從原理入手!減少踩坑的概率!

上一節我們完成了認證的流程,接下來我們來談一談授權

在正式開始之前,先給大家提個醒,授權這個東西,相比起認證,其實更偏業務一點,在技術上不難,關鍵是業務設計。這里面是個大學問,每一個項目的權限設計都不同,怎么設計好用戶角色權限的關系(我劇透了?)其實才是最難的點。

什么是授權

根據用戶的權限來控制用戶使用資源的過程就是授權

用微信來舉例子,微信登錄成功后用戶即可使用微信的功能,比如,發紅包、發朋友圈、添加好友等,沒有綁定銀行卡的用戶是無法發送紅包的,綁定銀行卡的用戶才可以發紅包。

發紅包功能、發朋友圈功能都是微信的資源即功能資源,用戶擁有發紅包功能的權限才可以正常使用發送紅包功能,擁有發朋友圈功能的權限才可以使用發朋友圈功能,這個根據用戶的權限來控制用戶使用資源的過程就是授權。

為什么要授權?

認證是為了保證用戶身份的合法性,授權則是為了更細粒度的對隱私數據進行划分,授權是在認證通過后發生的, 控制不同的用戶能夠訪問不同的資源。

授權是用戶認證通過根據用戶的權限來控制用戶訪問資源的過程,擁有資源的訪問權限則正常訪問,沒有權限則拒絕訪問。

SpringSecurity中的授權

AbstractAccessDecisionManager

根據相關信息,做出授權決定

這個類中有一個decide(Object)的方法,接收Object類型的參數,是一個安全對象。其安全對象具體是什么,SpringSecurity並沒有去嚴格的限制它。其檢查邏輯需要去自定義

基於投票的AccessDecisionManager實現

AccessDecisionManager對一組AccessDecisionVoter(投票器)實現進行輪詢授權決定(和之前我們學習認證的時候,去輪詢AuthenticationProvider是一樣的)。然后AccessDecisionManager根據對投票的評估,決定是否拋出一個AccessDeniedException異常。

三種投票策略

  • ConsensusBased 多數服從少數
  • AffirmaticeBased 有一票就可以通過
  • UnanimousBased 需要全票才能通過(默認采取的策略)

AccessDecisionVoter的一種實現:RoleVoter

如果有任何的ConfigAttribute是以ROLE_開頭的,它就會進行投票

如果GrantedAuthority(權限列表)中有一個或者多個以ROLE_開頭的角色能夠匹配上ConfigAttribute中設置的角色,這個投票器就投票授予訪問權限

如果沒有任何GrantedAuthority返回的字符串與角色字符串相匹配,它就會投票拒絕訪問

如果沒有ConfigAttributeROLE_開頭的角色,那么就放棄投票

ConfigAttribute是指配置的訪問資源需要的角色配置,比如:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests(req -> req
                        //訪問 /admin路徑下的請求 要有管理員權限
                        .antMatchers("/admin/**").hasRole("ADMIN")
                       ...
    }

這里的hasRole("ADMIN")就是一個ConfigAttribute,並且通過hasRole("ADMIN")設置的ConfigAttribute,都會自動加上前綴ROLE_

hasRole("ADMIN") = hasAuthority("ROLE_ADMIN")

這個類圖可以表示:AccessDecisionManagerAccessDecisionVoter進行輪詢。

RoleVoter就是一種AccessDecisionVoter

AuthenticatedVoter是另一種AccessDecisionVoter,當你認證成功,它就會投票授予訪問權限

安全表達式

安全表達式是SpringSecurity中非常重要,並且非常受歡迎的功能,能夠自定義安全策略並且將其獨立於業務代碼之外。

之前我們只是粗略地了解了一下安全表達式,現在咱們深入學習一下

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests(req -> req
                        .antMatchers("/authorize/**").permitAll()
                        //訪問 /admin路徑下的請求 要有管理員權限
                        .antMatchers("/admin/**").hasRole("ADMIN")
                        .antMatchers("/api/**").access("hasRole('ADMIN') or hasRole('USER')")
                       ...
    }

安全表達式的順序很重要,作用越廣泛的規則要放在最后,避免其他規則失效

類似permitAll的函數:

  • denyAll :拒絕用戶訪問
  • anonymous : 匿名用戶
  • rememberMe : 記住我用戶
  • authenticated : 已認證用戶
  • fullyAuthenticated: 既不是匿名用戶也不是記住我用戶

類似hasRole的函數:

  • hasAnyRole : 有其中一個角色即可
  • haeAuthority : hasRole("ADMIN") 等價於 haeAuthority("ROLE_ADMIN")
  • haeAnyAuthority : hasAnyRole("ADMIN","USER") 等價於haeAnyAuthority("ROLE_ADMIN","ROLE_USER")

復雜表達式應用

access:更復雜的表達式,支持SpEL表達式,可以引用HttpServletRequest中的屬性,也可以引用@Bean

現在這里我們有一個接口:

@GetMapping("/users/{username}")
public String getCurrentUserName(){
  ...
}

我們希望的是,只有用戶本人或者管理員才可以訪問:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests(req -> req
                        .antMatchers("/users/{username}")
                        //只有當前認證的用戶和管理員才能訪問  
                        .access("hasRole('ADMIN') or authentication.name.equals(#username)")
                        .anyRequest.denyAll()
                       ...
    }

這里我們直接引用了認證對象authentication和路徑參數username

如何去使用一個bean?

存在這樣一個bean

@Service
public class UserService{
  public boolean checkUsername(Authentication authentication,String username){
    ...
  }
}

使用@引用bean

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests(req -> req
                        .antMatchers("/users/{username}")
                        //只有當前認證的用戶和管理員才能訪問  
                        .access("@userService.checkUsername(authentication,#username)")
                        .anyRequest.denyAll()
                       ...
    }

方法級的安全表達式

安全表達式主要是作用於url的,當客戶端請求url的時候,去進行一個授權。

但是在更復雜的場景里面,我們還需要對方法進行一些限制

ps:方法級的授權和url的授權是不一樣的,url和方法級的授權是相互獨立的,都需要進行投票器的投票,即如果一個接口即設定了url級別的授權和方法級別的授權,那么會進行兩次授權投票,一次是url,一次是方法,並且如果有一個授權不通過,則拒絕訪問

配置

/**
 * `@EnableWebSecurity` 注解 deug參數為true時,開啟調試模式,會有更多的debug輸出
 * 啟用`@EnableGlobalMethodSecurity(prePostEnabled = true)`注解后即可使用方法級安全注解
 * 方法級安全注解:
 * pre : @PreAuthorize(執行方法之前授權)  @PreFilter(執行方法之前過濾)
 * post : @PostAuthorize (執行方法之后授權) @PostFilter(執行方法之后過濾)
 *
 * @author 硝酸銅
 * @date 2021/6/2
 */
@EnableWebSecurity(debug = true)
@Slf4j
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
  ...
}

ps:@PreAuthorize,@PreFilter,@PostAuthorize,@PostFilter都是通過注解形式實現的,也就是通過AOP來實現的,那么由於動態代理的原因,必須是從外部調用該方法才能生效,一個類調用自己的方法是沒用的,比如

/**
 * 匿名用戶
 * @author 硝酸銅
 * @date 2021/6/7
 */
@RestController
@RequestMapping("/authorize")
public class AuthorizeResource {
   ...
  
    @GetMapping(value = "/test")
    public String test(){
        return findAll();
    }
  
  	@PreAuthorize(value = "hasAuthority('ADMIN')")
    public String findAll(){
        return "All";
    }

}

test接口中調用同一個類中的方法,@PreAuthorize不會生效

pre

    @PreAuthorize(value = "hasAuthority('ADMIN')")
    @GetMapping(value = "/hello")
    public String getHello(){
        return "Hello ";
    }

在方法上加上注解即可,其用法和access相同

我們用只有USER角色的test賬號去訪問:

當然403被拒絕

只有有ADMIN角色的賬號才能訪問成功

post

方法調用后的安全注解,先執行方法,利用對返回的對象進行某種判斷,決定是否授權

@PostAuthorize("returnObject.username == authentication.name")
@GetMapping("/users/email/{email}")
public User getUserByEmail(@PathVariable String email){
  return userService.findByEmail(email);
}

這里的例子就是一種場景,根據email查詢用戶數據,並且只能返回當前認證的用戶的數據

因為authentication中是沒有email的,通過查詢得到用戶之后,才回去判斷查詢到的用戶是不是用戶本身,然后進行授權。

這種POST方式盡量少用,這種查詢還好,如果是存在數據變更操作的方法,那么就不推薦去使用了。因為這種授權方式是執行完方法之后才進行授權,即使沒有通過授權,方法里面的代碼已經被執行了,如果涉及數據變更,那么數據已經被更改了,有很大的安全隱患。

授權流程分析

我們來分析一下流程,為了區分url和方法級注解,我將@pre注解放在了Service里面

現在有這樣一個接口:

/**
 * 匿名用戶
 * @author 硝酸銅
 * @date 2021/6/7
 */
@RestController
@RequestMapping("/authorize")
public class AuthorizeResource {
   ...
     
    @Resource
    private UserDetailsServiceImpl userDetailsService;
  
    @GetMapping(value = "/test")
    public String test(){
        return userDetailsService.findAll();
    }

}

@Service
public class UserDetailsServiceImpl extends ServiceImpl<UserMapper, User> implements UserDetailsService {

  ...
    @PreAuthorize(value = "hasAuthority('ADMIN')")
    public String findAll(){
        return "All";
    }
}

這個接口沒用限制,並且在SecurityConfig中,我們設置/authorize/**的安全表達式為permitAll(),它調用了UserDetailsServiceImpl中一個只能ADMIN角色訪問的方法

不去登陸訪問這個接口:

/authorize/test接口授權過程:

url支持匿名訪問,在web url這一層WebExpressionVoter投票器已經給你投票通過了

並且這里用的策略是AffirmativeBased,表示只要有一票通過即可。

也就是說現在你可以訪問/authorize/test接口了,也就是說匿名用戶可以進入test()方法中

findAll()方法授權過程:

最上面顯示使用了方法級的攔截器

我們這里因為沒有登陸,所以是匿名用戶

然后PreInvocationAuthorizationAdviceVoter(注意,這里名字中有帶Advice說明是面向切面的方式進行攔截的)投票器一看,不是ADMIN角色,不滿足要求,投了拒絕票

RoleVoter一看,沒有設定ROLE_開頭的ConfigAttribute,直接投了棄權票

AuthenticatedVoter一看,匿名用戶,我直接棄權

並且這里用的策略也是AffirmativeBased,表示只要有一票通過即通過。

但是這里的投票器都沒有通過的票,所以最后拒絕訪問

授權源碼解析

流程分析清楚了,我們來看看授權的源碼

來分析一下,SpringSecurity是如何將安全表達式轉化為這么靈活的一個機制的?其內部的檢查流程是怎么樣的?

方法級安全表達式,pre前置安全表達式的投票器是PreInvocationAuthorizationAdviceVoter這個類,來看看其內部的部分核心代碼

public class PreInvocationAuthorizationAdviceVoter implements AccessDecisionVoter<MethodInvocation> {
   ...

    public int vote(Authentication authentication, MethodInvocation method, Collection<ConfigAttribute> attributes) {
     		//取出安全表達式
        PreInvocationAttribute preAttr = this.findPreInvocationAttribute(attributes);
        if (preAttr == null) {
            return 0;
        } else {
          
            //檢查是否允許授權,是則返回1
            return this.preAdvice.before(authentication, method, preAttr) ? 1 : -1;
        }
    }

    ...
}

這里的投票方法vote()邏輯為:首先取出安全表達式,然后判斷是否允許授權,允許則投贊成票

進入before()方法,其進入了一個叫做ExpressionBasedPreInvocationAdvice的類中,邏輯如下

public class ExpressionBasedPreInvocationAdvice implements PreInvocationAuthorizationAdvice {
    ...

    public boolean before(Authentication authentication, MethodInvocation mi, PreInvocationAttribute attr) {
        //同樣首先取出安全表達式
        PreInvocationExpressionAttribute preAttr = (PreInvocationExpressionAttribute)attr;
        EvaluationContext ctx = this.expressionHandler.createEvaluationContext(authentication, mi);
        //preFilter 設定的前置過濾器
        Expression preFilter = preAttr.getFilterExpression();
        //preAuthorize ,也就是設定的類似 hasRole('Admin')這樣的安全表達式
        Expression preAuthorize = preAttr.getAuthorizeExpression();
        if (preFilter != null) {
            Object filterTarget = this.findFilterTarget(preAttr.getFilterTarget(), ctx, mi);
            this.expressionHandler.filter(filterTarget, preFilter, ctx);
        }

        //判斷當前認證的用戶是否能夠通過安全表達式
        return preAuthorize != null ? ExpressionUtils.evaluateAsBoolean(preAuthorize, ctx) : true;
    }
  ...
}

關於安全表達式的解析和判斷,這里就不多做贅述了,有興趣的小伙伴們可以自行看看源碼哈

這里一個投票器的投票邏輯就結束了

之前說過,AccessDecisionManager對一組AccessDecisionVoter(投票器)實現進行輪詢授權決定,那么是怎么樣輪詢的呢?

如果你在上面投票的源代碼中打一個斷點,你就會發現,PreInvocationAuthorizationAdviceVoter.vote()方法結束后,來到了AffirmativeBased類的decide()方法中。

AffirmativeBased類就是AbstractAccessDecisionManager的一個具體實現,其源碼邏輯如下:

public class AffirmativeBased extends AbstractAccessDecisionManager {
    public AffirmativeBased(List<AccessDecisionVoter<?>> decisionVoters) {
        super(decisionVoters);
    }

    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
        int deny = 0;
        Iterator var5 = this.getDecisionVoters().iterator();

        //輪詢投票器
        while(var5.hasNext()) {
            AccessDecisionVoter voter = (AccessDecisionVoter)var5.next();
            //投票
            int result = voter.vote(authentication, object, configAttributes);
            switch(result) {
            case -1:
                ++deny;
                break;
            // 授予權限,投票1
            case 1:
                return;
            }
        }

        if (deny > 0) {
            throw new AccessDeniedException(this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied"));
        } else {
            this.checkAllowIfAllAbstainDecisions();
        }
    }
}

三種投票策略

  • ConsensusBased 多數服從少數
  • AffirmaticeBased 有一票就可以通過
  • UnanimousBased 需要全票才能通過(默認采取的策略)

這里的AffirmaticeBased是指有一票就可以通過,所以當一個投票器投票1,即同意授權,則就授予權限。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM