並行性サポート

ほとんどの環境では、セキュリティは Thread ごとに保存されます。これは、新しい Thread で作業が行われると、SecurityContext が失われることを意味します。Spring Security は、これを管理しやすくするためのインフラストラクチャを提供します。Spring Security は、マルチスレッド環境で Spring Security を操作するための低レベルの抽象化を提供します。実際、これは Spring Security が AsyncContext.start(Runnable) および Spring MVC 非同期統合と統合するために構築されたものです。

DelegatingSecurityContextRunnable

Spring Security の同時実行サポート内の最も基本的な構成要素の 1 つは、DelegatingSecurityContextRunnable です。デリゲート Runnable をラップして、デリゲートに指定された SecurityContext で SecurityContextHolder を初期化します。次に、デリゲート Runnable を呼び出し、後で SecurityContextHolder をクリアするようにします。DelegatingSecurityContextRunnable は次のようになります。

public void run() {
try {
	SecurityContextHolder.setContext(securityContext);
	delegate.run();
} finally {
	SecurityContextHolder.clearContext();
}
}

非常にシンプルですが、SecurityContext をある Thread から別の Thread にシームレスに転送できます。ほとんどの場合、SecurityContextHolder は Thread ごとに動作するため、これは重要です。例: サービスの 1 つを保護するために Spring Security の <global-method-security> サポートを使用した可能性があります。これで、現在の Thread の SecurityContext を、保護されたサービスを呼び出す Thread に転送できます。次の例は、その方法を示しています。

Runnable originalRunnable = new Runnable() {
public void run() {
	// invoke secured service
}
};

SecurityContext context = SecurityContextHolder.getContext();
DelegatingSecurityContextRunnable wrappedRunnable =
	new DelegatingSecurityContextRunnable(originalRunnable, context);

new Thread(wrappedRunnable).start();

上記のコード:

  • 保護されたサービスを呼び出す Runnable を作成します。Spring Security を認識していないことに注意してください。

  • 使用したい SecurityContext を SecurityContextHolder から取得し、DelegatingSecurityContextRunnable を初期化します。

  • DelegatingSecurityContextRunnable を使用して Thread を作成します。

  • 作成した Thread を起動します。

SecurityContextHolder から SecurityContext を使用して DelegatingSecurityContextRunnable を作成するのが一般的であるため、そのためのショートカットコンストラクターがあります。次のコードは、前のコードと同じ効果があります。

Runnable originalRunnable = new Runnable() {
public void run() {
	// invoke secured service
}
};

DelegatingSecurityContextRunnable wrappedRunnable =
	new DelegatingSecurityContextRunnable(originalRunnable);

new Thread(wrappedRunnable).start();

このコードは簡単に使えますが、Spring Security を使っているという知識が必要です。次のセクションでは、Spring Security を使っているという事実を隠すために DelegatingSecurityContextExecutor をどのように利用できるかを見ていきます。

DelegatingSecurityContextExecutor

前のセクションで、DelegatingSecurityContextRunnable を使用するのは簡単であることがわかりましたが、使用するには Spring Security を意識する必要があるため、理想的ではありませんでした。次に、DelegatingSecurityContextExecutor が Spring Security を使用しているという知識からコードを保護する方法を見ていきます。

DelegatingSecurityContextExecutor の設計は、デリゲート Runnable の代わりにデリゲート Executor を受け入れることを除いて、DelegatingSecurityContextRunnable の設計と似ています。次の例は、その使用方法を示しています。

SecurityContext context = SecurityContextHolder.createEmptyContext();
Authentication authentication =
	UsernamePasswordAuthenticationToken.authenticated("user","doesnotmatter", AuthorityUtils.createAuthorityList("ROLE_USER"));
context.setAuthentication(authentication);

SimpleAsyncTaskExecutor delegateExecutor =
	new SimpleAsyncTaskExecutor();
DelegatingSecurityContextExecutor executor =
	new DelegatingSecurityContextExecutor(delegateExecutor, context);

Runnable originalRunnable = new Runnable() {
public void run() {
	// invoke secured service
}
};

executor.execute(originalRunnable);

このコード:

この例では、SecurityContext を手動で作成していることに注意してください。ただし、SecurityContext をどこでどのように取得するかは重要ではありません(たとえば、SecurityContextHolder から取得できます)。* 送信された Runnable オブジェクトの実行を担当する delegateExecutor を作成します。* 最後に、execute メソッドに渡された Runnable を DelegatingSecurityContextRunnable でラップすることを担当する DelegatingSecurityContextExecutor を作成します。次に、ラップされた Runnable を delegateExecutor に渡します。この場合、DelegatingSecurityContextExecutor に送信されるすべての Runnable に同じ SecurityContext が使用されます。これは、昇格された特権を持つユーザーが実行する必要のあるバックグラウンドタスクを実行する場合に便利です。* この時点で、「これにより、Spring Security に関する知識のコードがどのように保護されるのか」と自問するかもしれません。独自のコードで SecurityContext と DelegatingSecurityContextExecutor を作成する代わりに、すでに初期化されている DelegatingSecurityContextExecutor のインスタンスを挿入できます。

次の例を考えてみましょう。

@Autowired
private Executor executor; // becomes an instance of our DelegatingSecurityContextExecutor

public void submitRunnable() {
Runnable originalRunnable = new Runnable() {
	public void run() {
	// invoke secured service
	}
};
executor.execute(originalRunnable);
}

これで、コードは SecurityContext が Thread に伝播されていることを認識せず、originalRunnable が実行され、SecurityContextHolder がクリアされます。この例では、同じユーザーが各スレッドの実行に使用されています。executor.execute(Runnable) を呼び出して originalRunnable を処理したときに、SecurityContextHolder のユーザー(つまり、現在ログインしているユーザー)を使用したい場合はどうなるでしょうか? これを行うには、DelegatingSecurityContextExecutor コンストラクターから SecurityContext 引数を削除します。

SimpleAsyncTaskExecutor delegateExecutor = new SimpleAsyncTaskExecutor();
DelegatingSecurityContextExecutor executor =
	new DelegatingSecurityContextExecutor(delegateExecutor);

これで、executor.execute(Runnable) が実行されるたびに、SecurityContext が最初に SecurityContextHolder によって取得され、次にその SecurityContext が DelegatingSecurityContextRunnable の作成に使用されます。これは、executor.execute(Runnable) コードの呼び出しに使用されたのと同じユーザーで Runnable を実行していることを意味します。