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
動作確認
- ADクライアントにリモートデスクトップ接続
- ブラウザを起動し、サンプルアプリのURL入力し、SSOできていることを確認