Spring SecurityでForm認証とRequest-Header認証を併用する場合の実装サンプル

<h1>きっかけ</h1>
とあるWebアプリケーション開発において、以下の2つを同時に実現する要件があった。
①ログイン画面からIDとパスワードを入力することでForm認証ができる
②HTTPリクエストヘッダの特定の属性の値で認証ができる

②は、既存の外部認証システムが、認証成功時にHTTPリクエストヘッダにトークンを設定してリダイレクトするため、それを利用したSSOを実現するという要件。

①単独、または②単独の実装サンプルはインターネットの随所に見られるんだけど、併用するパターンは見受けられなかった（かつそれなりにテクニカルだった）ので、どうやって実現したかを紹介する。

<h1>ソース</h1>
以下のように設定する。
https://github.com/ShandyGaffLover/sample001/blob/master/src/main/resources/spring-security.xml


<h1>解説</h1>
「PRE_AUTH_FILTER」と「FORM_LOGIN_FILER」の2つをカスタマイズする。

```
    <sec:custom-filter position="PRE_AUTH_FILTER" ref="siteminderFilter" />
    <sec:custom-filter position="FORM_LOGIN_FILTER" ref="formLoginFilter" />
```

<h2>PRE_AUTH_FILTERのカスタマイズ</h2>

「PRE_AUTH_FILTER」のカスタマイズは以下のようにRequestHeaderAuthenticationFilterクラスを利用する。Form認証と併用する都合上、exceptionIfHeaderMissingプロパティをfalseに、continueFilterChainOnUnsuccessfulAuthenticationプロパティをtrueに設定する。

```
  <bean id="siteminderFilter" class="org.springframework.security.web.authentication.preauth.RequestHeaderAuthenticationFilter">
    <property name="principalRequestHeader" value="SM_USER"/>
    <property name="authenticationManager" ref="sm_authenticationManager" />
    <property name="exceptionIfHeaderMissing" value="false"/>

    <property name="continueFilterChainOnUnsuccessfulAuthentication" value="true"/>
  </bean>

```


ここで、AuthenticationManager（とAuthenticationProvider）は以下のように定義する。

```
  <sec:authentication-manager id="sm_authenticationManager">
    <sec:authentication-provider ref="preauthAuthProvider" />
  </sec:authentication-manager>

  <bean id="preauthAuthProvider" class="org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider">
    <property name="preAuthenticatedUserDetailsService">
      <bean id="userDetailsServiceWrapper"
          class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper">
        <property name="userDetailsService" ref="customUserDetailsService"/>
      </bean>
    </property>
  </bean>
```
AuthenticationProviderにはPreAuthenticatedAuthenticationProviderクラスを利用する。このときpreAuthenticatedUserDetailsServiceプロパティに与えるUserDetailsServiceはUserDetailsByNameServiceWrapperクラスによりラッパーされたものでなければならない点に注意。

<h2>FORM_LOGIN_FILERのカスタマイズ</h2>
「FORM_LOGIN_FILER」のカスタマイズは以下の通りである。

```
 <sec:authentication-manager id="form_authenticationManager">
    <sec:authentication-provider  user-service-ref="customUserDetailsService" >
    </sec:authentication-provider>
  </sec:authentication-manager>

  <bean id="formLoginFilter" class="org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
    <property name="authenticationManager" ref="form_authenticationManager" />
    <property name="authenticationSuccessHandler" ref="formLoginSuccessHandler" />
    <property name="authenticationFailureHandler" ref="formLoginFailureHandler" />
    <property name="filterProcessesUrl" value="/login" />
  </bean>

```

ここで、カスタムフィルターを使うので、以下のように<http>タグのentry-point-ref属性を設定する必要がある点に注意。

```
  <sec:http auto-config="false" use-expressions="true" entry-point-ref="loginUrlAuthenticationEntryPoint">

　　~中略~

    <sec:custom-filter position="PRE_AUTH_FILTER" ref="siteminderFilter" />
    <sec:custom-filter position="FORM_LOGIN_FILTER" ref="formLoginFilter" />
  </sec:http>

  <bean id="loginUrlAuthenticationEntryPoint" class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
    <constructor-arg value="/loginPage" />
  </bean>

```

<h1>実物</h1>
以下から動作確認ができる。[^1]
https://shandygafflover.herokuapp.com/

①ログイン画面から、usernameに「user」、passwordに「password」を入力しログインすると、認証される。
②HTTPリクエストヘッダの属性「SM_USER」に、値「user」を設定[^2]すると、認証される。
　ここでは以下の記事で紹介されているModHeaderを利用する。

　ブラウザのHTTPヘッダーに外部から認証情報を設定する - Qiita
　https://qiita.com/utwang/items/1eeb25d27e8acea33a8a

[^1]: 大変申し訳ないのですが記事投稿時点ではhttpsに対応していないというダサいことになっています（認証なのに！）。すみません。時間作って対応しておきます。

[^2]: リクエストヘッダの属性名「SM_USER」はあくまでも一例。実際の開発では、属性名は推測しにくいものとし機密事項とすべき。

<h1>実行結果</h1>
HTTPリクエストヘッダの属性「SM_USER」に、値「user」を設定する。
![requestheader.png](https://qiita-image-store.s3.amazonaws.com/0/322008/7b04e084-5929-255d-8d10-7ee600665ee6.png)

この状態で例えば以下のURLにアクセスする。
![url.png](https://qiita-image-store.s3.amazonaws.com/0/322008/01a4e9fc-0daa-8d61-1ab6-e8953a903537.png)

すると認証されるので、ページが表示される。
![greeting.png](https://qiita-image-store.s3.amazonaws.com/0/322008/3048c35e-20bf-0f69-f3b4-d33f6223a28d.png)

HTTPリクエストヘッダに属性「SM_USER」が設定されていない場合はログイン画面に遷移するので、usernameとpasswordにより認証される。
![login.png](https://qiita-image-store.s3.amazonaws.com/0/322008/0b4978e9-7c81-4cd0-f2ec-a17feb3837b7.png)
![greeting.png](https://qiita-image-store.s3.amazonaws.com/0/322008/be42e459-242d-7153-5da7-15ecab3a960e.png)


<h1>参考URL</h1>

Spring Security 使い方メモ　認証・認可 - Qiita
https://qiita.com/opengl-8080/items/032ed0fa27a239bdc1cc

03-1.Spring Security 2の設定方法 - soracane
https://sites.google.com/site/soracane/home/springnitsuite/spring-security/spring-securityno-settei-houhou

テックノート – spring security 認証処理を自作する方法を解説
http://javatechnology.net/spring/spring-security-login-filter-original/

Spring Securityを適用するときの作業メモ - なみひらブログ
http://namihira.hatenablog.com/entry/20160817/1471396975

AbstractAuthenticationProcessingFilter (Spring Security 4.2.6.RELEASE API)
https://docs.spring.io/spring-security/site/docs/4.2.6.RELEASE/apidocs/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.html

Spring Security Java - Multiple Authentication Manager - 2 bean found error - Stack Overflow
https://stackoverflow.com/questions/32105846/spring-security-java-multiple-authentication-manager-2-bean-found-error