月: 2025年6月

  • Spring Security の認証処理をカスタマイズ

    今までSpring Security の認証処理を理解できていなかった。
    単純なパスワード突合だけなら、UserDetailsService を実装してDB からselect するだけで良いので何とかそれで乗り切っていた。
    しかしながら、現在のプロジェクトで複雑なログイン要件が追加されたため、それだけでは立ち行かなくなってしまった。

    参考までに追加された要件

    • パスワードを連続5回失敗失敗したらアカウントをロックする
    • パスワード変更の有効期限を過ぎたらアカウントをロックする

    いろいろ探しところ、DaoAuthenticationProvider のadditionalAuthenticationChecks() を継承すると非常に柔軟に実装できることが分かった。

    コードはこちら。

    @Component
    @Slf4j
    public class AppAuthenticationProvider extends DaoAuthenticationProvider {
    
      public static final String DEFAULT_LOGIN_ERROR = "ユーザー名またはパスワードが違います";
      public static final String ERROR_TOO_MANY_ATTEMPTS = "連続5回ログインに失敗しました";
      public static final String ERROR_PASSWORD_EXPIRED = "パスワードの有効期限が過ぎています";
    
      private final PlatformTransactionManager transactionManager;
      private final AccountRepository accountRepository;
    
      public AppAuthenticationProvider(PasswordEncoder passwordEncoder, UserDetailsService userDetailsService, PlatformTransactionManager transactionManager, AccountRepository accountRepository) {
        super(passwordEncoder);
        // 親クラスで必要
        setUserDetailsService(userDetailsService);
        this.transactionManager = transactionManager;
        this.accountRepository = accountRepository;
      }
    
      @Override
      protected void additionalAuthenticationChecks(
          UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
    
        // 5回以上ログイン失敗している場合はログイン不可
        if (LOGIN_FAIL_MAX_COUNT <= userDetails.getLoginFailCount()) {
          throw new AppLoginException(ERROR_TOO_MANY_ATTEMPTS);
        }
    
        // パスワード変更期限を過ぎていたらログイン不可
        if (userDetails.getPasswordExpiredDate() != null && now().isAfter(account.getPasswordExpiredDate())) {
          throw new AppLoginException(ERROR_PASSWORD_EXPIRED);
        }
    
        // パスワードチェック
        try {
          // 親クラス(DaoAuthenticationProvider)のパスワードチェックを呼び出す
          // パスワード相違時は例外がthrow される
          super.additionalAuthenticationChecks(userDetails, authentication);
          // パスワード一致時はカウンターをリセットする
          new TransactionTemplate(transactionManager).executeWithoutResult(status -> {
            // spring security から呼ばれるのでトランザクションを持っていない
            accountRepository.resetFailCounter(userDetails.getAccountId());
          });
        } catch (AuthenticationException ae) {
          // パスワード相違時は、カウンターをインクリメント
          new TransactionTemplate(transactionManager).executeWithoutResult(status -> {
            // spring security から呼ばれるのでトランザクションを持っていない
            accountRepository.incrementFailCounter(userDetails.getAccountId());
          });
          // すでに4回失敗していたら、エラーメッセージを変更する
          if ((LOGIN_FAIL_MAX_COUNT - 1) <= userDetails.getLoginFailCount()) {
            throw new AppLoginException(ERROR_TOO_MANY_ATTEMPTS);
          } else {
            throw new AppLoginException(DEFAULT_LOGIN_ERROR);
          }
        }
      }
    }

    こちらを@Bean 登録しすると有効になる。

    @Bean
    public AuthenticationManager authenticationManager(AppAuthenticationProvider provider) {
      return new ProviderManager(provider);
    }

    ビルトインの機能を使って、UserDetails のisAccountNonExpired() で似たような実装を行うこともできる。その場合、すでに4回失敗した状態でパスワード相違した時にメッセージを変更することができないと思った。

    throw された例外は、formLogin のfailureHandler に入ってくるので、それを一時的にセッションへ格納し、ログイン画面を表示するコントローラでセッションの値を取得して画面に表示すると良い。
    セッションの値は一度限りで即時削除する。(redirectAttribute と同じだがここでは使えない)

    また、failureHandler を使わず、additionalAuthenticationChecks() でセッションに例外を詰めることもできるが、ユーザ名不正の場合は、additionalAuthenticationChecks() が呼ばれないのでハンドリングできない。(1敗)

    【広告】

  • 購入履歴取り込み変更点

    先日更新した、アマゾン購入履歴csv取得の変更点について説明します。

    変更前(v1.1) まではこちらの方式でした。

    ポップアップからコンテンツへ直接指示を出していました。

    この方式の問題は、ポップアップが突然閉じてしまうと、収集処理が途中で止まってしまうことです。

    更新後(v1.2+)の方式がこちらです。

    バックグラウンドは画面を持たない仮想のタブのようなものです。こちらは、ポップアップと違って、閉じることがありません。そのため、安定して全ページを取得することができます。

    バックグラウンドについてはこちらの記事が分かりやすかったです。

    【広告】

  • recaptcha v2 とv3 でタグが異なる

    下のエラー原因が分からず、数時間を無駄にした。悲しいので記録を残す。

    結論は件名の通り。公式マニュアルのコードを例に出して違いを記載する。

    v2 の正しい書き方

    <div class="g-recaptcha" data-sitekey="your_site_key"></div>

    v3 の正しい書き方

    <button class="g-recaptcha" data-sitekey="reCAPTCHA_site_key" 
            data-callback='onSubmit' 
            data-action='submit'>Submit</button>

    そして、私がやっていたダメな書き方(やってはいけない)

    <div class="g-recaptcha" data-sitekey="reCAPTCHA_site_key" 
            data-callback='onSubmit' 
            data-action='submit'>Submit</div>

    v3 を使う場合、<button> タグが正解であり、<div> はエラーになる。

    もう少し別のエラー表記にしてもらえれば、時間を無駄にしなかったのに。。。

    タグの間違い以外は、こちらのサイトが詳しかった。

    【広告】

  • アマゾン購入履歴CSV取得Chrome Extension を更新しました

    Chrome Extension でアマゾンの購入履歴をCSV としてダウンロードするアプリに、以下の点を改良してリリースしました。

    • 文字コード選択(SJIS or UTF8)
    • UI改善
    • 動作が途中で止まるバグ対応

    正直に言いましてChrome Web Store を作成以来放置しておりました。先週になってようやくコメントをいただいていたことに気づきました。(遅くなってすみません)
    多くが文字コードに関するものでした。

    Chrom extenson デベロッパーコンソールが上の画像で、ここから約8割の方がwindows での利用されている状況と分かりました。そのため、SJIS でダウンロードする機能が必要と考えて、文字コード選択機能を追加しました。

    よかったら使ってみてください。
    アマゾン購入履歴取得ツール

    SJIS の変換は encoding-japanese を使わせていただきました。
    https://www.npmjs.com/package/encoding-japanese

    【広告】