如何从 PermissionEvaluator 内部访问 Keycloak 的 AuthorizationContext?

How to access Keycloak's AuthorizationContext from inside a PermissionEvaluator?

我正在尝试使用 spring 引导和 Keycloak 在我的应用程序中实现细粒度权限。

因此我想使用以下注释:

@PreAuthorize("hasPermission('myResource', 'myScope')")

所以我实现了以下内容:

@Slf4j
public class KeycloakPermissionEvaluator implements PermissionEvaluator {

    @Override
    public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
        // This should probably never be the case but let's check it anyways
        if (authentication == null) {
            log.warn("Permission denied because of illegal 'authentication' argument (null)");
            return false;
        }
        // targetDomainObject will be the resourceName.
        // It can be null in case we only want to check permissions for a specific scope else it should be a String
        if (targetDomainObject != null && !(targetDomainObject instanceof String)) {
            log.warn("Permission denied because of illegal 'targetDomainObject' argument ({})", targetDomainObject.getClass().getName());
            return false;
        }
        // permission will be the scopeName.
        // It can be null in case we only want to check permissions for a specific resource else it should be a String
        if (permission != null && !(permission instanceof String)) {
            log.warn("Permission denied because of illegal 'permission' argument ({})", permission.getClass().getName());
            return false;
        }

        return hasPermission((KeycloakPrincipal) authentication.getPrincipal(), (String) targetDomainObject, (String) permission);
    }

    @Override
    public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
        // This should probably never be the case but let's check it anyways
        if (authentication == null) {
            log.warn("Permission denied because of illegal 'authentication' argument (null)");
            return false;
        }
        // targetId needs to be set else we should be using hasPermission(Authentication, Object, Object)
        // It will be used to build the resourceName
        if (!(targetId instanceof String)) {
            log.warn("Permission denied because of illegal 'targetId' argument ({}). Did you mean to use hasPermission(resource, scope) ?", targetId == null ? "null" : targetId.getClass().getName());
            return false;
        }
        // targetType will be used to build the resourceName
        if (targetType == null) {
            log.warn("Permission denied because of illegal 'targetType' argument (null)");
            return false;
        }
        // permission will be the scopeName.
        // It can be null in case we only want to check permissions for a specific resource else it should be a String
        if (permission != null && !(permission instanceof String)) {
            log.warn("Permission denied because of illegal 'permission' argument ({})", permission.getClass().getName());
            return false;
        }

        String resourceName = String.format("%s/%s", targetType, targetId);
        return hasPermission((KeycloakPrincipal) authentication.getPrincipal(), resourceName, (String) permission);
    }

    private boolean hasPermission(KeycloakPrincipal principal, String resourceName, String scopeName) {
        AuthorizationContext authorizationContext = principal.getKeycloakSecurityContext().getAuthorizationContext();

        if (resourceName == null) {
            if (scopeName == null) {
                log.warn("Permission denied because both resourceName and scopeName are null.");
                return false;
            }
            return authorizationContext.hasScopePermission(scopeName);
        } else if (scopeName == null) {
            return authorizationContext.hasResourcePermission(resourceName);
        } else {
            return authorizationContext.hasPermission(resourceName, scopeName);
        }
    }

}

但这似乎不起作用。 authorizationContext 最终成为 null。 这是 hasPermission 中身份验证对象的样子

这是我的安全配置

@Slf4j
@KeycloakConfiguration
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {

    /**
     * Registers the KeycloakAuthenticationProvider with the authentication manager.
     */
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) {
        KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
        keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
        auth.authenticationProvider(keycloakAuthenticationProvider);
    }

    /**
     * Defines the session authentication strategy.
     */
    @Bean
    @Override
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
        return new NullAuthenticatedSessionStrategy();
    }

    @Override
    protected AuthenticationEntryPoint authenticationEntryPoint() {
        return (request, response, authException) -> {
            log.warn("401 Unauthorized while processing " + request.getRequestURI(), authException);
            response.sendError(401, "Unauthorized");
        };
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);

        // @formatter:off
        http
            .csrf().disable()
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and().headers()
                .frameOptions().sameOrigin()
            .and().authorizeRequests()
                // Public
                .antMatchers(
                    "/v3/api-docs/**",
                    "/swagger-ui.html",
                    "/swagger-ui/**").permitAll()
                // Private
                .anyRequest().authenticated();
        // @formatter:on
    }
}

和我的方法安全配置

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
    @Autowired
    private ApplicationContext applicationContext;

    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
        DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
        expressionHandler.setPermissionEvaluator(new KeycloakPermissionEvaluator());
        expressionHandler.setApplicationContext(applicationContext);

        return expressionHandler;
    }
}

Keycloak 配置:

{
  "realm": "${env.KEYCLOAK_REALM}",
  "auth-server-url": "${env.KEYCLOAK_AUTH_SERVER}",
  "ssl-required": "external",
  "enable-cors": true,
  "resource": "my-server",
  "credentials": {
    "secret": "${env.KEYCLOAK_SECRET}"
  },
  "confidential-port": 0,
  "policy-enforcer": {
    "enforcement-mode": "PERMISSIVE",
    "paths": [
      {
        "name": "Default",
        "path": "*/*"
      }
    ]
  }
}

这似乎与 KEYCLOAK-12260

有关

我添加了一个控制器来展示我的问题:

@RestController
@RequestMapping("/test")
public class TestController {
    @Autowired
    HttpServletRequest autowWiredRequest;

    @GetMapping
    public String hello(Principal principal) {
        KeycloakAuthenticationToken principal1 = (KeycloakAuthenticationToken) this.autowWiredRequest.getUserPrincipal();
        KeycloakAuthenticationToken principal2 = (KeycloakAuthenticationToken) ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest().getUserPrincipal();
        KeycloakAuthenticationToken principal3 = (KeycloakAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();

        boolean allTheSame = (principal1 == principal2 && principal2 == principal3);

        KeycloakPrincipal<?> keycloakPrincipal = (KeycloakPrincipal<?>) principal1.getPrincipal();
        KeycloakSecurityContext keycloakSecurityContext = keycloakPrincipal.getKeycloakSecurityContext();
        AuthorizationContext authorizationContext = keycloakSecurityContext.getAuthorizationContext();

        return authorizationContext == null ? "null" : "OMG we're there!";
    }
}

这是调试的样子:

这里还有一些有趣的东西:似乎还有另一个 SecurityContext 包含我想要的所有内容

经过大量挖掘和同事的一些帮助,发现某些对象具有多个 bean 的事实引导我们 that page

因此将以下行添加到 SecurityContext 使 AuthorizationContext 可用。

    @Bean
    public FilterRegistrationBean keycloakAuthenticationProcessingFilterRegistrationBean(
            KeycloakAuthenticationProcessingFilter filter) {
        FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
        registrationBean.setEnabled(false);
        return registrationBean;
    }

    @Bean
    public FilterRegistrationBean keycloakPreAuthActionsFilterRegistrationBean(
            KeycloakPreAuthActionsFilter filter) {
        FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
        registrationBean.setEnabled(false);
        return registrationBean;
    }

    @Bean
    public FilterRegistrationBean keycloakAuthenticatedActionsFilterBean(
            KeycloakAuthenticatedActionsFilter filter) {
        FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
        registrationBean.setEnabled(false);
        return registrationBean;
    }

    @Bean
    public FilterRegistrationBean keycloakSecurityContextRequestFilterBean(
        KeycloakSecurityContextRequestFilter filter) {
        FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
        registrationBean.setEnabled(false);
        return registrationBean;
    }

现在我希望与之合作的东西还没有奏效,但至少我在这里的问题得到了回答。

编辑:我走得更远,遇到了更多阻力。我直接在Keycloak Jira上提了个issue:KEYCLOAK-16515