在我的 Java Spring Boot 3 应用程序中,我想将 Spring Security 与
@PreAuthorize
/ @PostAuthorize
一起使用。由于某种原因,我收到的 Keycloak 生成的令牌在 Authorities
下没有 Spring 所期望的角色,而是在 "realm_access"
属性下。所以,一些代码
表达式根
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;
}
}
表达式处理程序
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;
}
}
安全配置
@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();
}
}
控制器
@PreAuthorize("hasRealmRole('FOOBAR')")
@GetMapping("/{id}")
public ResponseEntity<Asset> getAsset(@UUIDConstraint @PathVariable("id") String id) {
...
最后,基于上面添加的 Spring 参考的 Beans
@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;
}
}
现在,当我向 /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
知道为什么我的CustomMethodSecurityExpressionRoot
没有正确注册吗?
there。它与领域级别定义的 Keycloak 角色(您在 realm_access.roles
声明中找到的角色)以及客户端级别(
resource-access.{client-id}.roles
声明)定义的 Keycloak 角色兼容。丰富安全性 SpEL DSL 实际上有点棘手(我必须复制 Spring 受保护的类才能使其工作),并且应该在需要超过
Role Based Aaccess C控制时使用。我有另一个教程,但它非常符合我的库和初学者(我链接的上一个不是)。
这是一个示例表达式处理程序配置:
@Bean
static MethodSecurityExpressionHandler methodSecurityExpressionHandler() {
return new SpringAddonsMethodSecurityExpressionHandler(ProxiesMethodSecurityExpressionRoot::new);
}
用这个表达词根:
static final class ProxiesMethodSecurityExpressionRoot extends SpringAddonsMethodSecurityExpressionRoot {
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");
}
}
这是我必须从 Spring Security 复制的代码:
@RequiredArgsConstructor
public class SpringAddonsMethodSecurityExpressionHandler extends DefaultMethodSecurityExpressionHandler {
private final Supplier<SpringAddonsMethodSecurityExpressionRoot> 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 SpringAddonsMethodSecurityEvaluationContext(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 SpringAddonsMethodSecurityEvaluationContext extends MethodBasedEvaluationContext {
SpringAddonsMethodSecurityEvaluationContext(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()));
}
}
}
public class SpringAddonsMethodSecurityExpressionRoot extends SecurityExpressionRoot implements MethodSecurityExpressionOperations {
private Object filterObject;
private Object returnObject;
private Object target;
public SpringAddonsMethodSecurityExpressionRoot() {
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;
}
}