Spring Security资源服务器配置accessDecisionManager遇SpEL表达式错误
问题
在为资源服务器的HttpSecurity配置关联AccessDecisionManager并使用OAuth2 SpEL表达式时遇到异常。已按照文档及相关指导实现,但始终报错。当前使用版本:spring-security-oauth2-2.3.6、spring-security-core-5.6.2。
资源服务器配置代码
@Slf4j @Configuration @EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Autowired private AccessDeniedHandler oauthAccessDeniedHandler; @Autowired private ResourceServerTokenServices tokenServices; @Override public void configure(HttpSecurity http) throws Exception { http .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .csrf().disable() .anonymous().disable() .authorizeRequests() .antMatchers("/authenticated/**") .access("hasAnyAuthority('ROLE_USER','SCOPE_READ')") .expressionHandler(oauthExpressionHandler()) .accessDecisionManager(accessDecisionManager()) .and() .exceptionHandling().accessDeniedHandler(oauthAccessDeniedHandler) .defaultAuthenticationEntryPointFor(oauthAuthenticationEntryPoint(), new AntPathRequestMatcher("/authenticated/**")); } @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources .tokenServices(tokenServices); //.expressionHandler(oauthExpressionHandler); } @Bean public OAuth2WebSecurityExpressionHandler oauthExpressionHandler() { return new OAuth2WebSecurityExpressionHandler(); } @Bean protected AuthenticationEntryPoint oauthAuthenticationEntryPoint() { OAuth2AuthenticationEntryPoint entryPoint = new OAuth2AuthenticationEntryPoint(); entryPoint.setRealmName("RealName"); return entryPoint; } @Bean protected AccessDecisionManager accessDecisionManager() { List<AccessDecisionVoter<? extends Object>> decisionVoters = Arrays.asList( new ScopeVoter(), new RoleVoter(), new WebExpressionVoter(), new AuthenticatedVoter()); return new UnanimousBased(decisionVoters); } }
运行时异常
14:52:52 TRACE org.springframework.web.servlet.mvc.method.annotation.HttpEntityMethodProcessor.traceDebug: Writing [{timestamp=Fri Sep 16 14:52:52 WEST 2022, status=500, error=Internal Server Error, exception=java.lang.IllegalArgumentException, trace=java.lang.IllegalArgumentException: Failed to evaluate expression '#oauth2.throwOnError(hasAnyAuthority('ROLE_USER','SCOPE_READ'))' at org.springframework.security.access.expression.ExpressionUtils.evaluateAsBoolean(ExpressionUtils.java:33) at org.springframework.security.web.access.expression.WebExpressionVoter.vote(WebExpressionVoter.java:59) at org.springframework.security.web.access.expression.WebExpressionVoter.vote(WebExpressionVoter.java:39) at org.springframework.security.access.vote.UnanimousBased.decide(UnanimousBased.java:68) at org.springframework.security.access.intercept.AbstractSecurityInterceptor.attemptAuthorization(AbstractSecurityInterceptor.java:239) at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:208) at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:113) at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:81) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) ...(省略中间栈帧) Caused by: org.springframework.expression.spel.SpelEvaluationException: EL1011E: Method call: Attempted to call method throwOnError(java.lang.Boolean) on null context object at org.springframework.expression.spel.ast.MethodReference.throwIfNotNullSafe(MethodReference.java:154) at org.springframework.expression.spel.ast.MethodReference.getValueRef(MethodReference.java:83) at org.springframework.expression.spel.ast.CompoundExpression.getValueRef(CompoundExpression.java:70) at org.springframework.expression.spel.ast.CompoundExpression.getValueInternal(CompoundExpression.java:91) at org.springframework.expression.spel.ast.SpelNodeImpl.getTypedValue(SpelNodeImpl.java:117) at org.springframework.expression.spel.standard.SpelExpression.getValue(SpelExpression.java:308) at org.springframework.security.access.expression.ExpressionUtils.evaluateAsBoolean(ExpressionUtils.java:30) ... 67 more , message=Failed to evaluate expression '#oauth2.throwOnError(hasAnyAuthority('ROLE_USER','SCOPE_READ'))', path=/authenticated/profile}]
移除accessDecisionManager配置后一切正常,但希望使用包含指定投票器的UnanimousBased决策管理器而非默认的AffirmativeBased,询问如何实现及该需求是否合理。
解决方案
问题原因
异常核心是#oauth2表达式上下文为null,因为自定义的WebExpressionVoter没有关联OAuth2WebSecurityExpressionHandler。默认配置中,资源服务器会自动将OAuth2WebSecurityExpressionHandler绑定到WebExpressionVoter,但自定义AccessDecisionManager时,需要手动为WebExpressionVoter设置表达式处理器。
修复步骤
修改accessDecisionManager()方法,为WebExpressionVoter注入OAuth2WebSecurityExpressionHandler:
@Bean protected AccessDecisionManager accessDecisionManager() { WebExpressionVoter webExpressionVoter = new WebExpressionVoter(); // 绑定OAuth2表达式处理器 webExpressionVoter.setExpressionHandler(oauthExpressionHandler()); List<AccessDecisionVoter<? extends Object>> decisionVoters = Arrays.asList( new ScopeVoter(), new RoleVoter(), webExpressionVoter, new AuthenticatedVoter()); return new UnanimousBased(decisionVoters); }
同时,确保在ResourceServerSecurityConfigurer中启用表达式处理器(取消注释):
@Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources .tokenServices(tokenServices) .expressionHandler(oauthExpressionHandler()); }
需求合理性
使用UnanimousBased是合理的,它要求所有投票器都投赞成票才允许访问,适合需要严格权限校验的场景。相比默认的AffirmativeBased(只要有一个投票器赞成就通过),UnanimousBased能实现更严格的权限控制,比如同时验证角色和OAuth2 scope的有效性。
内容的提问来源于stack exchange,提问作者LuisFerrolho

