Spring Cloud GatewayのWebFilterでSa-Tokenを使用する際の「SaTokenContext未初期化」エラーの解消方法

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、遅すぎるとスレッドリソースの競合を引き起こします。
  • フィルター優先度の差異: WebFilterGlobalFilter よりも優先度が高く評価されるため、ルーティングマッチング以前に実行される必要がある場合、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"));
    }
}

タグ: Sa-Token spring-cloud-gateway WebFlux Reactive-Streams java-security

8月22日 01:50 投稿