Java Spring Security
对Keycloak适配Spring Security的执行流程做一个分析,简单了解一下其定制的一些Spring Security过滤器。

/admin/foo的执行流程

在适配了Keycloak和Spring Security的Spring Boot应用中,编写了一个/admin/foo的接口并对这个接口进行了权限配置:

  1. @Override
  2. protected void configure(HttpSecurity http) throws Exception {
  3. super.configure(http);
  4. http
  5. .authorizeRequests()
  6. .antMatchers("/customers*").hasRole("USER")
  7. .antMatchers("/admin/**").hasRole("base_user")
  8. .anyRequest().permitAll();
  9. }

这是典型的Spring Security配置,拥有base_user角色的用户有权限访问/admin/**。这里需要大家明白的是所谓的用户和base_user角色目前都由Keycloak平台管理,而应用目前只能控制资源的访问策略。为了探明执行的流程开启了所有的日志打印,当访问/admin/foo时经过了以下过滤器:

  1. Security filter chain: [
  2. WebAsyncManagerIntegrationFilter
  3. SecurityContextPersistenceFilter
  4. HeaderWriterFilter
  5. CsrfFilter
  6. KeycloakPreAuthActionsFilter
  7. KeycloakAuthenticationProcessingFilter
  8. LogoutFilter
  9. RequestCacheAwareFilter
  10. SecurityContextHolderAwareRequestFilter
  11. KeycloakSecurityContextRequestFilter
  12. KeycloakAuthenticatedActionsFilter
  13. AnonymousAuthenticationFilter
  14. SessionManagementFilter
  15. ExceptionTranslationFilter
  16. FilterSecurityInterceptor
  17. ]

这里除了Spring Security常规的内置过滤器外还加入了Keycloak适配器的几个过滤器,结合执行流程来认识一下它们。

KeycloakPreAuthActionsFilter

这个过滤器的作用是暴露一个Keycloak适配器对象PreAuthActionsHandler给Spring Security。而这个适配器的作用就是拦截处理一个Keycloak的职能请求处理接口,这些内置接口都有特定的后缀:

  1. // 退出端点
  2. public static final String K_LOGOUT = "k_logout";
  3. // 重置什么公钥的?
  4. public static final String K_PUSH_NOT_BEFORE = "k_push_not_before";
  5. // 测试用的
  6. public static final String K_TEST_AVAILABLE = "k_test_available";
  7. // 获取 jwk 相关的
  8. public static final String K_JWKS = "k_jwks";

一般不深入底层可以不管这个过滤器。

KeycloakAuthenticationEntryPoint

KeycloakAuthenticationEntryPointAuthenticationEntryPoint的实现,配置于KeycloakWebSecurityConfigurerAdapter
当请求被过滤器FilterSecurityInterceptor时发现当前的用户是个匿名用户,不符合/admin/foo的访问控制要求而抛出了AccessDeniedException。会通过ExceptionTranslationFilter传递给KeycloakAuthenticationEntryPoint处理401异常。

  1. @Override
  2. public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
  3. HttpFacade facade = new SimpleHttpFacade(request, response);
  4. if (apiRequestMatcher.matches(request) || adapterDeploymentContext.resolveDeployment(facade).isBearerOnly()) {
  5. commenceUnauthorizedResponse(request, response);
  6. } else {
  7. commenceLoginRedirect(request, response);
  8. }
  9. }

它执行了两种策略:

  • 当请求时登录请求/sso/login或者是BearerOnly(这些属性上一篇可介绍了一部分哦)就直接返回标头含WWW-Authenticate 的401响应。
  • 其它情况就跳一个OIDC认证授权请求。

    1. protected void commenceLoginRedirect(HttpServletRequest request, HttpServletResponse response) throws IOException {
    2. if (request.getSession(false) == null && KeycloakCookieBasedRedirect.getRedirectUrlFromCookie(request) == null) {
    3. // If no session exists yet at this point, then apparently the redirect URL is not
    4. // stored in a session. We'll store it in a cookie instead.
    5. response.addCookie(KeycloakCookieBasedRedirect.createCookieFromRedirectUrl(request.getRequestURI()));
    6. }
    7. String queryParameters = "";
    8. if (!StringUtils.isEmpty(request.getQueryString())) {
    9. queryParameters = "?" + request.getQueryString();
    10. }
    11. String contextAwareLoginUri = request.getContextPath() + loginUri + queryParameters;
    12. log.debug("Redirecting to login URI {}", contextAwareLoginUri);
    13. response.sendRedirect(contextAwareLoginUri);
    14. }

    接口明显走的上面的方法,很明显要跳登录页了。这时需要看看/admin/foo有没有缓存起来,因为登录完还要去执行/admin/foo的逻辑。如果Spring Security没有存Session或者Cookie中也没有就会把/admin/foo缓存到Cookie中,然后重定向到Keycloak授权页:
    http://localhost:8011/auth/realms/fcant.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模式。当输入帐号密码同意授权时,授权服务器会请求一个携带code和state的回调链接(这里是/sso/login)。负责拦截处理/sso/login的是KeycloakAuthenticationProcessingFilter。这个接口不单单处理登录,只要携带了授权头Authorization、access_token、Keycloak Cookie三种之一的它都会拦截处理。
    在这个过滤器和熟悉的UsernamePasswordAuthenticationFilter一样都继承了AbstractAuthenticationProcessingFilter其实大致流程也很相似,只不过走的是Keycloak认证授权的API。认证授权成功就从Session中重新获取/admin/foo接口并跳转。整个简单的Keycloak认证授权过程就完成了。

    KeycloakSecurityContextRequestFilter

    这个过滤器功能比较单一,它是用来判断是不是RefreshableKeycloakSecurityContext,可刷新的安全上下文,如果是就在ServletRequest对象中放个RefreshableKeycloakSecurityContext,后续其它过滤器会根据这个标记做一些事情。

    KeycloakAuthenticatedActionsFilter

    这个过滤器就是用来捕捉KeycloakSecurityContextRequestFilter放在请求对象ServletRequest中的RefreshableKeycloakSecurityContext的。核心就这些:

    1. public boolean handledRequest() {
    2. log.debugv("AuthenticatedActionsValve.invoke {0}", facade.getRequest().getURI());
    3. if (corsRequest()) return true;
    4. String requestUri = facade.getRequest().getURI();
    5. if (requestUri.endsWith(AdapterConstants.K_QUERY_BEARER_TOKEN)) {
    6. queryBearerToken();
    7. return true;
    8. }
    9. if (!isAuthorized()) {
    10. return true;
    11. }
    12. return false;
    13. }

    这里返回true就阻断不往下走了。主要是根据Keycloak提供的策略来判断是否已经授权,看上去逻辑还挺复杂的。
    基于篇幅的原理,后续再详细介绍Keycloak的过滤器,先知道大致它们都干什么用的。

    补充

    其实要想搞清楚任何一个框架的运行流程,最好的办法就是从日志打印中提炼一些关键点。Keycloak Spring Security Adapter的运行流程如果想搞清楚,最好是自己先试一试。把开启Keycloak适配器的注解拆解开以打开Spring Security的日志:

    1. @Configuration
    2. @ComponentScan(
    3. basePackageClasses = {KeycloakSecurityComponents.class}
    4. )
    5. @EnableWebSecurity(debug = true)
    6. public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
    7. //ignore
    8. }

    为了看到更多日志,把Spring Boot的org相关包的日志也调整为debug

    1. logging:
    2. level:
    3. org : debug

    然后代码运行的流程会在控制台Console非常清晰,极大方便了了解Keycloak的运行流程。Keycloak的流程简单了解一下就好,感觉非常平淡无奇,大部分也没有定制化的需要,重心其实不在这里,如何根据业务定制Keycloak的用户管理、角色管理等一系列管理API才是使用好它的关键。