You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Graal Native Image中@PreAuthorize结合SpEL失效,请求参数为null

问题背景

在RestController中使用@PreAuthorize校验请求体中的persnr与当前用户的persnr是否一致,JVM环境下运行正常,但Graal Native Image中SpEL表达式里的#request参数为null,导致空指针异常。

原实现代码

Controller层

@PreAuthorize("@authorization.hasPermission(principal, #request)")
@PostMapping("/items")
public ResponseEntity<CalendarResponse> getCalendarItems(@Valid @NotNull @RequestBody CalendarRequest request)
{
   // 业务逻辑
}

权限校验组件

@Component
public class Authorization
{
    public boolean hasPermission(User user, /* Native Image中为null */ CalendarRequest request) 
    {
        return user.getPersnr().equals(request.getPersnr());
    }
}

请求体定义

public class CalendarRequest
{
    @NotBlank(message = "\"persnr darf nicht leer sein")
    private String persnr; 

    public String getPersnr()
    {
        return persnr;
    }
    // 其他getter/setter
}

尝试过的无效方案

  1. WebExpressionAuthorizationManager替代:配置后未生效
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception
{
    http.authorizeHttpRequests(auth -> 
            auth
                // 其他配置
                .dispatcherTypeMatchers(DispatcherType.ERROR).permitAll()                               
                .requestMatchers("/calendar/items").access(new WebExpressionAuthorizationManager("@authorization.hasPermission(principal, #request)"))
                .anyRequest().authenticated());
    
    // 其他配置

    return http.build();
}
  1. 自定义AuthorizationManager模拟@PreAuthorize:#request参数仍为null
@Configuration
@EnableMethodSecurity(prePostEnabled = false, securedEnabled = false, jsr250Enabled = false)
public class AuthorizationConfig
{
    @Bean
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    Advisor preAuthorize(CustomAuthorizationManager manager) 
    {
        Pointcut pointcut = new AnnotationMatchingPointcut(null, PreAuthorize.class, true);
        AuthorizationManagerBeforeMethodInterceptor interceptor = new AuthorizationManagerBeforeMethodInterceptor(pointcut, manager);
        interceptor.setOrder(AuthorizationInterceptorsOrder.PRE_AUTHORIZE.getOrder());
        return interceptor;
    }
}

@Component
public class CustomAuthorizationManager implements AuthorizationManager<MethodInvocation>
{
    @Autowired
    ApplicationContext context;

    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation mi)
    {
        PreAuthorize annotation = mi.getMethod().getAnnotation(PreAuthorize.class);
    
        DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
        expressionHandler.setApplicationContext(context);
        Expression expression = expressionHandler.getExpressionParser().parseExpression(annotation.value());
    
        EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication, mi);
        boolean granted = ExpressionUtils.evaluateAsBoolean(expression, ctx);
        return new ExpressionAuthorizationDecision(granted, expression);
    }
}

可行解决方案

方案1:直接从MethodInvocation获取请求参数

绕过SpEL的参数绑定,在自定义AuthorizationManager里直接提取方法参数,不用依赖#request变量:

@Component
public class CustomAuthorizationManager implements AuthorizationManager<MethodInvocation>
{
    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation mi)
    {
        // 获取当前用户
        Authentication auth = authentication.get();
        User user = (User) auth.getPrincipal();
        
        // 提取方法中的CalendarRequest参数
        CalendarRequest request = null;
        for (Object arg : mi.getArguments()) {
            if (arg instanceof CalendarRequest) {
                request = (CalendarRequest) arg;
                break;
            }
        }
        
        // 校验逻辑
        boolean granted = request != null && user.getPersnr().equals(request.getPersnr());
        return new AuthorizationDecision(granted);
    }
}

方案2:配置Graal Native Image元数据

如果坚持使用原SpEL方式,需要为CalendarRequest和相关方法添加反射配置,确保Native Image能识别参数绑定:
创建src/main/resources/META-INF/native-image/your-group-id/your-artifact-id/reflect-config.json:

[
  {
    "name": "com.yourpackage.CalendarRequest",
    "allDeclaredConstructors": true,
    "allPublicConstructors": true,
    "allDeclaredMethods": true,
    "allPublicMethods": true,
    "allDeclaredFields": true,
    "allPublicFields": true
  },
  {
    "name": "com.yourpackage.Authorization",
    "allPublicMethods": true
  }
]

同时确保Spring Security的SpEL相关类被正确代理,可添加spring-native.properties配置:

spring.native.additional-sources=com.yourpackage
spring.native.mode=agent

方案3:改用方法参数直接注入校验逻辑

放弃@PreAuthorize的SpEL方式,直接在Controller方法开头加入校验逻辑,或者封装成自定义注解+AOP:

@PostMapping("/items")
public ResponseEntity<CalendarResponse> getCalendarItems(@Valid @NotNull @RequestBody CalendarRequest request, Authentication authentication)
{
    User user = (User) authentication.getPrincipal();
    if (!user.getPersnr().equals(request.getPersnr())) {
        throw new AccessDeniedException("无权限访问");
    }
    // 业务逻辑
}

或者封装成AOP:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckPersnrMatch {}

@Aspect
@Component
public class PersnrCheckAspect {
    @Autowired
    private Authentication authentication;

    @Before("@annotation(CheckPersnrMatch)")
    public void check(JoinPoint joinPoint) {
        User user = (User) authentication.getPrincipal();
        CalendarRequest request = null;
        for (Object arg : joinPoint.getArgs()) {
            if (arg instanceof CalendarRequest) {
                request = (CalendarRequest) arg;
                break;
            }
        }
        if (request == null || !user.getPersnr().equals(request.getPersnr())) {
            throw new AccessDeniedException("无权限访问");
        }
    }
}

// Controller使用
@CheckPersnrMatch
@PostMapping("/items")
public ResponseEntity<CalendarResponse> getCalendarItems(@Valid @NotNull @RequestBody CalendarRequest request)
{
   // 业务逻辑
}

方案4:使用RequestContextHolder读取请求体

通过RequestContextHolder获取当前请求,重新解析请求体(注意请求体只能读取一次,需要配置ContentCachingRequestWrapper):
首先配置过滤器缓存请求体:

@Bean
public FilterRegistrationBean<ContentCachingRequestWrapperFilter> contentCachingFilter() {
    FilterRegistrationBean<ContentCachingRequestWrapperFilter> registrationBean = new FilterRegistrationBean<>();
    registrationBean.setFilter(new ContentCachingRequestWrapperFilter());
    registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
    return registrationBean;
}

public class ContentCachingRequestWrapperFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
        filterChain.doFilter(wrappedRequest, response);
    }
}

然后在AuthorizationManager中读取:

@Component
public class CustomAuthorizationManager implements AuthorizationManager<MethodInvocation> {
    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation mi) {
        Authentication auth = authentication.get();
        User user = (User) auth.getPrincipal();
        
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        ContentCachingRequestWrapper wrappedRequest = (ContentCachingRequestWrapper) request;
        byte[] content = wrappedRequest.getContentAsByteArray();
        CalendarRequest calendarRequest = null;
        try {
            calendarRequest = objectMapper.readValue(content, CalendarRequest.class);
        } catch (IOException e) {
            // 处理解析异常
            return new AuthorizationDecision(false);
        }
        
        boolean granted = calendarRequest != null && user.getPersnr().equals(calendarRequest.getPersnr());
        return new AuthorizationDecision(granted);
    }
}

内容的提问来源于stack exchange,提问作者Meini

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 13:34:52