spring securityでlogin画面を使わない様な独自の認証方式を実現する方法について記載するページ。

TL;DR

さくっと実装方法が知りたい場合はspring security preauthでググると速いかも。
結論を先に書いておくとAbstractPreAuthenticatedProcessingFilterとAuthenticationUserDetailsServiceを実装すれば一通りの認証は実装できそうである。※ただし認証のみ。細かな部分については周辺の実装が必要になる。

spring securityのカスタム認証

oauthでもないけど認証情報は手元になくて、自アプリでは認証せず他の認証サーバと連携してといった独自の認証を実装しなければならないことがまれにある。そういった場合はSpring securityが持っているForm認証や、ベーシック認証で対応できないので独自の認証方式を実装しなきゃと思うことがありますが、そういった場合でもSpring securityで対応できます。

例えば、他のシステムと連携してユーザ認証後、認証結果のみが連携されるまたは、認証Tokenがリクエストヘッダーやクエリパラメータなどに付与されて送られてきてそれを認証サーバに問い合わせを行って認証するとか。他にも様々な独自の認証方式があるかと思います。そういった場合でもSpring Securityで対応できるようにSpring securityは設計されているため認証方式を1からスクラッチで作る必要はありません。Spring securityの用意しているinterfaceを実装することでそういった独自の認証も簡単に実装ができ、認証部分以降はSpring securityの仕組みが使用できます。例えばSessionからユーザを特定してというような仕組みを独自に作るのは結構辛いのでSpiring securityを使いたいですよね。ちなみにですが、Spring securityのお作法に従わず独自に認証してHttpSessionに状態を書き込んだり、Spring securityのContextを直接操作するようなことはしないほうがいいです。以下の様なコードが出てくる場合は何かがおかしいです。(自分の一番最初の独自認証はこんな感じで対応してしまったので・・・)

SecurityContextHolder.setContext(securityContext)

Spring公式ドキュメントの
Pre-Authentication Scenariosを参照しています。詳細が知りたい場合はこのドキュメントを読んでください。

このページではざっくり公式ドキュメントを読んで概要を掴み、後半で簡単な実装をやってみます。

Spring securityのお作法

Spring Securityが用意しているinterfaceについて。

AbstractPreAuthenticatedProcessingFilter

HTTP Request毎にgetUserPrincipal()メソッドを呼び出すことによってユーザーを識別します。
認証成功・失敗時のHandlerはこのFilterで追加できます。

  • setAuthenticationFailureHandler
  • setAuthenticationSuccessHandler
    HTTP Requestからユーザー情報を抽出するか、またはHTTP Requestに含まれるTokenなどを利用し認証サーバに問い合わせし、ユーザ情報(Principal / Credentials)を返すAbstractPreAuthenticatedProcessingFilterのメソッドを実装します。

このクラスは、SecurityContextの現在の内容をチェックし、空の場合は、HTTPリクエストからユーザー情報を抽出し、AuthenticationManagerに渡します。あくまでもこのFilterはPrincipalとCredentialsを抽出する実装で認証は後続で行う。

次のメソッドをオーバーライドします。

protected abstract Object getPreAuthenticatedPrincipal(HttpServletRequest request);
protected abstract Object getPreAuthenticatedCredentials(HttpServletRequest request);

これらを呼びだした後、フィルタは返されたデータを含むPreAuthenticatedAuthenticationTokenを作成し、認証のためにSubmitします。
他のSpring Security認証フィルタと同様に、pre-authenticationフィルタは、セッション識別子と認証オブジェクトの詳細プロパティでIPアドレスを発信元などの追加情報を格納するためのオブジェクトWebAuthenticationDetailsを作成するauthenticationDetailsSourceを持っています。

J2eeBasedPreAuthenticatedWebAuthenticationDetailsSource

フィルタがこのクラスのインスタンスであるauthenticationDetailsSourceで構成されている場合は、権限情報は、「マッピング可能なROLE」が予めセットされており、それぞれについてのisUserInRole(String型のROLE)メソッドを呼び出すことによって得られます。クラスが設定されMappableAttributesRetrieverからこれらを取得します。実装では、アプリケーションのコンテキストでリストをハードコーディングし、web.xmlファイル内のロールのから情報を読み取ることができます。

Spring securityが提供しているauthenticationUserDetailsあたりを継承しておけば問題なさそうです。独自に実装する場合はRoleも自分でやってねってことでしょうか。

Roleを管理するためにGrantedAuthorityを実装したクラスを作りROLEの制御をします。

PreAuthenticatedAuthenticationProvider

PreAuthenticatedAuthenticationProviderは認証をAuthenticationUserDetailsServiceに委譲することによって認証を行います。AuthenticationUserDetailsServiceは標準のUserDetailsServiceに似ていますが、ユーザー名だけではなく認証オブジェクトも取ります。

public interface AuthenticationUserDetailsService {
UserDetails loadUserDetails(Authentication token) throws UsernameNotFoundException;
}

認証失敗の場合はUsernameNotFoundExceptionをthrowすればよさそうですね。

このインタフェースは、他の用途を有していてもよいが、事前認証では前節で説明したように、認証オブジェクトにパッケージ化されたauthoritiesへのアクセスを可能にします。 PreAuthenticatedGrantedAuthoritiesUserDetailsServiceクラスは、認証オブジェクトにパッケージ化されたauthoritiesへのアクセスを可能にします。あるいは、UserDetailsByNameServiceWrapperの実装を介して、標準UserDetailsServiceに委任することができます。

Http403ForbiddenEntryPoint

AuthenticationEntryPointは、技術的な概要の章で説明しました。(認証失敗のExeptionがthrowされたら403をレスポンスするぜみたいな話。)通常は、保護されたリソースにアクセスしようとすると認証されていないユーザーのための認証処理をする責任がありますが、pre-authenticationの場合には、これは適用されません。あなたが他の認証機構との組み合わせでpre-authenticationを使用していない場合にのみ、このクラスのインスタンスでExceptionTranslationFilterを構成します。ユーザーはnull認証の結果AbstractPreAuthenticatedProcessingFilterによって拒否された場合には呼び出されます。呼び出された場合、それは、常に403forbiddenのレスポンスコードを返します。

以下の様な実装。認証に失敗し403を返す時にリダイレクトしたりできる。

public class HogeForbiddenEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
throws IOException, ServletException {
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.sendRedirect("/auth-error.html");
}
}

EntryEndpointを指定しないとform認証のときのように認証が失敗したときのリダイレクトが動作しないので注意が必要。

403だけでなく他のEndpointはInterface AuthenticationEntryPointの実装として幾つか用意されている。

Interface AuthenticationEntryPoint

LoginUrlAuthenticationEntryPointをendpointとして指定して、リダイレクト先のURIの設定はbuildRedirectUrlToLoginPageメソッドをOverrideすれば良い。

Endpointの指定はjavaConfigで以下のように指定する。

@Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling().authenticationEntryPoint(customEndpoint);
}

Endpointの指定にexceptionHandling()が必要なのはExceptionをHandlingしてリダイレクト先とかを決めているからと理解すれば自然な設計か。

Request-Header Authentication

外部認証システムは、HTTP要求に特定のヘッダを設定することにより、アプリケーションに情報を供給することができます。このよく知られた例はSM_USERというヘッダーにユーザ名を渡し方法です。これは単にヘッダからユーザー名を抽出し認証に利用するというメカニズムです。クラスRequestHeaderAuthenticationFilterによってサポートされています。デフォルトではヘッダ名のSM_USERを使用します。詳細については、Javadocを参照してください。

J2EE Container Authentication

クラスJ2eePreAuthenticatedProcessingFilterは、HttpServletRequestのUserPrincipalプロパティからユーザ名を抽出します。このユーザ名を利用して認証を行います。

独自認証の実装方式

例えば、認証サーバからリクエストヘッダーにTOKENがセットされてアプリ側にリダイレクトされて、そのTOKENを認証サーバに問い合わせることでユーザ認証するといった場合をやってみる。

MyPreAuthenticatedProcessingFilter

AbstractPreAuthenticatedProcessingFilterをextendしたクラスで認証情報を取得する(Principal/Credentials)を独自認証用に実装する。

@Slf4j
public class MyPreAuthenticatedProcessingFilter extends AbstractPreAuthenticatedProcessingFilter {
@Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest httpServletRequest) {
//今回は認証時にPrincipalは使わない。
return "";
}
@Override
protected Object getPreAuthenticatedCredentials(HttpServletRequest httpServletRequest) {
log.debug("X-HOGE-AUTH: " + httpServletRequest.getHeader("X-HOGE-AUTH"));
String credentials = httpServletRequest.getHeader("X-HOGE-AUTH");
return credentials == null ? "" : credentials;
}
}

MyAuthenticationUserDetailsService

AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken>をimplementしたクラスで認証を行う。

前のMyPreAuthenticatedProcessingFilterで設定したPrincipal/CredentialsがPreAuthenticatedAuthenticationTokenに詰められてMyAuthenticationUserDetailsServiceに渡されます。tokenからPrincipal/Credentialsを取得して認証を行います。ここではCredentialsからTOKENを取り出して認証サーバに問い合わせを行う処理を書きます。(実装は書きませんが。)

@Slf4j
public class MyAuthenticationUserDetailsService implements AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> {
@Override
public UserDetails loadUserDetails(PreAuthenticatedAuthenticationToken token) throws UsernameNotFoundException {
Object credentials = token.getCredentials();
if(credentials.toString() == "") throw new UsernameNotFoundException("ユーザが見つかりません。");

//credentialsを使って認証サーバに問い合わせを行う。レスポンスを見てNGの場合はExceptionをthrowすればOK。
//UserNameなども同時に取得して以下の処理で設定する。

Collection<GrantedAuthority> authorities =new HashSet<GrantedAuthority>() ;
authorities.add(new UserAuthority());
authorities.add(new AdminAuthority());
//org.springframework.security.core.userdetails.Userを利用。
//UserNameは都度適切に設定し,Passwordなどはないので常にブランクにした。
return new User("ishii01","",authorities);
}
}

UserAuthority (AdminAuthority)

ただのRoleクラス。

public class UserAuthority implements GrantedAuthority {
@Override
public String getAuthority() {
return "ROLE_USER";
}
}

SecurityConfig

“/“以外は認証が必要。あとは定義したClassを使用するように設定する。”/logout”でログアウトし、preAuthenticatedProcessingFilterをフィルターとして利用するように設定する。preAuthenticatedProcessingFilterで認証が行われると以後は認証は行われずLogin状態となる。

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> authenticationUserDetailsService() {
return new MyAuthenticationUserDetailsService();
}
@Bean
public PreAuthenticatedAuthenticationProvider preAuthenticatedAuthenticationProvider() {
PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider();
provider.setPreAuthenticatedUserDetailsService(authenticationUserDetailsService());
provider.setUserDetailsChecker(new AccountStatusUserDetailsChecker());
return provider;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(preAuthenticatedAuthenticationProvider());
}
@Bean
public AbstractPreAuthenticatedProcessingFilter preAuthenticatedProcessingFilter() throws Exception {
MyPreAuthenticatedProcessingFilter filter = new MyPreAuthenticatedProcessingFilter();
filter.setAuthenticationManager(authenticationManager());
return filter;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/").permitAll()
.anyRequest().authenticated()
.and()
.logout().logoutUrl("/logout")
.and()
.addFilter(preAuthenticatedProcessingFilter());
.exceptionHandling()
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("loginFormUrl"));
}
}

まとめ

Spring securityで独自の認証に対応出きることが分かった。

おわり。

参考

  1. http://docs.spring.io/spring-security/site/docs/4.2.x-SNAPSHOT/reference/html/preauth.html
  2. http://stackoverflow.com/questions/9902783/preauthentication-with-spring-security-based-on-url-parameters
  3. http://qiita.com/masato_ka/items/519144098ba5370f1a26
  4. http://qiita.com/masatsugumatsus/items/7111983f04c08a5df1f8
  5. http://qiita.com/kazuki43zoo/items/e925f134e65d7595aa3c