Spring Security + Kerberos認証+SPNEGOでSSO(4)<APサーバ側の実装>

散々もったいぶってきてしまいましたが、本命のサーバサイドの実装です。 といってもサンプル的なものを作るだけならそれほどやることは多くありません。

ベースとなるソースコードの準備

(多分)公式なサンプルプロジェクトがGitHub上にあるのでコチラからcloneしておきます。 色々フォルダが並んでいますが記事タイトルでもあるSpringSecurity+Kerberos+SPNEGOで作る場合は以下のプロジェクトをベースに作成します。

  • spring-security-kerberos-samples/sec-server-spnego-form-auth

より詳細にソースを追いかけたい場合は、上記に加えて以下も。

  • spring-security-kerberos-core

サンプルコードを必要に応じて微修正

cloneしたら、とりあえず「spring-security-kerberos-samples/sec-server-spnego-form-auth」の下にあるソールコード『WebSecurityConfig』をeclipseでもVSCodeでもなんでもいいので開きましょう。

Beanに「AuthenticationManager」を追加

  • Spring Bootのバージョンにもよると思いますが、そのままサンプルコードを動かそうとすると「AuthenticationManagerコンポーネントがも見つからない」というようなエラーが出るはずです。なので、それを追加してやります。親クラスのWebSecurityConfigurerAdapterにauthenticationManagerインスタンスを取得できるメソッドがあるのでそれを呼ぶようにします。
@Bean
public AuthenticationManager authenticationManager() throws Exception {
    return super.authenticationManager();
}

AuthenticationManagerBuilder の定義の書き換え他

  • サンプルコードのWebSecurityConfigの中では以下のコードが書かれており、恐らくprovider A(kerberosAuthenticationProvider)で認証できなかったらprovider B(kerberosServiceAuthenticationProvider)で認証する(もしくはその逆)だと思うのですが、そのまま動かした限りではどちらかに失敗すると認証失敗になってしまうのでここも修正します。また、SSOできない状態だった場合用に、従来のID/PW入力による認証に切り替えられるようにしたかったので、そこも修正します。
  • WebSecurityConfig(修正前)
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //  kerberosAuthenticationProvider:  SSOではなく直接ケルベロス認証でログイン 
       //   kerberosServiceAuthenticationProvider:   SPNEGOを利用いてSSO
    auth
        .authenticationProvider(kerberosAuthenticationProvider())            
        .authenticationProvider(kerberosServiceAuthenticationProvider());
}
  • WebSecurityConfig(修正後)
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.authenticationProvider(authenticationProvider());
}
@Bean
public AuthenticationProvider authenticationProvider() {
    AuthenticationProviders providers = new AuthenticationProviders();
    providers.addProvider(pwAuthenticationProvider());
    providers.addProvider(kerberosServiceAuthenticationProvider());
    return providers;
}
@Override
protected PWAuthenticationProvider pwAuthenticationProvider () {
    return new PWAuthenticationProvider(dummyUserDetailService());
}
@Bean
public SpnegoAuthenticationProcessingFilter spnegoAuthenticationProcessingFilter(
              AuthenticationManager authenticationManager) {
    SpnegoAuthenticationProcessingFilter filter = new SpnegoAuthenticationProcessingFilter();
    filter.setAuthenticationManager(authenticationManager);
    filter.setFailureHandler(new AuthenticationFailureHandler() {
        @Override
        public void onAuthenticationFaulure(HttpServletRequest request, HttpServletResponse response,
                 AuthenticationException exception) throws IOException, ServletException {
             // 認証失敗時はログインページにとばす
             response.sendRedirect(redirectLoginUrl + "/login");
        }
    });
    return filter;
}
  • AuthenticationProviders(新規作成)
// (package, importはここでは省略)
public class AuthenticationProviders implements AuthenticationProvider {
    private final List<AuthenticationProvider> providers = new ArrayList<>();

    public void addProvider(AuthenticationProvider provider) {
        this.providers.add(provider);
    }
        
    @Override
    public Authentication authentiate(Authentication authentication) throws AuthenticationException {
         for(AuthenticationProvider provider : this.providers) {
             try {
                 final Authentication auth = provider.authenticate(authentication);
                 if(auth != null) {
                     return auth;
                 } catch(Exception ex) {
                     // サンプルということでprintStackTraceで片付けてます
                     ex.printStackTrace();
                 }
          }
          throw new UsernameNotFoundException("Authentication is failured.");
    }
 
    @Override
    public boolean supports(Class<?> authentication) {
          return true;
    }
}
  • PWAuthenticationProvider(新規作成)
// (package, importはここでは省略)
@Component
@RequiredArgsConstructor
public class PWAuthenticationProvider implements AuthenticationProvider {
    private final UserDetailsService userDetailsService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        UserDetails user = this.userDetailsService.loadUserByUsername(authentication.getName());
        // (パスワードの検証は省略)
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
            user, authentication.getDetails());
        token.setDetails(authentication.getDetails());
        return token;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return true;
    }
}

application.yml(application.propertiesでもやることは同じ)

server:
    port: 8080
app:
    service-principal: HTTP/XXX.kk.example.com@KK.EXAMPLE.COM
    keytab-location: /etc/krb5.keytab

起動

  • 最後に、修正したプロジェクトをビルド、起動して完了です。
java -jar XXXXX.war

動作確認

  1. ADクライアントにリモートデスクトップ接続 f:id:koyak:20211213112236p:plain
  2. ブラウザを起動し、サンプルアプリのURL入力し、SSOできていることを確認 f:id:koyak:20211213112343p:plain

Spring Security + Kerberos認証+SPNEGOでSSO(3)<ADクライアント側の設定>

※前回の更新から早速間があいてしまった上に記述が汚くて申し訳ないです。随時見直し、整理していくつもりです。ご容赦ください。

ActiveDirectoryの検証環境を作る際、地味に苦労したのがActiveDirectoryで認証するクライアント用の各種設定。単にActiveDirectoryにPCを参加させるだけなら簡単なのですが、AWS上に環境を作ってリモートデスクトップ接続もできるようにしようとすると、意外と苦労します。

注:(2021/11/28)セキュリティグループ設定、ADクライアント用のADユーザ追加について、前回の記事に更新があります。ご了承ください。

手順

  • AWSWindows Server 2019のインスタンスを作成(以後、クライアント)
    • ※Windows10のインスタンスが見当たらなかったので代用。スペックは求めていなかったのでt2.small
  • 作成したクライアントにリモートデスクトップ接続します。この時点のログインユーザは「Administrator」です。
  • AWS上で構築した場合、時刻や言語設定が米国仕様になっているので日本に合わせておきましょう。Windowsの検索欄から「言語」と入力すると設定画面を開けます。
  • コンピュータの管理>ローカルユーザーとグループ>ユーザ>(右クリック)>新しいユーザ
  • ADサーバ側に登録しているユーザと被らない名前(AdministratorやGuest以外)のユーザを作成(これをやっておかないとクライアントをAD参加させた後、何もできなくなることがある)
    • 権限は管理者(Administrator)にしておく
  • インターネットオプションでDNS設定
    • コントロールパネル>ネットワークとインターネット>ネットワークと共有センター
    • 「アクティブなネットワークの表示」の「接続」欄に表示されているリンクをクリック
    • f:id:koyak:20211128080803p:plain

      ネットワークの設定
    • プロパティ>Internet Protocol Version 4(TCP/IPv4)>プロパティ
    • 「優先DNSサーバ」「代替DNSサーバ」にADサーバのプライベートIPアドレスを設定します。自分の場合はDNSサーバはADサーバ内で稼働する一台しか用意しなかったので、「優先DNSサーバ」のみ設定していました
    • f:id:koyak:20211128081131p:plain

      DNS設定
  • 最初に作ったユーザーでログイン
  • コントロールパネル>システムとセキュリティ>リモートアクセスの許可
  • 「リモート」タブを開き、「このコンピューターへのリモート接続を許可する」
    • ※この設定をしておくことで、後述のActiveDirectoryに参加させた後、「リモートデスクトップ接続できなくなった!」という時に色々調査できるようになります
  • 「コンピュータ名」タブを開く
  • "コンピュータ名を変更したりドメインやワークグループを変更したりするには[変更]をクリックしてください。"と書かれた欄に表示されている「変更」ボタンをクリック
  • 所属するグループ>ドメイン欄に、ADサーバで設定したドメイン名を入力
  • OKボタンを押下
  • 管理者ユーザ名、パスワード入力を求められるので入力
  • "kk.example.com(ADサーバで設定したドメイン名)にようこそ"とダイアログが表示されるのでOKボタンを押下
  • "再起動する必要があります"と表示されるのでこれもOK
    • 警告が出ることがありますが、その場合は、ADサーバ側の設定(セキュリティグループなど)
  • PCを再起動
  • リモートデスクトップ接続し直す場合、ユーザ名にドメイン名を含めるようにする
  • f:id:koyak:20211128084210p:plain

    リモートデスクトップ接続
  • ADユーザのパスワード入力を求められるので入力
  • 正常に設定できていれば余程遅いネットワークを使っていない限りは1秒以内くらいで画面が切り替わってログインできるはずです。10秒以上待っても何も反応がない場合はAD参加、またはADユーザによるリモートデスクトップ接続設定が正常に行われていない可能性があります
    • ADクライアント側の設定を見直す場合は、作成したユーザでログイン(ユーザ名にドメイン名は含めず".\(ユーザ名)"という形で入力)し、各種設定をご確認ください
    • または、ADユーザにそもそもリモートデスクトップ接続が許可されていない可能性があるので、ADサーバ側のユーザ設定も見直してみてください

Windows認証設定

最後に、ブラウザがWindows認証を使うように設定する必要があります。

コチラに書かれている手順で設定を行えば、ADクライアント側のSSO準備は完了です。Edge向けに書かれていますが、Chromeなどでも同じ手順でOKです。

Spring Security + Kerberos認証+SPNEGOでSSO(2)<ActiveDirectory側の設定>

ユーザ管理、認証を行うActiveDirectory(以下、AD)側の設定について、以下の作業を行います(既にActiveDirectoryインストール済ならユーザ追加から)。

  • ADサーバの用意
  • ActiveDirectoryのインストール
  • クライアント、SSOで認証させたいアプリが稼働しているサーバ(以下、APサーバ)用のADユーザを追加
  • APサーバ用のSPN(サービスプリンシパル名)を登録
  • APサーバ用のキータブファイルの作成
  • (それ以外にDNSの逆引き登録もあるようですが、なくてもSSOはできるようなので省略)

注:ActiveDirectoryで検索すると、「Azure Active Directory」やAWSが提供しているActiveDirectory管理サービスなどがヒットすることがありますが、この記事では素のActiveDirectoryを使う場合について書いていきます。

ADサーバの用意

私はADサーバもクライアントもAPサーバもAWS上に構築しました。

ADサーバは、MarketPlaceからWindows Server 2019 Baseを選択し、インスタンスはt2.large、ストレージはデフォルトのままで作成しました。
(勿論、本番で運用するにはもっと高スペックにする必要があると思います)

セキュリティグループの設定

以下、ADクライアントも含めた「全部のせ」のセキュリティグループ設定(インバウンドのみ)を挙げます。実際の本番運用では各サーバの役割に応じて、アウトバウンドも含めて適切な設定をそれぞれ行う必要があります。

※設定しなくてもいい項目が含まれているかもしれませんが、ご了承ください。

許可項目 プロトコル ポート番号 許可対象
NTP UDP 123

同サブネット

RPC UDP 135

同サブネット

RPC TCP 135

同サブネット

ICMP(ping) ICMP すべて

同サブネット

LDAP TCP 389

同サブネット

LDAP UDP 389

同サブネット

LDAPS TCP 636

同サブネット

DNS UDP 53

同サブネット

DNS TCP 53

同サブネット

Kerberos-kdc-admin TCP 749

APサーバ

Kerberos-kdc TCP 88

APサーバ

Kerberos認証 TCP 464

同サブネット

RDP(内部) TCP 3389

同サブネット

RDP(作業PC) TCP 3389

作業環境のグローバルIP

rpc動的ポート TCP 1024~65535

同サブネット

Netlogon UDP 137-138

同サブネット

Netlogon TCP 139

同サブネット

グローバルカタログ TCP 3268-3269

同サブネット

smb TCP 445

同サブネット

smb UDP 445

同サブネット

RSync TCP 873

同サブネット

ActiveDirectoryのインストール

インストール手順は色々なところで紹介されていますので、「ActiveDirectory インストール」と検索すれば、わかりやすい手順が紹介されているはず。

私は以下のブログを参考にさせていただきました。

REMSYSTEM:Windows Server 2019でActive Directoryをインストールする手順

 

ここでドメイン名を決めておく必要があります。

この記事では、検証用のドメイン名として、「KK.EXAMPLE.COM」というドメイン名をつけています。

APサーバ(以下、サーバ)用のADユーザを追加

起動したWindows Serverにリモートデスクトップでログインし、「Windows管理ツール」から「ActiveDirectory ユーザとコンピュータ」を選択。

f:id:koyak:20211113212935p:plain

「Users」下に新しいユーザを追加(ここではkrb5-testと命名)。

f:id:koyak:20211113213321p:plain

ADユーザの追加

あくまでAPサーバが使うユーザなのでリモートデスクトップ接続の権限設定は不要です。

次にKerberos認証にでSSOを行う際に必須となる、SPNの登録とkeytabの発行を行います。

SPNの登録手順は以下の通りです。

  • コマンドプロンプトをたちあげ(多分PowerShellでも可)、カレントディレクトリはどこでもいいので以下のコマンドを実行。
  • setspn -a HTTP/XXXXX.XXX.XXX krb5-test
  • XXXXX.XXX.XXXはAPサーバのDNS名が入ります。AWSで構築している場合、「プライベートIP v4 DNS」と書かれている名前などがそのまま使えます。

f:id:koyak:20211113215044p:plain

DNS名の例
  • 以下のコマンドで登録内容を確認できます。
  • setspn -l account name
  • "krb5-test"は↑で追加したADユーザに読み替えてください。

続いてkeytabファイルの発行

  • 同じくコマンドプロンプトを立ち上げ、以下のコマンドを実行。
  • ktpass /princ HTTP/XXXXX.XXX.XXX@KK.EXAMPLE.COM /mapuser krb5-test@KK.EXAMPLE.COM /pass ******** /out krb5.keytab /crypto all /ptype KRB5_NT_PRINCIPAL /mapop set
  • XXXXX.XXX.XXX部分にはsetspnしたときと同じ名前が入ります。
  • /mapuser、/passには追加したADユーザのログオン名(ドメイン名部分は大文字)、パスワードを入力してください。
  • 特にパスを指定しなければkeytabファイルはカレントディレクトリに出力されるため、別のところに出力したい場合は/outの内容を任意のパスに書き換えます。

出力したkeytabをAPサーバにアップロードしておきます。

テスト時のADクライアント用のADユーザを追加

先ほどのAPサーバ用のADユーザと同様の手順でADユーザを追加します。

このままでは、この作成したADユーザでADクライアントPCにリモートデスクトップ接続できないため、以下の設定を行ってリモートデスクトップ接続を許可します。

  • Windows管理ツール>ActiveDirectoryユーザとコンピュータ
  • 左側のペイン上に表示されている、作成したドメイン名下の「Build In」というフォルダをクリック
  • グループ名が表示されるので、その中の「Remote Desktop Users」を選択
  • 「メンバー」タブを選択し、「追加」ボタンを押下
  • 「選択するオブジェクト名を入力してください」と書かれているテキストエリアに、作成したADユーザを入力し、OKボタンを押下
  • これでADユーザにリモートデスクトップ権限が割り当てられます。

Spring Security + Kerberos認証+SPNEGOでSSO(1)<前書きと検証環境の構成>

10年近くぶりの更新です。

若干古いネタになりますが、WindowsクライアントからLinux上で動くアプリケーションサーバへSSOで接続できるようにするためにここ最近色々検証する機会があったので、書き残しておこうと思います。

ブログを書くのは久々なのでリハビリ代わりに小分けにして書かせていただきます。ご容赦ください。

 

 対象

  • SSO、Kerberos認証、SPNEGOなどの理屈は大体理解したけど、具体的にどうJavaで実装すればいいか、困っている方

検証に使った環境の構成

  • OS
  • 認証方式:Kerberos認証、SPNEGO
  • 言語:Java (Adopt Open JDK 11)
  • FW:Spring Boot(Spring Security)
  • その他
    • 検証環境はAWS上に構築(なのでクライアントもWindows Server2019)
    • 無償利用分だけだとスペック的にきつそうだったため、有償のインスタンスも使って構成しているので注意
      • ADはt2.large、APはt2.middle、クライアントはt2.small

Androidでのメール送信処理を実装する際につまづいたこと

現在、思うところあってAndroidでのメール送信処理を書いています。
その際につまづいたことを備忘録として書き連ねてみます。
送信処理そのものは検索すると色々見つかるので割愛させていただきます。

コードビハインドを使わずにWPF画面を作ろうとしたときに最初にぶち当たりそうな壁(Windowの操作)

 久方ぶりにSeasar.NETのリリース情報以外の更新です。
 ネタは枯れきっているわけでもなく最新の技術ってわけでもないWPF
 微妙なネタでごめんなさい。


 現在、ちまちまとWPFを個人的にさわっております。
 基本方針は「WindowクラスにVisualStudioが自動生成する以外のコードを書かない」。
 (MVVMやデザインとコードの分離、といった真面目な議論は割愛させていただきます)


 環境は.NET Framework4.0。ライブラリは下記を使用しています。さすがにこれが無いと色々と面倒そうです。
 Microsoft Expression Blend 4 Software Development Kit (SDK) for .NET 4
 このライブラリの使い方については検索すると色々引っかかるため割愛させていただきます。


 以下、WPF画面(System.Windows.Windowクラスを継承した画面クラス)のことを『Window』と呼ばせていただきます。


 上記方針でぶち当たった壁が「Windowをどうやって操作するか?」です。
 『EventTrigger』『TriggerAction』『Command』などを組み合わせればイベントを捕まえるのは簡単なのですが、画面を閉じる、などのWindow操作を行おうとしたときに「……で、Windowオブジェクトはどう取得すればいいの?」となります。コードビハインドを使う場合はWindowクラスの中にイベント処理を書くので、出くわすことはない壁です。


================================================================================================================
2012.09.06追記 後述のように面倒な処理を書かなくても簡単にWindow操作を行える方法を教えていただきました!
いやはやお恥ずかしい。。。 Window.GetWindowなるメソッドを使えばOKです。例えば「画面を閉じる」処理を書きたい場合はこんな感じで。


/// Windowを「閉じる」アクションクラス
public class CloseWindowAction : TriggerAction {
protected override void Invoke(object parameter) {
Window.GetWindow(AssociatedObject).Close();
}
}
}
 自分への戒めのために、元の文章も残しておきますorz ↓↓↓
================================================================================================================


 上記問題を解決する一案としてこんな実装を考えてみました。


 まずは画面のxamlコード。











 「閉じる」ボタンを押すと画面を閉じる。それだけの画面です。
 (Windowの名前が『ConfigWindow』となっているのは自分が作っている某アドインのコードをそのまま持ってきているためです。どうかご了承&読み替えていただけたらと思います)


 上記画面で『閉じる』ボタンを押下するとバインドされた『CloseWindowCommand』が呼ばれます。
『CloseWindowCommand』はViewModel側で定義します。

namespace VSArrange.Config.ViewModel {
/// 設定画面ViewModel
public class ConfigWindowViewModel {
private readonly PlainRequest _closeWindowRequest = new PlainRequest();

///

Windowを閉じるイベント通知
public PlainRequest CloseWindowRequest {
get { return _closeWindowRequest; }
}

///

Windowを閉じるCommand
public ICommand CloseWindowCommand {
get { return new DelegateCommand(() => CloseWindowRequest.Raise(), null); }
}
}
}
 『CloseWindowCommand』が呼ばれると『CloseWindowRequest.Raise』メソッドを実行し、"画面を閉じる"イベントを発生させます。
サンプルで使用している『DelegateCommand』『PlainRequest』の実装は下記の通りです。

namespace AddInCommon.Presentation.Command {
/// 処理委譲Commandクラス
/// 匿名メソッドに処理を委譲するCommand
public class DelegateCommand : ICommand {
/// Executeメソッド委譲処理
private readonly Action _invokeExecute;

///

CanExecuteメソッド委譲処理
private readonly Func _invokeCanExecute;

///

コンストラクタ
///
///
public DelegateCommand(Action invokeExecute, Func invokeCanExecute) {
if (invokeExecute == null) { throw new ArgumentNullException("invokeExecute"); }
_invokeExecute = invokeExecute;
_invokeCanExecute = invokeCanExecute;
}

#region ICommand メンバー

public bool CanExecute(object parameter) {
return _invokeCanExecute == null ? true : _invokeCanExecute();
}

public event EventHandler CanExecuteChanged;

public void Execute(object parameter) {
_invokeExecute();
}

#endregion
}
}


namespace AddInCommon.Presentation.InteractionRequest {
/// 単純にイベントを通知するだけの汎用イベント発生クラス
public class PlainRequest {
/// 汎用イベント
public event EventHandler Raised;

///

イベント発生
public void Raise() {
var handle = this.Raised;
if (handle != null) {
handle(this, new EventArgs());
}
}
}
}
 Raiseメソッドが呼ばれると『RequestTrigger』で"画面を閉じる(Raise)"イベントとして処理します。

namespace AddInCommon.Presentation.InteractionRequest {
/// 汎用イベントトリガー
public class RequestTrigger : EventTrigger {
protected override string GetEventName() {
// Requestクラスのイベント名
return "Raised";
}
}
}
 "画面を閉じる(Raise)"イベントが発生するとxamlで紐づけられた『CloseWindowAction』が呼ばれ、Windowを閉じる処理が行われます。

namespace AddInCommon.Presentation.TriggerAction {
/// Windowを「閉じる」アクションクラス
public class CloseWindowAction : TriggerAction {
protected override void Invoke(object parameter) {
AssociatedObject.Close();
}
}
}
 ようやく辿り着きました。『TriggerAction』とすることでAssociatedObjectを介してWindowオブジェクトを操作できます。


 長くなってしまいましたが、処理の流れをまとめると下記のようになります。
<閉じるボタン押下(画面操作)>

『CloseWindowCommand』で『CloseWindowRequest』呼び出し

『CloseWindowRequest』で"画面を閉じる"イベント発生

『RequestTrigger』で"画面を閉じる"イベントをキャッチし『CloseWindowAction』呼び出し

『CloseWindowAction』でWindowオブジェクトのCloseメソッド呼び出し

<画面を閉じる>


 パッと見、コードビハインドで同様の処理を書く場合に比べてかなり面倒です。
 しかし、上記のようなクラスを予め作成しておくことで使い回しがきく上に『CloseWindowAction』以外はWindowに依存していないためテストコードを書きやすくなります。
 『Prisim』などのライブラリを活用したり、ジェネリックやAction,Funcを使った処理委譲と組み合わせれば、より汎用的な部品が作ることが可能です。

S2Container/S2Dao.NET 1.4.0 RC3をリリースしました

一年って、早いですね。

S2Container/S2Dao 1.4.0 RC3をリリースしました。

変更点は下記ページをご覧下さい。

不具合修正の反映が中心です。

http://www.seasar.org/wiki/index.php?cmd=edit&page=SeasarWhatsNew%2F2012-08-13

DBFlute.NET 0.8.9.45の下記exampleプロジェクトで

動作確認をしています。

・dfnet-asp.net-example

・dfnet-basic-example

・dfnet-multipledb-quill-example