Sa-Token icon indicating copy to clipboard operation
Sa-Token copied to clipboard

未能获取有效的上下文

Open Joker-Q4 opened this issue 1 year ago • 11 comments

使用版本:

1.38.0

涉及的功能模块:

gateway

测试步骤:

  • 我经过以下步骤测试:
  1. 使用Apache HttpClient5构造了一个WebClient.Builder
@Bean("ticketFluxBuilder")
    public WebClient.Builder ticketBuilder(HttpComponentsClientHttpConnector httpComponentsClientHttpConnector){
        return WebClient.builder()
                .baseUrl(iscProperties.getServer())
                .clientConnector(httpComponentsClientHttpConnector)
                .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
                .defaultHeader(HttpHeaders.CONNECTION, "Keep-Alive")
                .defaultHeader(HttpHeaders.ACCEPT_ENCODING, "br,deflate.gzip,x-gzip");
    }
  1. 通过WebClient.Builder构造了一个请求
public Mono<String> checkTicket(WebClient.Builder builder, String ticket, String service, String pgtUrl, boolean renew) {
        return builder.build()
                .post()
                .uri(uriBuilder -> {
                    URI build = uriBuilder
                            .path("/serviceValidate")
                            .queryParam("ticket", ticket)
                            .queryParam("service", service)
                            .queryParam("pgtUrl", pgtUrl)
                            .queryParam("renew", renew)
                            .build();
                    log.info("url: {}", build);
                    return build;
                })
                .retrieve()
                .bodyToMono(String.class)
                .publishOn(Schedulers.parallel())
                .retry(3)
                .doOnError(throwable -> {
                    ExceptionUtil.printStackTrace(throwable);
                });
    }
  1. 获取到相关信息之后,经过一系列转换,使用StpUtil.login
fluxUser.flatMap((Function<User, Mono<Resp<User>>>)
                user -> {
                    StpUtil.login(user.name);
                    return Mono.just(checkTicket(user));
                }
  • 得出以下结果:
cn.dev33.satoken.exception.SaTokenContextException: 未能获取有效的上下文
	at cn.dev33.satoken.context.SaTokenContextForThreadLocalStorage.getBoxNotNull(SaTokenContextForThreadLocalStorage.java:72)
  • 其中第 xx 行的代码输出表现 和文档上描述的不一致:
SaTokenContextForThreadLocalStorage的getBoxNotNull()的box为空

public static Box getBoxNotNull() {
        Box box = (Box)boxThreadLocal.get();
        if (box == null) {
            throw (new SaTokenContextException("未能获取有效的上下文")).setCode(10002);
        } else {
            return box;
        }
    }
  • 我的理解是: WebClient.Builder请求后切换了线程,导致SaTokenContextForThreadLocalStorage的InheritableThreadLocal拿不到数据,但是WebClient是webflux的最佳请求方案
builder.build()
                .post()
                .uri(uriBuilder -> {
                    URI build = uriBuilder
                            .path("/serviceValidate")
                            .queryParam("ticket", ticket)
                            .queryParam("service", service)
                            .queryParam("pgtUrl", pgtUrl)
                            .queryParam("renew", renew)
                            .build();
                    log.info("url: {}", build);
                    return build;
                })
                .retrieve()
                .bodyToMono(String.class)


                .subscribeOn(Schedulers.fromExecutorService(createExecutorService))


                .publishOn(Schedulers.parallel())
                .retry(3)
                .doOnError(throwable -> {
                    ExceptionUtil.printStackTrace(throwable);
                });

有没有办法在不添加ExecutorService的情况下解决这个问题?

Joker-Q4 avatar Aug 23 '24 06:08 Joker-Q4

可以参考这个文章解决:https://blog.csdn.net/weixin_40116478/article/details/141557676?csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22141557676%22%2C%22source%22%3A%22weixin_40116478%22%7D

CKzcb avatar Aug 26 '24 03:08 CKzcb

可以参考这个文章解决:https://blog.csdn.net/weixin_40116478/article/details/141557676?csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22141557676%22%2C%22source%22%3A%22weixin_40116478%22%7D

不是这个问题,是webclient和框架冲突了

Joker-Q4 avatar Aug 26 '24 04:08 Joker-Q4

builder.build()
                .post()
                .uri(uriBuilder -> {
                    URI build = uriBuilder
                            .path("/serviceValidate")
                            .queryParam("ticket", ticket)
                            .queryParam("service", service)
                            .queryParam("pgtUrl", pgtUrl)
                            .queryParam("renew", renew)
                            .build();
                    log.info("url: {}", build);
                    return build;
                })
                .retrieve()
                .bodyToMono(String.class)
                .retry(3)
                .doOnError(throwable -> {
                    ExceptionUtil.printStackTrace(throwable);
                });

添加了也不行

Joker-Q4 avatar Sep 03 '24 02:09 Joker-Q4

我也遇到了同样的问题,使用的是spring-boot-starter-webflux 和 sa-token-reactor-spring-boot3-starter 1.39.0

	/**
	 * 在当前线程的 SaRequest 包装对象
	 * 
	 * @return /
	 */
	public static SaRequest getRequest() {
		return getBoxNotNull().getRequest();
	}

	/**
	 * 在当前线程的 SaResponse 包装对象
	 * 
	 * @return /
	 */
	public static SaResponse getResponse() {
		return getBoxNotNull().getResponse();
	}

	/**
	 * 在当前线程的 SaStorage 存储器包装对象
	 * 
	 * @return /
	 */
	public static SaStorage getStorage() {
		return getBoxNotNull().getStorage();
	}

我发现http请求中进行StpUtil.login()时,会调用两次getRequest,调用一次getStorage,其中在getStorage()时,报错:未能获取有效的上下文

分析源码发现box的初始化发生在此处:

SaReactorSyncHolder:

public static void setContext(ServerWebExchange exchange) {
		SaRequest request = new SaRequestForReactor(exchange.getRequest());
		SaResponse response = new SaResponseForReactor(exchange.getResponse());
		SaStorage storage = new SaStorageForReactor(exchange);
		SaTokenContextForThreadLocalStorage.setBox(request, response, storage);
	}

SaTokenContextForThreadLocalStorage中:

	/**
	 * 基于 ThreadLocal 的 [ Box 存储器 ]
	 */
	public static ThreadLocal<Box> boxThreadLocal = new InheritableThreadLocal<>();

	/**
	 * 初始化当前线程的 [ Box 存储器 ]
	 * @param request {@link SaRequest}
	 * @param response {@link SaResponse}
	 * @param storage {@link SaStorage}
	 */
	public static void setBox(SaRequest request, SaResponse response, SaStorage storage) {
		Box bok = new Box(request, response, storage);
		boxThreadLocal.set(bok);
	}

两次getRequest使用的是同一个box,而getStorage重新初始化了一个box,也就是重新调用了setContext方法(setContext时,box是初始化成功的了,但是在getBoxNotNull时,拿到的为null)

执行过程中其实SaSession已经创建好,并且在debug时也可以通过打印的token拿出来,但是在SaStorage storage = SaHolder.getStorage();时发生错误,我发获取storage

我不知道是否是因为getStorage前调用了setContext重新初始化box导致的

zhuyeHe avatar Sep 04 '24 06:09 zhuyeHe

以下方法执行失败报错:未能获取有效的上下文

public Mono<Result> login(AccountDTO accountDTO) {
        return Mono.just(accountDTO)
                .flatMap(dto -> {
                    if (StrUtil.isNotBlank(dto.getMail())) {
                        return getByMail(dto.getMail());
                    } else if (StrUtil.isNotBlank(dto.getUsername())) {
                        return getByUsername(dto.getUsername());
                    } else {
                        return Mono.error(new ServiceException(CommonResultType.PARAMETER_ERROR));
                    }
                })
                .map(account -> {
                    StpUtil.login(account.getAccountId());
                    return Result.success(StpUtil.getSessionByLoginId(account.getAccountId()));
                });
    }

强制转化为阻塞式之后执行成功

public Mono<Result> login(AccountDTO accountDTO) {
        return Mono.fromCallable(() -> {
                    if (StrUtil.isBlank(accountDTO.getPassword()) || StrUtil.isBlank(accountDTO.getMail())) {
                        return Result.fail(CommonResultType.PARAMETER_ERROR);
                    }
                    Account loginAccount = getByMail(accountDTO.getMail()).block();
                    StpUtil.login(loginAccount.getAccountId());
                    //登陆成功,返回token
                    return Result.success(StpUtil.getTokenInfo());
                })
                .subscribeOn(Schedulers.boundedElastic())  // 切换到 boundedElastic 线程池
                ;
    }

两种写法,在执行getRequest和getStorage时都是不同的线程,但是不太清除为什么强制阻塞后可以执行成功

zhuyeHe avatar Sep 04 '24 06:09 zhuyeHe

同样问题

zhangruiyu avatar Oct 06 '24 16:10 zhangruiyu

spring-boot-starter-webflux,sa-token-reactor-spring-boot3-starter +1

ahzvenol avatar Oct 14 '24 01:10 ahzvenol

https://github.com/dromara/Sa-Token/issues/445

zhangruiyu avatar Oct 31 '24 07:10 zhangruiyu

https://github.com/dromara/Sa-Token/issues/506 参考着2个应该可以解决

zhangruiyu avatar Oct 31 '24 07:10 zhangruiyu

遇到同样问题...

shaokeyibb avatar Jan 01 '25 16:01 shaokeyibb

同样的问题,使用webclient返回Mono后,报获取不到上下文,研究了一下。是因为异步的原因。自己还解决不了。 最后全换成restTemplate方式了。就没问题, 有没有大佬帮忙看看这个问题怎么解决。

zxttt avatar May 26 '25 14:05 zxttt