一個接口是如何在Keycloak和Spring Security之間執(zhí)行的

在上一篇我們對Keycloak的常用配置進行了熟悉,今天我們來對Keycloak適配Spring Security的執(zhí)行流程做一個分析,簡單了解一下其定制的一些Spring Security過濾器。
/admin/foo的執(zhí)行流程
在適配了Keycloak和Spring Security的Spring Boot應(yīng)用中,我編寫了一個/admin/foo的接口并對這個接口進行了權(quán)限配置:
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http
.authorizeRequests()
.antMatchers("/customers*").hasRole("USER")
.antMatchers("/admin/**").hasRole("base_user")
.anyRequest().permitAll();
}
這是典型的Spring Security配置,擁有base_user角色的用戶有權(quán)限訪問/admin/**。這里需要大家明白的是所謂的用戶和base_user角色目前都由Keycloak平臺管理,而我們的應(yīng)用目前只能控制資源的訪問策略。為了探明執(zhí)行的流程我開啟了所有的日志打印,當(dāng)我訪問/admin/foo時經(jīng)過了以下過濾器:
Security filter chain: [
WebAsyncManagerIntegrationFilter
SecurityContextPersistenceFilter
HeaderWriterFilter
CsrfFilter
KeycloakPreAuthActionsFilter
KeycloakAuthenticationProcessingFilter
LogoutFilter
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
KeycloakSecurityContextRequestFilter
KeycloakAuthenticatedActionsFilter
AnonymousAuthenticationFilter
SessionManagementFilter
ExceptionTranslationFilter
FilterSecurityInterceptor
]
這里除了Spring Security常規(guī)的內(nèi)置過濾器外還加入了Keycloak適配器的幾個過濾器,結(jié)合執(zhí)行流程來認(rèn)識一下它們。
KeycloakPreAuthActionsFilter
這個過濾器的作用是暴露一個Keycloak適配器對象PreAuthActionsHandler給Spring Security。而這個適配器的作用就是攔截處理一個Keycloak的職能請求處理接口,這些內(nèi)置接口都有特定的后綴:
// 退出端點
public static final String K_LOGOUT = "k_logout";
// 重置什么公鑰的?
public static final String K_PUSH_NOT_BEFORE = "k_push_not_before";
// 測試用的
public static final String K_TEST_AVAILABLE = "k_test_available";
// 獲取 jwk 相關(guān)的
public static final String K_JWKS = "k_jwks";
?
一般不深入底層可以不管這個過濾器。
KeycloakAuthenticationEntryPoint
?
KeycloakAuthenticationEntryPoint是AuthenticationEntryPoint的實現(xiàn),配置于KeycloakWebSecurityConfigurerAdapter。
當(dāng)請求被過濾器FilterSecurityInterceptor時發(fā)現(xiàn)當(dāng)前的用戶是個匿名用戶,不符合/admin/foo的訪問控制要求而拋出了AccessDeniedException。會通過ExceptionTranslationFilter傳遞給KeycloakAuthenticationEntryPoint處理401異常。
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
HttpFacade facade = new SimpleHttpFacade(request, response);
if (apiRequestMatcher.matches(request) || adapterDeploymentContext.resolveDeployment(facade).isBearerOnly()) {
commenceUnauthorizedResponse(request, response);
} else {
commenceLoginRedirect(request, response);
}
}
它執(zhí)行了兩種策略:
當(dāng)請求時登錄請求 /sso/login或者是BearerOnly(這些屬性上一篇可介紹了一部分哦)就直接返回標(biāo)頭含WWW-Authenticate的401響應(yīng)。其它情況就跳一個OIDC認(rèn)證授權(quán)請求。
protected void commenceLoginRedirect(HttpServletRequest request, HttpServletResponse response) throws IOException {
if (request.getSession(false) == null && KeycloakCookieBasedRedirect.getRedirectUrlFromCookie(request) == null) {
// If no session exists yet at this point, then apparently the redirect URL is not
// stored in a session. We'll store it in a cookie instead.
response.addCookie(KeycloakCookieBasedRedirect.createCookieFromRedirectUrl(request.getRequestURI()));
}
String queryParameters = "";
if (!StringUtils.isEmpty(request.getQueryString())) {
queryParameters = "?" + request.getQueryString();
}
String contextAwareLoginUri = request.getContextPath() + loginUri + queryParameters;
log.debug("Redirecting to login URI {}", contextAwareLoginUri);
response.sendRedirect(contextAwareLoginUri);
}
我們的接口明顯走的上面的方法,很明顯要跳登錄頁了。這時需要看看/admin/foo有沒有緩存起來,因為登錄完還要去執(zhí)行/admin/foo的邏輯。如果Spring Security沒有存Session或者Cookie中也沒有就會把/admin/foo緩存到Cookie中,然后重定向到Keycloak授權(quán)頁:
http://localhost:8011/auth/realms/felord.cn/protocol/openid-connect/auth?response_type=code&client_id=CLIENT&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fsso%2Flogin&state=STATE&login=true&scope=openid
KeycloakAuthenticationProcessingFilter
上面是一個典型的 Authorization Code Flow模式。當(dāng)輸入帳號密碼同意授權(quán)時,授權(quán)服務(wù)器會請求一個攜帶code和state的回調(diào)鏈接(這里是/sso/login)。負(fù)責(zé)攔截處理/sso/login的是KeycloakAuthenticationProcessingFilter。這個接口不單單處理登錄,只要攜帶了授權(quán)頭Authorization、access_token、Keycloak Cookie三種之一的它都會攔截處理。
在這個過濾器和我們熟悉的UsernamePasswordAuthenticationFilter一樣都繼承了AbstractAuthenticationProcessingFilter其實大致流程也很相似,只不過走的是Keycloak認(rèn)證授權(quán)的API。認(rèn)證授權(quán)成功就從Session中重新獲取/admin/foo接口并跳轉(zhuǎn)。整個簡單的Keycloak認(rèn)證授權(quán)過程就完成了。
KeycloakSecurityContextRequestFilter
這個過濾器功能比較單一,它是用來判斷是不是RefreshableKeycloakSecurityContext,可刷新的安全上下文,如果是就在ServletRequest對象中放個RefreshableKeycloakSecurityContext,后續(xù)其它過濾器會根據(jù)這個標(biāo)記做一些事情。
KeycloakAuthenticatedActionsFilter
這個過濾器就是用來捕捉KeycloakSecurityContextRequestFilter放在請求對象ServletRequest中的RefreshableKeycloakSecurityContext的。核心就這些:
public boolean handledRequest() {
log.debugv("AuthenticatedActionsValve.invoke {0}", facade.getRequest().getURI());
if (corsRequest()) return true;
String requestUri = facade.getRequest().getURI();
if (requestUri.endsWith(AdapterConstants.K_QUERY_BEARER_TOKEN)) {
queryBearerToken();
return true;
}
if (!isAuthorized()) {
return true;
}
return false;
}
這里返回true就阻斷不往下走了。主要是根據(jù)Keycloak提供的策略來判斷是否已經(jīng)授權(quán),看上去邏輯還挺復(fù)雜的。
?
基于篇幅的原理,我們后續(xù)再詳細(xì)介紹Keycloak的過濾器,今天我們先知道大致它們都干什么用的。
補充
其實要想搞清楚任何一個框架的運行流程,最好的辦法就是從日志打印中提煉一些關(guān)鍵點。Keycloak Spring Security Adapter的運行流程如果你想搞清楚,最好是自己先試一試。我把開啟Keycloak適配器的注解拆解開以打開Spring Security的日志:
@Configuration
@ComponentScan(
basePackageClasses = {KeycloakSecurityComponents.class}
)
@EnableWebSecurity(debug = true)
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
//ignore
}
為了看到更多日志,把Spring Boot的org相關(guān)包的日志也調(diào)整為debug:
logging:
level:
org : debug
然后代碼運行的流程會在控制臺Console非常清晰,極大方便了我弄清楚Keycloak的運行流程。Keycloak的流程簡單了解一下就好,感覺非常平淡無奇,大部分也沒有定制化的需要,個人覺得重心其實不在這里,如何根據(jù)業(yè)務(wù)定制Keycloak的用戶管理、角色管理等一系列管理API才是使用好它的關(guān)鍵。不要走開,后續(xù)會結(jié)合一些場景來魔改keycloak。
往期推薦
