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