@Async异步方法中OAuth2授权OpenFeign调用servletRequest为空问题
问题根因
你遇到的授权失败问题,本质是上下文传播不全导致的:
- 你配置的
DelegatingSecurityContextAsyncTaskExecutor仅负责传播SecurityContext,不会传递Spring Web绑定在RequestContextHolder里的RequestAttributes(也就是包含HttpServletRequest的请求上下文)。 - 默认的
DefaultOAuth2AuthorizedClientManager执行授权时,会优先从请求属性里读取HttpServletRequest做同请求内的token缓存,异步线程拿不到请求上下文时就会出现null异常。
关于
threadContextInheritable配置的说明 不推荐通过开启RequestContextFilter的threadContextInheritable参数解决问题。
该配置基于InheritableThreadLocal实现上下文传递,仅在手动新建线程的场景生效,完全无法适配线程池场景:线程池中的线程是长期复用的,InheritableThreadLocal只会在线程第一次创建时拷贝父线程上下文,后续执行任务时会拿到过期的请求信息,极易出现请求串号、上下文泄漏的严重问题。
正确解决方案
根据你的使用场景(客户端凭证授权模式),优先选择第一种方案,无上下文传播开销、兼容性最好。
方案1:移除客户端凭证模式对Servlet上下文的依赖(推荐)
客户端凭证授权是服务自身的身份认证行为,和当前HTTP请求、当前登录用户没有关联,完全不需要依赖HttpServletRequest。你只需要修改OAuth2授权请求的构造逻辑,显式声明不使用Servlet上下文即可:
static class OAuthClientCredentialsFeignManager { public String getAccessToken(OAuth2AuthorizedClientManager manager, ClientRegistration clientRegistration) { try { var oAuth2AuthorizeRequest = OAuth2AuthorizeRequest .withClientRegistrationId(clientRegistration.getRegistrationId()) // 客户端凭证模式不需要绑定当前请求的用户,传入服务级标识即可 .principal("service-client:" + clientRegistration.getRegistrationId()) // 显式置空Servlet相关属性,跳过从请求上下文读取的逻辑 .attribute(HttpServletRequest.class.getName(), null) .attribute(HttpServletResponse.class.getName(), null) .build(); var client = manager.authorize(oAuth2AuthorizeRequest); if (isNull(client)) { throw new IllegalStateException("client credentials flow on " + clientRegistration.getRegistrationId() + " failed, client is null"); } return client.getAccessToken().getTokenValue(); } catch (Exception ex) { log.error("client credentials error " + ex.getMessage()); } return null; } }
改完后哪怕没有Request上下文(比如定时任务、消息消费场景触发Feign调用),也能正常获取token。
方案2:自定义线程池同时传播Security上下文和Request上下文
如果你后续需要在异步线程中使用代表用户的授权模式(比如授权码模式),确实需要传递Request上下文,可以通过TaskDecorator手动实现上下文传播,避免线程复用带来的问题:
@Bean("threadPoolTaskExecutor") public TaskExecutor getAsyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(20); executor.setMaxPoolSize(1000); executor.setWaitForTasksToCompleteOnShutdown(true); executor.setThreadNamePrefix("Async-"); // 自定义任务装饰器,提交任务时快照上下文,执行时绑定 executor.setTaskDecorator(runnable -> { // 提交任务阶段在父请求线程捕获上下文快照 SecurityContext originalSecurityContext = SecurityContextHolder.getContext(); RequestAttributes originalRequestAttr = RequestContextHolder.getRequestAttributes(); return () -> { try { // 子线程执行前绑定上下文 SecurityContextHolder.setContext(originalSecurityContext); if (originalRequestAttr != null) { RequestContextHolder.setRequestAttributes(originalRequestAttr); } runnable.run(); } finally { // 执行完成后清理上下文,避免线程复用时上下文泄漏串号 SecurityContextHolder.clearContext(); RequestContextHolder.resetRequestAttributes(); } }; }); executor.initialize(); return executor; }
注意:使用该配置时不需要再包装DelegatingSecurityContextAsyncTaskExecutor,装饰器已经完成了SecurityContext的传播逻辑。
内容的提问来源于stack exchange,提问作者luso
相关产品推荐
相关产品推荐

