Spring Cloud Gateway(WebFlux基盤)の開発環境において、`GlobalFilter`内でSa-Tokenの認証APIを呼び出すと、ランタイム時に以下のような例外が発生することがあります。
// 問題が発生する実装例
@Component
@Slf4j
public class UserIdPropagator implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange ctx, GatewayFilterChain chain) {
Long userId = StpUtil.getLoginId(-1L);
ServerHttpRequest updatedReq = ctx.getRequest().mutate()
.header("X-Auth-User", String.valueOf(userId))
.build();
return chain.filter(ctx.mutate().request(updatedReq).build());
}
}
実行すると SaTokenContextException(コード: 10002)がスローされます。これは内部のコンテキストホルダーが空の状態でアクセスされた結果です。
根本原因
Sa-Tokenの内部実装では、リクエストスコープのデータを SaTokenContextModelBox に格納して管理しています。Servlet環境ではThreadLocalに依存しますが、WebFlux環境ではReactor Contextや専用ホルダーを利用します。フィルターチェーンの評価順序により、初期化処理よりも先に認証ロジックが実行されると、モデルボックスがnull判定され例外が発行されます。
解決策の実装
Servlet向け公式ドキュメントとは異なり、Reactive系フレームワークでは SaReactorHolder を介したコンテキストバインディングが必要です。リクエスト開始時にコンテキストを束縛し、応答完了後に後始末を行うフィルターを追加します。
@Component
@Slf4j
@Order(SaTokenConsts.SA_TOKEN_CONTEXT_FILTER_ORDER)
public class SaTokenReactorInitializer implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.debug("[Sa-Token] Initializing context for reactive pipeline");
SaReactorSyncHolder.setContext(exchange);
return chain.filter(exchange)
.doFinally(signalType -> SaReactorSyncHolder.clearContext());
}
}
実装上の重要ポイント
- 実行順序の制御:
@Orderアノテーションには定数SA_TOKEN_CONTEXT_FILTER_ORDER(標準値:-104)を指定し、通常の認可フィルターより先に動作させる必要があります。 - 非同期クリーンアップのタイミング: コンテキスト破棄処理は必ず
chain.filter(...).doFinally(...)のチェーン末尾に配置してください。早すぎると後続フィルターでNullPointer、遅すぎるとスレッドリソースの競合を引き起こします。 - フィルター優先度の差異:
WebFilterはGlobalFilterよりも優先度が高く評価されるため、ルーティングマッチング以前に実行される必要がある場合、WebFilterインターフェースを実装する方が安全です。 - 状態検証: 現在有効なSa-Tokenインスタンスが存在するかは
SaManager.getSaTokenContext().isValid()で検出可能です。
これらの要件を網羅した最終的なフィルター実装は以下の通りです。
@Component
@Slf4j
public class SaTokenWebFluxBridge implements WebFilter {
private static final int BRIDGE_ORDER = SaTokenConsts.SA_TOKEN_CONTEXT_FILTER_ORDER;
@Override
@NonNull
public Mono<Void> filter(@NonNull ServerWebExchange exchange, @NonNull WebFilterChain chain) {
log.trace("[Sa-Token-Bridge] Starting reactive lifecycle");
// コンテキスト設定と交換オブジェクトの同期
SaReactorSyncHolder.setContext(exchange);
ServerWebExchange synchronizedExchange = SaReactorSyncHolder.getExchange();
return chain.filter(synchronizedExchange)
.contextWrite(ctx -> SaReactorHolder.setContext(ctx, exchange, chain))
.doFinally(signal -> log.trace("[Sa-Token-Bridge] Lifecycle teardown"));
}
}