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 }
尝试过的无效方案
- 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(); }
- 自定义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
相关产品推荐
相关产品推荐

