FilterSecurityInterceptor
簡介
Spring Security對於權限的控制有2種方式,1.通過ExpressionInterceptUrlRegistry進行配置,2.通過註解和切面的方式。FilterSecurityInterceptor是針對於第一種方式權限配置的控制機制,在項目或服務啓動時,Spring Security會把ExpressionInterceptUrlRegistry中配置的權限控制規則轉換成SecurityMetadataSource,客戶端或者瀏覽器發起請求時,會經過FilterSecurityInterceptor過濾器,過濾器根據SecurityMetadataSource進行權限校驗。第二種方式是通過且AOP切面實現的,與FilterSecurityInterceptor無關,因爲同爲權限控制,所以這裏附帶分析一下。
代碼分析
配置鑑權
步驟1:配置鑑權
筆者項目主要用與配置swagger以及feign api白名單,考慮到白名單變更的頻率也比較高,進行硬編碼也不太合適。於是寫了個SpringBootAutoConfiguration,將白名單配置到配置文件中修改起來也比較方便,筆者這裏只是配置白名單URL進行放行,用了permitAll(),還有其他權限校驗方法,比如hasRole(),hasAnyRole()等等,可以根據自己項目情況進行硬編碼設置,配置如下:
carp:
security:
oauth2:
resource:
enable: true
whitelabel-clients: carp
whitelabel-urls:
#內部接口加密但不攔截
- /feign/**
#swagger接口
- /doc/api/**
- /swagger-ui.html
- /swagger-ui.html**
- /webjars/**
- /swagger-resources/**
- /v2/api-docs/**
public abstract class AbstractResourceServerConfig extends ResourceServerConfigurerAdapter {
//注入配置文件中配置的屬性
@Resource
protected CarpAuthResourceProperties carpAuthResourceProperties;
@Override
public void configure(HttpSecurity http) throws Exception {
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http
.authorizeRequests();
//是否開啓資源權限校驗
if (!carpAuthResourceProperties.getEnable()) {
registry.anyRequest().permitAll();
return;
}
if (carpAuthResourceProperties != null && carpAuthResourceProperties.getWhitelabelUrls().size() > 0) {
//白名單URL放行
carpAuthResourceProperties.getWhitelabelUrls().forEach(s -> registry.antMatchers(s).permitAll());
}
registry.anyRequest().authenticated();
}
}
步驟2:鑑權攔截處理
FilterSecurityInterceptor的鑑權邏輯集中在父類AbstractSecurityInterceptor#beforeInvocation()方法中,對於FilterSecurityInterceptor而言,是針對請求url的權限攔截和鑑權,所以Collection attributes取到也是針對請求路徑的EL表達式鑑權配置,因此accessDecisionManager.decide()這裏進行的投票也是基於請求路徑進行規則匹配來進行投票,beforeInvocation()的大致邏輯,代碼如下:
//權限規則全局變量
private FilterInvocationSecurityMetadataSource securityMetadataSource;
//通過Set注入配置的權限過濾規則
public void setSecurityMetadataSource(FilterInvocationSecurityMetadataSource newSource) {
this.securityMetadataSource = newSource;
}
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
//chain是Servlet原生容器的過濾鏈,並非Spring Security中的過濾鏈
FilterInvocation fi = new FilterInvocation(request, response, chain);
//執行過濾的核心規則
invoke(fi);
}
public void invoke(FilterInvocation fi) throws IOException, ServletException {
if ((fi.getRequest() != null)
&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
&& observeOncePerRequest) {
//同一次請求中,過濾器已經執行過,直接進入原生過濾鏈
//FilterSecurityInterceptor是Spring Security過濾鏈的最後一個,所以要進入Servlet原生的過濾前
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
else {
if (fi.getRequest() != null && observeOncePerRequest) {
//打一個過濾器已執行標識
fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}
//原生servlet過濾鏈執行後進行權限規則校驗
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
finally {
//還原SpringSecurity上下文
super.finallyInvocation(token);
}
//後置處理,不做深入研究
super.afterInvocation(token, null);
}
}
protected InterceptorStatusToken beforeInvocation(Object object) {
Assert.notNull(object, "Object was null");
final boolean debug = logger.isDebugEnabled();
//對於FilterSecurityInterceptor SecureObjectClass 是FilterInvocation.class,判斷是否是合法的Invocation
if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
throw new IllegalArgumentException(
"Security invocation attempted for object "
+ object.getClass().getName()
+ " but AbstractSecurityInterceptor only configured to support secure objects of type: "
+ getSecureObjectClass());
}
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
.getAttributes(object);
//沒有配置任何權限過濾規則時,是否允許訪問接口
if (attributes == null || attributes.isEmpty()) {
if (rejectPublicInvocations) {
throw new IllegalArgumentException(
"Secure object invocation "
+ object
+ " was denied as public invocations are not allowed via this interceptor. "
+ "This indicates a configuration error because the "
+ "rejectPublicInvocations property is set to 'true'");
}
publishEvent(new PublicInvocationEvent(object));
return null; // no further work post-invocation
}
if (SecurityContextHolder.getContext().getAuthentication() == null) {
//身份認證信息爲空,發送對應事件,並拋出AuthenticationCredentialsNotFoundException異常
credentialsNotFound(messages.getMessage(
"AbstractSecurityInterceptor.authenticationNotFound",
"An Authentication object was not found in the SecurityContext"),
object, attributes);
}
//如果必要,則進行身份認證
Authentication authenticated = authenticateIfRequired();
// Attempt authorization
try {
//訪問投票決定是否可以訪問
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException accessDeniedException) {
publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
accessDeniedException));
throw accessDeniedException;
}
//發送授權事件
if (publishAuthorizationSuccess) {
publishEvent(new AuthorizedEvent(object, attributes, authenticated));
}
// Attempt to run as a different user
// 當attributes有屬性值以RUN_AS_XXX開頭時,buildRunAs會解析爲ROLE_XXX,重新根據權限生成新的身份認證信息
Authentication runAs = this.runAsManager.buildRunAs(authenticated, object,
attributes);
if (runAs == null) {
//包裝成一個複合對象
return new InterceptorStatusToken(SecurityContextHolder.getContext(), false,
attributes, object);
}
else {
//一下4行代碼,1:取出舊的上下文 2:創建一個空的上下文 3:將新生成的RunAs身份認證信息放到信息的上下文中 4:將舊的上下文放到複合對象
SecurityContext origCtx = SecurityContextHolder.getContext();
SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
SecurityContextHolder.getContext().setAuthentication(runAs);
//包裝成一個複合對象,由於上下被替換,當前放入的是舊上下文,故contextHolderRefreshRequired這裏爲true
return new InterceptorStatusToken(origCtx, true, attributes, object);
}
}
this.obtainSecurityMetadataSource().getAttributes(object)這行代碼直觀可能會疑惑,這裏就是把Request請求的取出,找出與請求路徑能夠匹配的ConfigAttribute,通過Debug,觀察變量值可能更直觀,代碼和截圖如下:
public Collection<ConfigAttribute> getAttributes(Object object) {
final HttpServletRequest request = ((FilterInvocation) object).getRequest();
for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap
.entrySet()) {
if (entry.getKey().matches(request)) {
return entry.getValue();
}
}
return null;
}
Authentication authenticated = authenticateIfRequired()是取出上下文中的身份認證信息,判斷是否進行必要驗證,代碼解析註釋到代碼當中了,如下:
private Authentication authenticateIfRequired() {
Authentication authentication = SecurityContextHolder.getContext()
.getAuthentication();
//一般 1:未登錄的用戶,匿名過濾器會生成一個匿名Token 2:登錄過的用戶會有一個Bearer Token
//在筆者的配置中,尚未執行到authenticationManager.authenticate(authentication);就return了
if (authentication.isAuthenticated() && !alwaysReauthenticate) {
return authentication;
}
authentication = authenticationManager.authenticate(authentication);
SecurityContextHolder.getContext().setAuthentication(authentication);
return authentication;
}
緊接着就是非常重要的accessDecisionManager.decide()方法,這裏的accessDecisionManager是一個AffirmativeBased實例,投票有3種結果ACCESS_GRANTED(授權)、ACCESS_ABSTAIN(棄權)、ACCESS_DENIED(拒絕),投票規則爲:所有的投票器進行一輪投票,一旦有一票通過則授權通過,棄權票作廢,全部拒絕則授權失敗。對於FilterSecurityInterceptor,AffirmativeBased持有的是一個WebExpressionVoter關鍵代碼如下:
public void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
int deny = 0;
for (AccessDecisionVoter voter : getDecisionVoters()) {
int result = voter.vote(authentication, object, configAttributes);
switch (result) {
//一旦有一個授權則授權成功
case AccessDecisionVoter.ACCESS_GRANTED:
return;
case AccessDecisionVoter.ACCESS_DENIED:
deny++;
break;
default:
break;
}
}
if (deny > 0) {
throw new AccessDeniedException(messages.getMessage(
"AbstractAccessDecisionManager.accessDenied", "Access is denied"));
}
//To get this far, every AccessDecisionVoter abstained
//deny=0情況可能有:1. 沒有投票器 2. 所有投票器棄權
//這裏就是判斷是否允許全部棄權的
checkAllowIfAllAbstainDecisions();
}
那麼decisionVoters是什麼時候注入的?我們在AbstractAccessDecisionManager構造方法方法上打個短斷點,執行堆棧和創建decisionManager的代碼如下:
@Override
@SuppressWarnings("rawtypes")
List<AccessDecisionVoter<?>> getDecisionVoters(H http) {
List<AccessDecisionVoter<?>> decisionVoters = new ArrayList<>();
WebExpressionVoter expressionVoter = new WebExpressionVoter();
expressionVoter.setExpressionHandler(getExpressionHandler(http));
decisionVoters.add(expressionVoter);
return decisionVoters;
}
至於RunAsManager,代碼中也做簡要的註釋,筆者對這部分代碼還沒有較深入的瞭解和實踐,不作過多說明。
FilterSecurityInterceptor是Spring Security過濾器中最後一個,執行beforeInvocation()後就要執行servlet中的過濾器了,截圖比較直觀,如下:
finallyInvocation()代碼比較簡單,當在beforeInvocation()中RunAsManager試圖模擬另外一個用戶身份創建新的身份認證信息成功時,contextHolderRefreshRequired=true,在執行finallyInvocation()就能進入if分支,將原來的認證信息給還原,代碼如下:
protected void finallyInvocation(InterceptorStatusToken token) {
//上下文被RunAsManager替換過,則進行恢復
if (token != null && token.isContextHolderRefreshRequired()) {
if (logger.isDebugEnabled()) {
logger.debug("Reverting to original Authentication: "
+ token.getSecurityContext().getAuthentication());
}
SecurityContextHolder.setContext(token.getSecurityContext());
}
}
afterInvocation()查了一些資料,是通過afterInvocationManager改變返回值的,但是有一點疑問,super.afterInvocation(token, null),這裏默認傳入了一個null,所以在invoke()中沒有任何地方引用,所以有點讓人費解,筆者大致看了一下afterInvocationManager的代碼,沒有深入研究,猜測afterInvocation()傳入的null,應該是一個默認值,對於FilterSecurityInterceptor返回值void,可能無需更改,但對於MethodSecurityInterceptor是Aop實現的,每個方法都有可能有返回值,估計是針對MethodSecurityInterceptor實現的。這裏不做深入研究了。
2. 註解式鑑權
步驟1:配置
註解式鑑權是Aop切面的機制,FilterSecurityInterceptor對其不能進行控制,因此二者關係並不大。其主要的實現類爲AspectJMethodSecurityInterceptor->MethodSecurityInterceptor->AbstractSecurityInterceptor,按箭頭方向依次繼承,使用註解式鑑權,開啓配置以及實例如下:
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Configuration
public class WebSecurityConfig {
}
@PreAuthorize("hasRole('ADMIN')")
@ApiOperation("異步獲取上下文用戶信息")
@GetMapping("/security/async")
public R<Object> asyncGetContextUser() throws Exception {
}
@PreAuthorize("hasAnyRole('ROOT','ADMIN')")
@ApiOperation("獲取上下文用戶信息")
@GetMapping("/security/sync")
public R<Object> getContextUser() {
}
步驟2:註解使用方式
securedEnabled
@EnableGlobalMethodSecurity(securedEnabled=true) 開啓@Secured 註解過濾權限
jsr250Enabled
@EnableGlobalMethodSecurity(jsr250Enabled=true)開啓@RolesAllowed 註解過濾權限
prePostEnabled
@PreAuthorize 在方法調用之前,基於表達式的計算結果來限制對方法的訪問
@PostAuthorize 允許方法調用,但是如果表達式計算結果爲false,將拋出一個安全性異常
@PostFilter 允許方法調用,但必須按照表達式來過濾方法的結果
@PreFilter 允許方法調用,但必須在進入方法之前過濾輸入值
接下來我們分析MethodSecurityInterceptor的相關代碼,代碼如下:
public Object invoke(MethodInvocation mi) throws Throwable {
InterceptorStatusToken token = super.beforeInvocation(mi);
Object result;
try {
result = mi.proceed();
}
finally {
super.finallyInvocation(token);
}
//可以在此處對Aop的返回結果進行修改
return super.afterInvocation(token, result);
}
步驟3:源碼分析
MethodSecurityInterceptor實現方式不同,beforeInvocation()中,obtainSecurityMetadataSource().getAttributes(object)這行代代碼與FilterSecurityInterceptor進行的操作也很不一樣,MethodSecurityInterceptor是通過反射,取到方法上的註解,然後包裝成ConfigAttribute,截圖和代碼如下:
public final Collection<ConfigAttribute> getAttributes(Object object) {
if (object instanceof MethodInvocation) {
MethodInvocation mi = (MethodInvocation) object;
Object target = mi.getThis();
Class<?> targetClass = null;
if (target != null) {
targetClass = target instanceof Class<?> ? (Class<?>) target
: AopProxyUtils.ultimateTargetClass(target);
}
Collection<ConfigAttribute> attrs = getAttributes(mi.getMethod(), targetClass);
if (attrs != null && !attrs.isEmpty()) {
return attrs;
}
if (target != null && !(target instanceof Class<?>)) {
attrs = getAttributes(mi.getMethod(), target.getClass());
}
return attrs;
}
同樣,this.accessDecisionManager.decide(authenticated, object, attributes);這行代碼也很不一樣,accessDecisionManager包含了三個投票器,如下:
- PreInvocationAuthorizationAdviceVoter
該投票器通過解析傳入attibutes,根據SpEL表達式判斷用戶的權限是否滿足條件,進行投票。
- RoleVoter
該投票器先判斷ConfigAttribute的屬性值是否以ROLE_前綴開頭,如果滿足條件,則進行權限比對,比對成功則投票通過。但是PreInvocationAttribute的getAttribute()始終返回null,因此PreInvocationAttribute不可能認證通過。
- AuthenticatedVoter
該投票器先判斷ConfigAttribute的屬性值是IS_AUTHENTICATED_FULLY、IS_AUTHENTICATED_REMEMBERED、IS_AUTHENTICATED_ANONYMOUSLY中哪一種認證方式,匹配到其中一種,則採用對應的方式進行認證,匹配不到則投棄權票。
這3種投票器是什麼時候注入的?通過debug,我們發現是在GlobalMethodSecurityConfiguration#accessDecisionManager()硬編碼寫入的,調用堆棧和代碼如下:
protected AccessDecisionManager accessDecisionManager() {
List<AccessDecisionVoter<?>> decisionVoters = new ArrayList<>();
if (prePostEnabled()) {
ExpressionBasedPreInvocationAdvice expressionAdvice =
new ExpressionBasedPreInvocationAdvice();
expressionAdvice.setExpressionHandler(getExpressionHandler());
decisionVoters
.add(new PreInvocationAuthorizationAdviceVoter(expressionAdvice));
}
if (jsr250Enabled()) {
decisionVoters.add(new Jsr250Voter());
}
RoleVoter roleVoter = new RoleVoter();
GrantedAuthorityDefaults grantedAuthorityDefaults =
getSingleBeanOrNull(GrantedAuthorityDefaults.class);
if (grantedAuthorityDefaults != null) {
roleVoter.setRolePrefix(grantedAuthorityDefaults.getRolePrefix());
}
decisionVoters.add(roleVoter);
decisionVoters.add(new AuthenticatedVoter());
return new AffirmativeBased(decisionVoters);
}