1. 概要

この短い記事では、Spring Bootアクチュエータモジュールと、SpringSecurityと組み合わせた認証および承認イベントの公開のサポートについて説明します。

2. Mavenの依存関係

まず、 spring-boot-starter-actuatorpom.xml:に追加する必要があります。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    <version>2.2.2.RELEASE</version>
</dependency>

最新バージョンは、 MavenCentralリポジトリーで入手できます。

3. 認証および承認イベントのリッスン

Spring Bootアプリケーションでのすべての認証と承認の試行をログに記録するには、リスナーメソッドを使用してBeanを定義するだけです。

@Component
public class LoginAttemptsLogger {

    @EventListener
    public void auditEventHappened(
      AuditApplicationEvent auditApplicationEvent) {
        
        AuditEvent auditEvent = auditApplicationEvent.getAuditEvent();
        System.out.println("Principal " + auditEvent.getPrincipal() 
          + " - " + auditEvent.getType());

        WebAuthenticationDetails details = 
          (WebAuthenticationDetails) auditEvent.getData().get("details");
        System.out.println("Remote IP address: " 
          + details.getRemoteAddress());
        System.out.println("  Session Id: " + details.getSessionId());
    }
}

AuditApplicationEvent で利用可能な情報の一部を出力して、利用可能な情報を示していることに注意してください。 実際のアプリケーションでは、その情報をリポジトリまたはキャッシュに保存してさらに処理することができます。

すべてのSpringBeanが機能することに注意してください。 新しいSpringイベントサポートの基本は非常に単純です。

  • @EventListenerでメソッドにアノテーションを付けます
  • メソッドの唯一の引数としてAuditApplicationEventを追加します

アプリケーションを実行した場合の出力は、次のようになります。

Principal anonymousUser - AUTHORIZATION_FAILURE
  Remote IP address: 0:0:0:0:0:0:0:1
  Session Id: null
Principal user - AUTHENTICATION_FAILURE
  Remote IP address: 0:0:0:0:0:0:0:1
  Session Id: BD41692232875A5A65C5E35E63D784F6
Principal user - AUTHENTICATION_SUCCESS
  Remote IP address: 0:0:0:0:0:0:0:1
  Session Id: BD41692232875A5A65C5E35E63D784F6

この例では、3つのAuditApplicationEventがリスナーによって受信されています。

  1. ログオンせずに、制限されたページへのアクセスが要求されました
  2. ログオン中に間違ったパスワードが使用されました
  3. 正しいパスワードが2回目に使用されました

4. 認証監査リスナー

Spring BootのAuthorizationAuditListenerによって公開される情報が十分でない場合は、独自のBeanを作成して、より多くの情報を公開できます。

例を見てみましょう。ここでは、承認が失敗したときにアクセスされたリクエストURLも公開しています。

@Component
public class ExposeAttemptedPathAuthorizationAuditListener 
  extends AbstractAuthorizationAuditListener {

    public static final String AUTHORIZATION_FAILURE 
      = "AUTHORIZATION_FAILURE";

    @Override
    public void onApplicationEvent(AbstractAuthorizationEvent event) {
        if (event instanceof AuthorizationFailureEvent) {
            onAuthorizationFailureEvent((AuthorizationFailureEvent) event);
        }
    }

    private void onAuthorizationFailureEvent(
      AuthorizationFailureEvent event) {
        Map<String, Object> data = new HashMap<>();
        data.put(
          "type", event.getAccessDeniedException().getClass().getName());
        data.put("message", event.getAccessDeniedException().getMessage());
        data.put(
          "requestUrl", ((FilterInvocation)event.getSource()).getRequestUrl() );
        
        if (event.getAuthentication().getDetails() != null) {
            data.put("details", 
              event.getAuthentication().getDetails());
        }
        publish(new AuditEvent(event.getAuthentication().getName(), 
          AUTHORIZATION_FAILURE, data));
    }
}

これで、リスナーにリクエストURLを記録できます。

@Component
public class LoginAttemptsLogger {

    @EventListener
    public void auditEventHappened(
      AuditApplicationEvent auditApplicationEvent) {
        AuditEvent auditEvent = auditApplicationEvent.getAuditEvent();
 
        System.out.println("Principal " + auditEvent.getPrincipal() 
          + " - " + auditEvent.getType());

        WebAuthenticationDetails details
          = (WebAuthenticationDetails) auditEvent.getData().get("details");
 
        System.out.println("  Remote IP address: " 
          + details.getRemoteAddress());
        System.out.println("  Session Id: " + details.getSessionId());
        System.out.println("  Request URL: " 
          + auditEvent.getData().get("requestUrl"));
    }
}

その結果、出力には要求されたURLが含まれるようになります。

Principal anonymousUser - AUTHORIZATION_FAILURE
  Remote IP address: 0:0:0:0:0:0:0:1
  Session Id: null
  Request URL: /hello

この例では、抽象 AbstractAuthorizationAuditListener から拡張したため、実装ではその基本クラスのpublishメソッドを使用できることに注意してください。

テストする場合は、ソースコードを確認して、次のコマンドを実行してください。

mvn clean spring-boot:run

その後、ブラウザで http:// localhost:8080 /.

5. 監査イベントの保存

デフォルトでは、SpringBootは監査イベントをAuditEventRepositoryに保存します。 独自の実装でBeanを作成しない場合は、InMemoryAuditEventRepositoryが配線されます。

InMemoryAuditEventRepository は、最後の4000の監査イベントをメモリに格納する一種の循環バッファです。 これらのイベントには、管理エンドポイントを介してアクセスできます http:// localhost:8080 / auditevents.

これにより、監査イベントのJSON表現が返されます。

{
  "events": [
    {
      "timestamp": "2017-03-09T19:21:59+0000",
      "principal": "anonymousUser",
      "type": "AUTHORIZATION_FAILURE",
      "data": {
        "requestUrl": "/auditevents",
        "details": {
          "remoteAddress": "0:0:0:0:0:0:0:1",
          "sessionId": null
        },
        "type": "org.springframework.security.access.AccessDeniedException",
        "message": "Access is denied"
      }
    },
    {
      "timestamp": "2017-03-09T19:22:00+0000",
      "principal": "anonymousUser",
      "type": "AUTHORIZATION_FAILURE",
      "data": {
        "requestUrl": "/favicon.ico",
        "details": {
          "remoteAddress": "0:0:0:0:0:0:0:1",
          "sessionId": "18FA15865F80760521BBB736D3036901"
        },
        "type": "org.springframework.security.access.AccessDeniedException",
        "message": "Access is denied"
      }
    },
    {
      "timestamp": "2017-03-09T19:22:03+0000",
      "principal": "user",
      "type": "AUTHENTICATION_SUCCESS",
      "data": {
        "details": {
          "remoteAddress": "0:0:0:0:0:0:0:1",
          "sessionId": "18FA15865F80760521BBB736D3036901"
        }
      }
    }
  ]
}

6. 結論

Spring Bootでのアクチュエータのサポートにより、ユーザーからの認証と承認の試行をログに記録するのは簡単になります。 読者は、いくつかの追加情報について、本番環境監査も参照されます。

この記事のコードは、GitHubにあります。