한국어 | English | 日本語
Webアプリケーションエンジニア (経験8.8年)
技術・開発
engineering
ウェブフロントエンドと バックエンド開発を扱います

Spring Security: SecurityContextHolder のスレッド共有戦略

並列処理のために導入した`parallelStream`内でSpring Securityのセッション情報が断続的に消失するという奇妙なバグに遭遇しました。「シュレーディンガーの猫」のように、リロードするたびに結果が変わるこの現象の原因を掘り下げ、SecurityContextHolderがマルチスレッド環境でセッションデータを共有する3つの戦略について考察します。
リストフィルタリングロジックに`parallelStream`を適用した際、一部のスレッドでのみ権限確認が失敗する原因を分析します。SecurityContextHolderのデフォルト戦略である`MODE_THREADLOCAL`が子スレッドとコンテキストを共有できないことで発生する問題を診断し、これを解決するための`MODE_INHERITABLETHREADLOCAL`など、共有モードごとの特徴と注意点を学びます。

多数の情報をリスト表示するページで、現在ログインしているユーザーが持つ権限に基づいて一部の情報が表示されないようにする処理が必要でした。そこで、まずAPIからリストを取得し、現在のSpring Securityログインセッションに保存されている権限を使用して一部の情報をフィルタリングし、最終的に表示ページにレンダリングする作業を行いました。

しかし、不思議なことに、リストに表示される行が全部で10個ある場合、約2〜3個、つまり約1/4の行にのみ「セッション権限フィルタリング」ロジックが適用され、残りの3/4には適用されないというバグを発見しました。 さらに、1/4に該当する2〜3個は、リロードするたびに不規則に変化していました。例えば、一度リロードすると2番目と3番目の行に「セッション権限フィルタリング」が適用され、もう一度リロードすると5番目と6番目の行に適用されるといった具合です。まるでシュレーディンガーの猫のようでした…

実装は以下の通りでした。

List<SomeInformation> list = someApi.retreive(condition);
list.parallelStream()
    .forEach(each -> {
        if (!SecurityHelper.hasRole("ROLE_CAN_SEE_SENSITIVE_NUMBERS")) {
            each.setSensitiveNumber1(null);
            each.setSensitiveNumber2(null);
        }
    })
public class SecurityHelper {

    public static boolean hasRole(String role) {
        SecurityContext context = SecurityContextHolder.getContext();
        if (Objects.isNull(context))) {
            return false;
        }
        Authentication authentication = context.getAuthentication();
        if (Objects.isNull(authentication))) { // `auth`を`authentication`に修正
            return false;
        }
        for (GrantedAuthority eachAuthority : authentication.getAuthorities()) { // `GrantAuthority`を`GrantedAuthority`に修正
            if (role.equals(eachAuthority.getAuthority())) {
                return true;
            }
        }
        return false;
    }
}

実際にテストしたログインアカウントにはROLE_CAN_SEE_SENSITIVE_NUMBERS権限があったため、リストのすべての行でsensitiveNumber1sensitiveNumber2が正常に表示されるのが正しいはずです。しかし、1/4しか表示されないのはどう考えてもおかしいので、parallelStream.forEachの内部にログを追加してみたところ、以下の結果が得られました。

INFO 2021-01-01 00:00:01 [ForkJoinPool.commonPool-worker-3] [TEST] hasRole: false
INFO 2021-01-01 00:00:01 [ForkJoinPool.commonPool-worker-2] [TEST] hasRole: false
INFO 2021-01-01 00:00:01 [ForkJoinPool.commonPool-worker-7] [TEST] hasRole: false
INFO 2021-01-01 00:00:01 [ForkJoinPool.commonPool-worker-1] [TEST] hasRole: false
INFO 2021-01-01 00:00:01 [http-nio-80-exec-3] [TEST] hasRole: true
INFO 2021-01-01 00:00:01 [ForkJoinPool.commonPool-worker-4] [TEST] hasRole: false
INFO 2021-01-01 00:00:01 [ForkJoinPool.commonPool-worker-5] [TEST] hasRole: false
INFO 2021-01-01 00:00:01 [ForkJoinPool.commonPool-worker-6] [TEST] hasRole: false
INFO 2021-01-01 00:00:01 [ForkJoinPool.commonPool-worker-2] [TEST] hasRole: false
INFO 2021-01-01 00:00:01 [http-nio-80-exec-3] [TEST] hasRole: true

見てみると、ForkJoinPool、つまりParallelStreamの実行のために割り当てられた子スレッド(ForkJoinPool.commonPool-worker-1~7)ではhasRoleが異常にfalseを返し、メインスレッド(http-nio-80-exec-3)ではhasRoleが正常にtrueを返していることがわかります。どうやらParallelStreamSecurityContextHolderの併用が問題のようです。

SecurityContextHolder のスレッド間共有モード

ParallelStreamのスレッドでhasRole = falseが返された第一の原因は、SecurityContext context = SecurityContextHolder.getContext()呼び出し時にnullが返されていたことです。一方、メインスレッドでSecurityContextHolder.getContext()を呼び出すと、正常にセッションデータを取得でき、hasRoleに適切な比較ロジックを実行できました。調べてみたところ、以下の事実を発見しました。

SecurityContextHolder は、SecurityContext ログインセッション情報をどのレベルのスレッドまで共有するかモードを指定できるようになっています。デフォルト値はMODE_THREADLOCALであり、SecurityContext 情報は**「メインスレッド」**でのみ見ることができます。

共有モードは合計3種類あります。

デフォルトモードはMODE_THREADLOCALだったため、何も設定していなかったサーバーでは、メインスレッド(http-nio-80-exec-3)でのみSecurityContextが返され、残りの子スレッド(ForkJoinPool.commonPool-worker-1~7)ではnullが返されていたのです。

まとめ

SecurityContextHolderのデフォルト設定は、SecurityContext情報をローカルスレッドのみで共有するように設計されているため、SecurityContextHolderを子スレッド内で直接呼び出して使用するよりも、メインスレッドで呼び出してその値を子スレッドから参照する方が、パフォーマンス的にもコードの可読性的にもよりクリーンなコードになるでしょう。

ParallelStreamAsync関連の機能を使用する際、子スレッドでSecurityContextHolderを使用する必要がある場合は、SecurityContextHolderの共有モードをMODE_INHERITABLETHREADLOCALに切り替えることを検討すべきです。



Spring Security: SecurityContextHolder のスレッド共有戦略
Author
Aaron
Posted on
Licensed Under
CC BY-NC-SA 4.0
CC BY-NC-SA 4.0
同じカテゴリーの関連記事
最新記事
LLMフィルターが奪う会話の筋肉とコミュニケーション様式
会話における無礼さを濾過し、洗練された回答を生成するLLMツールが日常化した現代において、私たちは本当に思慮深い会話をしているのだろうか?リアルタイムのコミュニケーションにおける数多くの失敗を通じて磨かれるべき会話能力が、外部ツールに依存することで退化している現象と、それがもたらす社会的な不安や世代間の行動様式の変化について考察する。
シニア採用における年俸交渉の最適なタイミングと戦略
年俸交渉は単なる数字の交換ではなく、心理的な駆け引きとタイミングが重要です。本稿では、企業側にとって、候補者が計算的な態度を取りがちな最終合格後よりも、採用プロセスの初期段階から段階的に交渉を進めることが、なぜより効率的であり、率直な情報の共有に繋がるのかを考察します。
法治主義の限界と人間の多様性
全ての人間の行為を単一の法体系で規制できるという信念は、傲慢であるかもしれない。この記事は、中世の階層的な統制から脱却し、現代の無限の自由を手に入れた人類が直面する法治主義の逆説と、多様性という名のもとに深化する社会的強制力と他者への悪魔化現象を鋭く分析する。
토스트 예시 메세지