系列導航
SpringSecurity系列
- SpringSecurity系列學習(一):初識SpringSecurity
- SpringSecurity系列學習(二):密碼驗證
- SpringSecurity系列學習(三):認證流程和源碼解析
- SpringSecurity系列學習(四):基於JWT的認證
- SpringSecurity系列學習(四-番外):多因子驗證和TOTP
- SpringSecurity系列學習(五):授權流程和源碼分析
- SpringSecurity系列學習(六):基於RBAC的授權
SpringSecurityOauth2系列
- SpringSecurityOauth2系列學習(一):初認Oauth2
- SpringSecurityOauth2系列學習(二):授權服務
- SpringSecurityOauth2系列學習(三):資源服務
- SpringSecurityOauth2系列學習(四):自定義登陸登出接口
- SpringSecurityOauth2系列學習(五):授權服務自定義異常處理
授權
看到標題了吧?這一節咱們不上號哈,先從原理入手!減少踩坑的概率!
上一節我們完成了認證的流程,接下來我們來談一談授權
在正式開始之前,先給大家提個醒,授權這個東西,相比起認證,其實更偏業務一點,在技術上不難,關鍵是業務設計。這里面是個大學問,每一個項目的權限設計都不同,怎么設計好用戶角色權限的關系(我劇透了?)其實才是最難的點。
什么是授權
根據用戶的權限來控制用戶使用資源的過程就是授權
用微信來舉例子,微信登錄成功后用戶即可使用微信的功能,比如,發紅包、發朋友圈、添加好友等,沒有綁定銀行卡的用戶是無法發送紅包的,綁定銀行卡的用戶才可以發紅包。
發紅包功能、發朋友圈功能都是微信的資源即功能資源
,用戶擁有發紅包功能的權限才可以正常使用發送紅包功能,擁有發朋友圈功能的權限才可以使用發朋友圈功能,這個根據用戶的權限來控制用戶使用資源的過程就是授權。
為什么要授權?
認證是為了保證用戶身份的合法性,授權則是為了更細粒度的對隱私數據進行划分,授權是在認證通過后發生的, 控制不同的用戶能夠訪問不同的資源。
授權是用戶認證通過根據用戶的權限來控制用戶訪問資源的過程,擁有資源的訪問權限則正常訪問,沒有權限則拒絕訪問。
SpringSecurity中的授權
AbstractAccessDecisionManager
根據相關信息,做出授權決定
這個類中有一個decide(Object)
的方法,接收Object
類型的參數,是一個安全對象。其安全對象具體是什么,SpringSecurity並沒有去嚴格的限制它。其檢查邏輯需要去自定義
基於投票的AccessDecisionManager實現
AccessDecisionManager
對一組AccessDecisionVoter
(投票器)實現進行輪詢授權決定(和之前我們學習認證的時候,去輪詢AuthenticationProvider
是一樣的)。然后AccessDecisionManager
根據對投票的評估,決定是否拋出一個AccessDeniedException
異常。
三種投票策略
ConsensusBased
多數服從少數AffirmaticeBased
有一票就可以通過UnanimousBased
需要全票才能通過(默認采取的策略)
AccessDecisionVoter的一種實現:RoleVoter
如果有任何的ConfigAttribute
是以ROLE_
開頭的,它就會進行投票
如果GrantedAuthority
(權限列表)中有一個或者多個以ROLE_
開頭的角色能夠匹配上ConfigAttribute
中設置的角色,這個投票器就投票授予訪問權限
如果沒有任何GrantedAuthority
返回的字符串與角色字符串相匹配,它就會投票拒絕訪問
如果沒有ConfigAttribute
以ROLE_
開頭的角色,那么就放棄投票
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")
這個類圖可以表示:AccessDecisionManager
對AccessDecisionVoter
進行輪詢。
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,即同意授權,則就授予權限。