Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

未能获取有效的上下文 #675

Open
Joker-Q4 opened this issue Aug 23, 2024 · 5 comments
Open

未能获取有效的上下文 #675

Joker-Q4 opened this issue Aug 23, 2024 · 5 comments

Comments

@Joker-Q4
Copy link

使用版本:

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的情况下解决这个问题?

@CKzcb
Copy link

CKzcb commented Aug 26, 2024

@Joker-Q4
Copy link
Author

@Joker-Q4
Copy link
Author

Joker-Q4 commented Sep 3, 2024

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);
                });

添加了也不行

@zhuyeHe
Copy link

zhuyeHe commented Sep 4, 2024

我也遇到了同样的问题,使用的是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
Copy link

zhuyeHe commented Sep 4, 2024

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

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时都是不同的线程,但是不太清除为什么强制阻塞后可以执行成功

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants