1. 概要

このチュートリアルでは、Apereo Central Authentication Service(CAS)を見て、Spring Bootサービスがそれを認証に使用する方法を確認します。 CAS は、オープンソースでもあるエンタープライズシングルサインオン(SSO)ソリューションです。

SSOとは何ですか? 同じクレデンシャルでYouTube、Gmail、マップにログインすると、シングルサインオンになります。 CASサーバーとSpring Bootアプリをセットアップして、これをデモンストレーションします。 SpringBootアプリは認証にCASを使用します。

2. CASサーバーのセットアップ

2.1. CASのインストールと依存関係

サーバーは、Maven(Gradle)War Overlayスタイルを使用して、セットアップとデプロイメントを容易にします。

git clone https://github.com/apereo/cas-overlay-template.git cas-server

このコマンドは、cas-overlay-templatecas-serverディレクトリに複製します。

ここで取り上げる側面には、JSONサービスの登録とJDBCデータベース接続が含まれます。 そこで、モジュールをbuild.gradleファイルのdependenciesセクションに追加します。

compile "org.apereo.cas:cas-server-support-json-service-registry:${casServerVersion}"
compile "org.apereo.cas:cas-server-support-jdbc:${casServerVersion}"

casServerの最新バージョンを確認してみましょう。

2.2. CASサーバーの構成

CASサーバーを起動する前に、いくつかの基本構成を追加する必要があります。 cas-server / src / main /resourcesフォルダーとこのフォルダーを作成することから始めましょう。 これに続いて、フォルダ内にapplication.propertiesも作成されます。

server.port=8443
spring.main.allow-bean-definition-overriding=true
server.ssl.key-store=classpath:/etc/cas/thekeystore
server.ssl.key-store-password=changeit

上記の構成で参照されているキーストアファイルの作成に進みましょう。 まず、 cas-server / src / main /resources/etc /cas/etc / cas /configのフォルダーを作成する必要があります。

次に、ディレクトリを cas-server / src / main / resources / etc / cas に変更し、コマンドを実行してキーストアを生成する必要があります。

keytool -genkey -keyalg RSA -alias thekeystore -keystore thekeystore -storepass changeit -validity 360 -keysize 2048

SSLハンドシェイクエラーが発生しないようにするには、名前と名前の値としてlocalhostを使用する必要があります。 組織名と単位にも同じものを使用する必要があります。 さらに、thekeystoreをクライアントアプリの実行に使用するJDK/JREにインポートする必要があります。

keytool -importkeystore -srckeystore thekeystore -destkeystore $JAVA11_HOME/jre/lib/security/cacerts

ソースキーストアとデスティネーションキーストアのパスワードはchangeitです。 Unixシステムでは、admin( sudo )権限でこのコマンドを実行する必要がある場合があります。 インポート後、実行中のJavaのすべてのインスタンスを再起動するか、システムを再起動する必要があります。

CASバージョン6.1.xで必要なため、JDK11を使用しています。 また、ホームディレクトリを指す環境変数$JAVA11_HOMEを定義しました。 これで、CASサーバーを起動できます。

./gradlew run -Dorg.gradle.java.home=$JAVA11_HOME

アプリケーションが起動すると、端末に「READY」と印刷され、サーバーは https:// localhost:8443で利用できるようになります。

2.3. CASサーバーのユーザー設定

ユーザーを設定していないため、まだログインできません。 CASには、スタンドアロンモードを含め、構成の管理のさまざまな方法があります。 プロパティファイルcas.propertiesを作成する構成フォルダーcas-server/ src / main / resources / etc / cas /configを作成しましょう。 これで、プロパティファイルで静的ユーザーを定義できます。

cas.authn.accept.users=casuser::Mellon

設定を有効にするには、configフォルダの場所をCASサーバーに通知する必要があります。 tasks.gradle を更新して、コマンドラインからJVM引数として場所を渡すことができるようにします。

task run(group: "build", description: "Run the CAS web application in embedded container mode") {
    dependsOn 'build'
    doLast {
        def casRunArgs = new ArrayList<>(Arrays.asList(
          "-server -noverify -Xmx2048M -XX:+TieredCompilation -XX:TieredStopAtLevel=1".split(" ")))
        if (project.hasProperty('args')) {
            casRunArgs.addAll(project.args.split('\\s+'))
        }
        javaexec {
            main = "-jar"
            jvmArgs = casRunArgs
            args = ["build/libs/${casWebApplicationBinaryName}"]
            logger.info "Started ${commandLine}"
        }
    }
}

次に、ファイルを保存して実行します。

./gradlew run
  -Dorg.gradle.java.home=$JAVA11_HOME
  -Pargs="-Dcas.standalone.configurationDirectory=/cas-server/src/main/resources/etc/cas/config"

cas.standalone.configurationDirectoryの値は絶対パスであることに注意してください。 これで、 https:// localhost:8443 にアクセスし、ユーザー名casuserとパスワードMellonでログインできます。

3. CASクライアントのセットアップ

Spring Initializr を使用して、Spring Bootクライアントアプリを生成します。 Web Security Freemarker DevToolsの依存関係があります。 さらに、SpringセキュリティCASモジュールの依存関係をpom.xmlに追加します。

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-cas</artifactId>
    <versionId>5.3.0.RELEASE</versionId>
</dependency>

最後に、次のSpring Bootプロパティを追加して、アプリを構成しましょう。

server.port=8900
spring.freemarker.suffix=.ftl

4. CASサーバーサービスの登録

クライアントアプリケーションは、認証の前にCASサーバーに登録する必要があります。 CASサーバーは、YAML、JSON、MongoDB、およびLDAPクライアントレジストリの使用をサポートしています。

このチュートリアルでは、JSONサービスレジストリメソッドを使用します。 さらに別のフォルダcas-server/ src / main / resources / etc / cas /servicesを作成しましょう。 サービスレジストリのJSONファイルを格納するのはこのフォルダーです。

クライアントアプリケーションの定義を含むJSONファイルを作成します。 ファイルの名前casSecuredApp-8900.json、は、パターンs erviceName-Id.jsonに従います。

{
  "@class" : "org.apereo.cas.services.RegexRegisteredService",
  "serviceId" : "http://localhost:8900/login/cas",
  "name" : "casSecuredApp",
  "id" : 8900,
  "logoutType" : "BACK_CHANNEL",
  "logoutUrl" : "http://localhost:8900/exit/cas"
}

serviceId 属性は、クライアントアプリケーションの正規表現URLパターンを定義します。 パターンは、クライアントアプリケーションのURLと一致する必要があります。

id属性は一意である必要があります。 つまり、同じCASサーバーに同じidが登録されているサービスが2つ以上存在してはなりません。 id が重複していると、競合が発生し、構成が上書きされます。

また、ログアウトタイプを BACK_CHANNEL に設定し、URLを http:// localhost:8900 / exit / cas に設定して、後で1回のログアウトを実行できるようにします。
CASサーバーがJSON構成ファイルを使用する前に、cas.propertiesでJSONレジストリを有効にする必要があります。
cas.serviceRegistry.initFromJson=true
cas.serviceRegistry.json.location=classpath:/etc/cas/services

5. CASクライアントのシングルサインオン構成

次のステップは、CASサーバーと連携するようにSpringSecurityを構成することです。 また、CASシーケンスと呼ばれるインタラクションのフルフローも確認する必要があります。

次のbean構成をSpring BootアプリのCasSecuredApplicationクラスに追加しましょう。

@Bean
public CasAuthenticationFilter casAuthenticationFilter(
  AuthenticationManager authenticationManager,
  ServiceProperties serviceProperties) throws Exception {
    CasAuthenticationFilter filter = new CasAuthenticationFilter();
    filter.setAuthenticationManager(authenticationManager);
    filter.setServiceProperties(serviceProperties);
    return filter;
}

@Bean
public ServiceProperties serviceProperties() {
    logger.info("service properties");
    ServiceProperties serviceProperties = new ServiceProperties();
    serviceProperties.setService("http://cas-client:8900/login/cas");
    serviceProperties.setSendRenew(false);
    return serviceProperties;
}

@Bean
public TicketValidator ticketValidator() {
    return new Cas30ServiceTicketValidator("https://localhost:8443");
}

@Bean
public CasAuthenticationProvider casAuthenticationProvider(
  TicketValidator ticketValidator,
  ServiceProperties serviceProperties) {
    CasAuthenticationProvider provider = new CasAuthenticationProvider();
    provider.setServiceProperties(serviceProperties);
    provider.setTicketValidator(ticketValidator);
    provider.setUserDetailsService(
      s -> new User("test@test.com", "Mellon", true, true, true, true,
      AuthorityUtils.createAuthorityList("ROLE_ADMIN")));
    provider.setKey("CAS_PROVIDER_LOCALHOST_8900");
    return provider;
}

ServiceProperties BeanのURLは、casSecuredApp-8900.jsonserviceIdと同じです。 これは、CASサーバーに対してこのクライアントを識別するため重要です。

ServicePropertiessendRenewプロパティがfalseに設定されています。 これは、ユーザーがサーバーにログイン資格情報を提示する必要があるのは1回だけであることを意味します。

AuthenticationEntryPoint Beanは、認証例外を処理します。 したがって、認証のためにユーザーをCASサーバーのログインURLにリダイレクトします。

要約すると、認証フローは次のようになります。

  1. ユーザーが安全なページにアクセスしようとすると、認証例外がトリガーされます
  2. 例外はAuthenticationEntryPointをトリガーします。 それに応じて、 AuthenticationEntryPoint は、ユーザーをCASサーバーのログインページ– https:// localhost:8443 / loginに移動します。
  3. 認証が成功すると、サーバーはチケットを使用してクライアントにリダイレクトします
  4. CasAuthenticationFilter はリダイレクトを取得し、CasAuthenticationProviderを呼び出します
  5. CasAuthenticationProvider は、 TicketValidator を使用して、CASサーバーに提示されたチケットを確認します
  6. チケットが有効な場合、ユーザーは要求された安全なURLへのリダイレクトを取得します

最後に、WebSecurityConfigでいくつかのルートを保護するようにHttpSecurityを構成しましょう。 このプロセスでは、例外処理用の認証エントリポイントも追加します。

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests().antMatchers( "/secured", "/login") 
      .authenticated() 
      .and().exceptionHandling() 
      .authenticationEntryPoint(authenticationEntryPoint());
}

6. CASクライアントのシングルログアウト設定

これまで、シングルサインオンを扱ってきました。 ここで、CASシングルログアウト(SLO)について考えてみましょう。

CASを使用してユーザー認証を管理するアプリケーションは、次の2つの場所からユーザーをログアウトできます。

  • クライアントアプリケーションは、それ自体からローカルでユーザーをログアウトできます。これは、同じCASサーバーを使用する他のアプリケーションでのユーザーのログインステータスには影響しません。
  • クライアントアプリケーションは、CASサーバーからユーザーをログアウトすることもできます。これにより、ユーザーは、同じCASサーバーに接続されている他のすべてのクライアントアプリからログアウトされます。

最初にクライアントアプリケーションでログアウトを設定し、次にCASサーバーでシングルログアウトに拡張します。

舞台裏で何が起こっているかを明らかにするために、ローカルログアウトを処理する logout()メソッドを作成します。 成功すると、シングルログアウトのリンクがあるページにリダイレクトされます。

@GetMapping("/logout")
public String logout(
  HttpServletRequest request, 
  HttpServletResponse response, 
  SecurityContextLogoutHandler logoutHandler) {
    Authentication auth = SecurityContextHolder
      .getContext().getAuthentication();
    logoutHandler.logout(request, response, auth );
    new CookieClearingLogoutHandler(
      AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY)
      .logout(request, response, auth);
    return "auth/logout";
}

シングルログアウトプロセスでは、CASサーバーは最初にユーザーのチケットを期限切れにし、次に登録されているすべてのクライアントアプリに非同期要求を送信します。 このシグナルを受信した各クライアントアプリは、ローカルログアウトを実行します。 これにより、一度ログアウトするという目標を達成すると、どこでもログアウトします。

そうは言っても、クライアントアプリにいくつかのbean構成を追加しましょう。 具体的には、 CasSecuredApplicaitonでは次のようになります。

@Bean
public SecurityContextLogoutHandler securityContextLogoutHandler() {
    return new SecurityContextLogoutHandler();
}

@Bean
public LogoutFilter logoutFilter() {
    LogoutFilter logoutFilter = new LogoutFilter("https://localhost:8443/logout",
      securityContextLogoutHandler());
    logoutFilter.setFilterProcessesUrl("/logout/cas");
    return logoutFilter;
}

@Bean
public SingleSignOutFilter singleSignOutFilter() {
    SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter();
    singleSignOutFilter.setCasServerUrlPrefix("https://localhost:8443");
    singleSignOutFilter.setLogoutCallbackPath("/exit/cas");
    singleSignOutFilter.setIgnoreInitConfiguration(true);
    return singleSignOutFilter;
}

logoutFilter は、 / logout / cas への要求をインターセプトし、アプリケーションをCASサーバーにリダイレクトします。 SingleSignOutFilter は、CASサーバーからの要求をインターセプトし、ローカルログアウトを実行します。

7. CASサーバーをデータベースに接続する

MySQLデータベースから資格情報を読み取るようにCASサーバーを構成できます。 ローカルマシンで実行されているMySQLサーバーのtestデータベースを使用します。 cas-server / src / main / resources / etc / cas / config /cas.propertiesを更新しましょう。

cas.authn.accept.users=

cas.authn.jdbc.query[0].sql=SELECT * FROM users WHERE email = ?
cas.authn.jdbc.query[0].url=
  jdbc:mysql://127.0.0.1:3306/test?
  useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
cas.authn.jdbc.query[0].dialect=org.hibernate.dialect.MySQLDialect
cas.authn.jdbc.query[0].user=root
cas.authn.jdbc.query[0].password=root
cas.authn.jdbc.query[0].ddlAuto=none
cas.authn.jdbc.query[0].driverClass=com.mysql.cj.jdbc.Driver
cas.authn.jdbc.query[0].fieldPassword=password
cas.authn.jdbc.query[0].passwordEncoder.type=NONE

cas.authn.accept.usersを空白に設定します。 これにより、CASサーバーによる静的ユーザーリポジトリの使用が無効になります。

上記のSQLによると、ユーザーの資格情報はusersテーブルに格納されます。 email 列は、ユーザーのプリンシパル( username )を表します。

サポートされているデータベース、使用可能なドライバー、方言のリストを必ず確認してください。 また、パスワードエンコーダタイプをNONEに設定します。 他の暗号化メカニズムとそれらの固有のプロパティも利用できます。

CASサーバーのデータベースのプリンシパルは、クライアントアプリケーションのプリンシパルと同じである必要があることに注意してください。

CasAuthenticationProvider を更新して、CASサーバーと同じユーザー名を使用してみましょう。

@Bean
public CasAuthenticationProvider casAuthenticationProvider() {
    CasAuthenticationProvider provider = new CasAuthenticationProvider();
    provider.setServiceProperties(serviceProperties());
    provider.setTicketValidator(ticketValidator());
    provider.setUserDetailsService(
      s -> new User("test@test.com", "Mellon", true, true, true, true,
      AuthorityUtils.createAuthorityList("ROLE_ADMIN")));
    provider.setKey("CAS_PROVIDER_LOCALHOST_8900");
    return provider;
}

CasAuthenticationProviderは認証にパスワードを使用しません。 それでも、認証を成功させるには、そのユーザー名がCASサーバーのユーザー名と一致している必要があります。 CASサーバーでは、MySQLサーバーがlocalhostのポート3306で実行されている必要があります。 ユーザー名とパスワードはrootである必要があります。

CASサーバーとSpringBootアプリをもう一度再起動します。 次に、認証に新しい資格情報を使用します。

8. 結論

SpringSecurityでCASSSOを使用する方法と、関連する多くの構成ファイルについて見てきました。 CAS SSOには、構成可能な他の多くの側面があります。 テーマやプロトコルタイプから認証ポリシーに至るまで。

これらと他のものはdocsにあります。 CASサーバーSpringBootアプリのソースコードは、GitHubで入手できます。