使用 Spring Security 6 实现自定义表达式处理

gha*_*las 2 java security spring spring-boot

在我的 Java Spring Boot 3 应用程序中,我想将 Spring Security 与@PreAuthorize/一起使用@PostAuthorize。由于某种原因,我收到的 Keycloak 生成的令牌没有Authorities像 Spring 期望的那样具有角色,而是在"realm_access"属性下。
因此,我决定继续实现一个自定义表达式处理程序,遵循我在https://docs.spring.io/spring-security/reference/servlet/authorization/method-security.html#customizing-expression中找到的内容-处理

所以,一些代码
表达式根

public class CustomMethodSecurityExpressionRoot
        extends SecurityExpressionRoot implements MethodSecurityExpressionOperations {

    public CustomMethodSecurityExpressionRoot(Authentication authentication) {
        super(authentication);
    }

    public boolean hasRealmRole(String role) {
        var principal = ((OAuth2AuthenticatedPrincipal)this.getPrincipal());
        if (principal == null) {
            return false;
        }

        var realmAccess = (JSONObject)principal.getAttribute("realm_access");
        if (realmAccess == null) {
            return false;
        }

        var roles = (JSONArray)realmAccess.get("roles");
        if (roles == null || roles.isEmpty()) {
            return false;
        }

        return roles.stream().anyMatch(x -> x.equals(role));
    }

    @Override
    public void setFilterObject(Object filterObject) {

    }

    @Override
    public Object getFilterObject() {
        return null;
    }

    @Override
    public void setReturnObject(Object returnObject) {

    }

    @Override
    public Object getReturnObject() {
        return null;
    }

    @Override
    public Object getThis() {
        return null;
    }
}
Run Code Online (Sandbox Code Playgroud)

表达式处理程序

public class CustomMethodSecurityExpressionHandler extends DefaultMethodSecurityExpressionHandler {
    private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();

    @Override
    protected MethodSecurityExpressionOperations createSecurityExpressionRoot(
            Authentication authentication, MethodInvocation invocation) {
        CustomMethodSecurityExpressionRoot root =
                new CustomMethodSecurityExpressionRoot(authentication);
        root.setPermissionEvaluator(getPermissionEvaluator());
        root.setTrustResolver(this.trustResolver);
        root.setRoleHierarchy(getRoleHierarchy());
        return root;
    }
}
Run Code Online (Sandbox Code Playgroud)

安全配置

@Configuration
@EnableMethodSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .authorizeHttpRequests(auth -> auth
                    .requestMatchers("/health/**").permitAll()
                    .requestMatchers("/swagger-ui/**").permitAll()
                    .requestMatchers("/swagger/**").permitAll()
                    .requestMatchers("/v3/api-docs/**").permitAll()
                    // Cloud config related endpoint
                    .requestMatchers("/configuration/**").permitAll()
                    .anyRequest().authenticated()
            )
            .oauth2ResourceServer(OAuth2ResourceServerConfigurer::opaqueToken);
        return http.build();
    }
}
Run Code Online (Sandbox Code Playgroud)

控制器

    @PreAuthorize("hasRealmRole('FOOBAR')")
    @GetMapping("/{id}")
    public ResponseEntity<Asset> getAsset(@UUIDConstraint @PathVariable("id") String id) {
    ...
Run Code Online (Sandbox Code Playgroud)

最后,基于上面添加的 Spring 参考的Bean

@Configuration
public class BaseConfig {
    @Bean
    public RoleHierarchy roleHierarchy() {
        return new RoleHierarchyImpl();
    }

    @Bean
    static MethodSecurityExpressionHandler methodSecurityExpressionHandler(RoleHierarchy roleHierarchy) {
        var handler = new CustomMethodSecurityExpressionHandler();
        handler.setRoleHierarchy(roleHierarchy);
        return handler;
    }
}
Run Code Online (Sandbox Code Playgroud)

现在,当我向端点发送请求时/GET,出现以下错误:

java.lang.IllegalArgumentException: Failed to evaluate expression 'hasRealmRole('FOOBAR')'  
...  
Caused by: org.springframework.expression.spel.SpelEvaluationException: EL1004E: Method call: Method hasRealmRole(java.lang.String) cannot be found on type org.springframework.security.access.expression.method.MethodSecurityExpressionRoot
Run Code Online (Sandbox Code Playgroud)

知道为什么我的CustomMethodSecurityExpressionRoot注册不正确吗?

ch4*_*4mp 5

首先,您所做的不太适合权限映射:有相应的身份验证(和权限)转换器。我在那里编写了一个配置相当通用的权限映射器的教程。realm_access.roles它与领域级别(您在声明中找到的那些)以及客户端级别(resource-access.{client-id}.roles声明)定义的 Keycloak 角色兼容。

丰富安全性 SpEL DSL 实际上有点棘手(我必须复制一个 Spring 受保护类才能使其工作),并且应该在需要的不仅仅是基于角色访问控制使用。我有另一个教程,但它非常符合我的库和初学者(我链接的上一个不是)。

以下是表达式处理程序配置示例:

@Bean
static MethodSecurityExpressionHandler methodSecurityExpressionHandler() {
    return new C4MethodSecurityExpressionHandler(ProxiesMethodSecurityExpressionRoot::new);
}
Run Code Online (Sandbox Code Playgroud)

有了这个表达式根:

static final class ProxiesMethodSecurityExpressionRoot extends C4MethodSecurityExpressionRoot {

    public boolean is(String preferredUsername) {
        return Objects.equals(preferredUsername, getAuthentication().getName());
    }

    public Proxy onBehalfOf(String proxiedUsername) {
        return get(ProxiesAuthentication.class).map(a -> a.getProxyFor(proxiedUsername))
                .orElse(new Proxy(proxiedUsername, getAuthentication().getName(), List.of()));
    }

    public boolean isNice() {
        return hasAnyAuthority("NICE", "SUPER_COOL");
    }
}
Run Code Online (Sandbox Code Playgroud)

这是我必须从 Spring Security 复制的代码:

@RequiredArgsConstructor
public class C4MethodSecurityExpressionHandler extends DefaultMethodSecurityExpressionHandler {
    private final Supplier<C4MethodSecurityExpressionRoot> expressionRootSupplier;

    /**
     * Creates the root object for expression evaluation.
     */
    @Override
    protected MethodSecurityExpressionOperations createSecurityExpressionRoot(Authentication authentication,
            MethodInvocation invocation) {
        return createSecurityExpressionRoot(() -> authentication, invocation);
    }

    @Override
    public EvaluationContext createEvaluationContext(Supplier<Authentication> authentication, MethodInvocation mi) {
        var root = createSecurityExpressionRoot(authentication, mi);
        var ctx = new C4MethodSecurityEvaluationContext(root, mi, getParameterNameDiscoverer());
        ctx.setBeanResolver(getBeanResolver());
        return ctx;
    }

    private MethodSecurityExpressionOperations createSecurityExpressionRoot(Supplier<Authentication> authentication,
            MethodInvocation invocation) {
        final var root = expressionRootSupplier.get();
        root.setThis(invocation.getThis());
        root.setPermissionEvaluator(getPermissionEvaluator());
        root.setTrustResolver(getTrustResolver());
        root.setRoleHierarchy(getRoleHierarchy());
        root.setDefaultRolePrefix(getDefaultRolePrefix());
        return root;
    }

    static class C4MethodSecurityEvaluationContext extends MethodBasedEvaluationContext {

        C4MethodSecurityEvaluationContext(MethodSecurityExpressionOperations root, MethodInvocation mi,
                ParameterNameDiscoverer parameterNameDiscoverer) {
            super(root, getSpecificMethod(mi), mi.getArguments(), parameterNameDiscoverer);
        }

        private static Method getSpecificMethod(MethodInvocation mi) {
            return AopUtils.getMostSpecificMethod(mi.getMethod(), AopProxyUtils.ultimateTargetClass(mi.getThis()));
        }

    }

}
Run Code Online (Sandbox Code Playgroud)
public class C4MethodSecurityExpressionRoot extends SecurityExpressionRoot implements MethodSecurityExpressionOperations {

    private Object filterObject;
    private Object returnObject;
    private Object target;

    public C4MethodSecurityExpressionRoot() {
        super(SecurityContextHolder.getContext().getAuthentication());
    }

    @SuppressWarnings("unchecked")
    protected <T extends Authentication> Optional<T> get(Class<T> expectedAuthType) {
        return Optional.ofNullable(getAuthentication()).map(a -> a.getClass().isAssignableFrom(expectedAuthType) ? (T) a : null).flatMap(Optional::ofNullable);
    }

    @Override
    public void setFilterObject(Object filterObject) {
        this.filterObject = filterObject;
    }

    @Override
    public Object getFilterObject() {
        return filterObject;
    }

    @Override
    public void setReturnObject(Object returnObject) {
        this.returnObject = returnObject;
    }

    @Override
    public Object getReturnObject() {
        return returnObject;
    }

    public void setThis(Object target) {
        this.target = target;
    }

    @Override
    public Object getThis() {
        return target;
    }

}
Run Code Online (Sandbox Code Playgroud)