1. 概要

簡単に言えば、SpringSecurityはメソッドレベルで承認セマンティクスをサポートします。

通常、サービスレイヤーを保護するには、たとえば、特定のメソッドを実行できる役割を制限し、専用のメソッドレベルのセキュリティテストサポートを使用してテストします。

このチュートリアルでは、いくつかのセキュリティアノテーションの使用法を確認します。 次に、さまざまな戦略を使用してメソッドのセキュリティをテストすることに焦点を当てます。

2. メソッドセキュリティの有効化

まず、Spring Method Securityを使用するには、spring-security-config依存関係を追加する必要があります。

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
</dependency>

最新バージョンはMavenCentralにあります。

Spring Bootを使用する場合は、spring-boot-starter-security依存関係を使用できます。これにはspring-security-configが含まれます。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

繰り返しになりますが、最新バージョンは MavenCentralにあります。

次に、グローバルメソッドセキュリティを有効にする必要があります。

@Configuration
@EnableGlobalMethodSecurity(
  prePostEnabled = true, 
  securedEnabled = true, 
  jsr250Enabled = true)
public class MethodSecurityConfig 
  extends GlobalMethodSecurityConfiguration {
}
  • prePostEnabled プロパティは、SpringSecurityの事前/事後注釈を有効にします。
  • securedEnabled プロパティは、@Securedアノテーションを有効にするかどうかを決定します。
  • jsr250Enabled プロパティを使用すると、@RoleAllowedアノテーションを使用できます。

これらのアノテーションについては、次のセクションで詳しく説明します。

3. メソッドセキュリティの適用

3.1. @Securedアノテーションの使用

@Securedアノテーションは、メソッドのロールのリストを指定するために使用されます。したがって、ユーザーは、指定されたロールの少なくとも1つを持っている場合にのみ、そのメソッドにアクセスできます。

getUsernameメソッドを定義しましょう。

@Secured("ROLE_VIEWER")
public String getUsername() {
    SecurityContext securityContext = SecurityContextHolder.getContext();
    return securityContext.getAuthentication().getName();
}

ここで、 @Secured(“ ROLE_VIEWER”)アノテーションは、ROLE_VIEWERの役割を持つユーザーのみがgetUsernameメソッドを実行できることを定義しています。

さらに、@Securedアノテーションで役割のリストを定義できます。

@Secured({ "ROLE_VIEWER", "ROLE_EDITOR" })
public boolean isValidUsername(String username) {
    return userRoleRepository.isValidUsername(username);
}

この場合、構成では、ユーザーがROLE_VIEWERまたはROLE_EDITORのいずれかを持っている場合、そのユーザーはisValidUsernameメソッドを呼び出すことができます。

@Secured アノテーションは、Spring式言語(SpEL)をサポートしていません。

3.2. @RolesAllowedアノテーションの使用

@RolesAllowed アノテーションは、JSR-250の@Securedアノテーションと同等のアノテーションです。

基本的に、@RolesAllowedアノテーションは@Securedと同様の方法で使用できます。

このようにして、getUsernameメソッドとisValidUsernameメソッドを再定義できます。

@RolesAllowed("ROLE_VIEWER")
public String getUsername2() {
    //...
}
    
@RolesAllowed({ "ROLE_VIEWER", "ROLE_EDITOR" })
public boolean isValidUsername2(String username) {
    //...
}

同様に、ROLE_VIEWERの役割を持つユーザーのみがgetUsername2を実行できます。

この場合も、ユーザーはROLE_VIEWERまたはROLER_EDITORロールの少なくとも1つを持っている場合にのみ、isValidUsername2を呼び出すことができます。

3.3. @PreAuthorizeおよび@PostAuthorizeアノテーションの使用

@PreAuthorizeと@PostAuthorizeの両方のアノテーションは、式ベースのアクセス制御を提供します。したがって、述語は SpEL(春の式言語)を使用して記述できます。

@PreAuthorizeアノテーションは、メソッドに入る前に指定された式をチェックしますが、 @PostAuthorizeアノテーションは、メソッドの実行後にそれを検証し、結果を変更する可能性があります。

次に、getUsernameInUpperCaseメソッドを次のように宣言しましょう。

@PreAuthorize("hasRole('ROLE_VIEWER')")
public String getUsernameInUpperCase() {
    return getUsername().toUpperCase();
}

@PreAuthorize(“ hasRole(’ROLE_VIEWER’)”)は、前のセクションで使用した @Secured(“ ROLE_VIEWER”)と同じ意味です。 以前の記事セキュリティ式の詳細を自由に見つけてください。

したがって、注釈 @Secured({“ ROLE_VIEWER”、” ROLE_EDITOR”})は、 @PreAuthorize(“ hasRole(’ROLE_VIEWER’)または hasRole( ‘ ROLE_EDITOR’)”)

@PreAuthorize("hasRole('ROLE_VIEWER') or hasRole('ROLE_EDITOR')")
public boolean isValidUsername3(String username) {
    //...
}

さらに、の一部としてメソッド引数を実際に使用できます。

@PreAuthorize("#username == authentication.principal.username")
public String getMyRoles(String username) {
    //...
}

ここで、ユーザーは、引数 username の値が現在のプリンシパルのユーザー名と同じである場合にのみ、getMyRolesメソッドを呼び出すことができます。

@PreAuthorize式を@PostAuthorize式に置き換えることができることに注意してください。

getMyRoles を書き直してみましょう:

@PostAuthorize("#username == authentication.principal.username")
public String getMyRoles2(String username) {
    //...
}

ただし、前の例では、ターゲットメソッドの実行後に承認が遅れます。

さらに、 @PostAuthorizeアノテーションは、メソッドresultにアクセスする機能を提供します。

@PostAuthorize
  ("returnObject.username == authentication.principal.nickName")
public CustomUser loadUserDetail(String username) {
    return userRoleRepository.loadUserByUserName(username);
}

ここで、 loadUserDetail メソッドは、返されたCustomUserusernameが現在の認証プリンシパルのニックネームと等しい場合にのみ正常に実行されます。

このセクションでは、主に単純なSpring式を使用します。 より複雑なシナリオでは、カスタムセキュリティ式を作成できます。

3.4. @PreFilterおよび@PostFilterアノテーションの使用

Spring Securityは、メソッドを実行する前にコレクション引数をフィルタリングするための@PreFilterアノテーションを提供します

@PreFilter("filterObject != authentication.principal.username")
public String joinUsernames(List<String> usernames) {
    return usernames.stream().collect(Collectors.joining(";"));
}

この例では、認証されたユーザー名を除くすべてのユーザー名を結合しています。

ここで、式ではという名前を使用して、コレクション内の現在のオブジェクトを表します。

ただし、メソッドにコレクションタイプである引数が複数ある場合は、 filterTarget プロパティを使用して、フィルタリングする引数を指定する必要があります。

@PreFilter
  (value = "filterObject != authentication.principal.username",
  filterTarget = "usernames")
public String joinUsernamesAndRoles(
  List<String> usernames, List<String> roles) {
 
    return usernames.stream().collect(Collectors.joining(";")) 
      + ":" + roles.stream().collect(Collectors.joining(";"));
}

さらに、 @PostFilterアノテーションを使用して、返されたメソッドのコレクションをフィルタリングすることもできます。

@PostFilter("filterObject != authentication.principal.username")
public List<String> getAllUsernamesExceptCurrent() {
    return userRoleRepository.getAllUsernames();
}

この場合、名前 filterObject は、返されたコレクション内の現在のオブジェクトを参照します。

この構成では、Spring Securityは返されたリストを反復処理し、プリンシパルのユーザー名に一致する値をすべて削除します。

Spring Security –@PreFilterと@PostFilterの記事では、両方のアノテーションについて詳しく説明しています。

3.5. メソッドセキュリティメタアノテーション

通常、同じセキュリティ構成を使用してさまざまなメソッドを保護する状況に陥ります。

この場合、セキュリティメタアノテーションを定義できます。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('VIEWER')")
public @interface IsViewer {
}

次に、@ IsViewerアノテーションを直接使用して、メソッドを保護できます。

@IsViewer
public String getUsername4() {
    //...
}

セキュリティメタアノテーションは、セマンティクスを追加し、ビジネスロジックをセキュリティフレームワークから切り離すため、優れたアイデアです。

3.6. クラスレベルでのセキュリティアノテーション

1つのクラス内のすべてのメソッドに同じセキュリティアノテーションを使用していることに気付いた場合は、そのアノテーションをクラスレベルに配置することを検討できます。

@Service
@PreAuthorize("hasRole('ROLE_ADMIN')")
public class SystemService {

    public String getSystemYear(){
        //...
    }
 
    public String getSystemDate(){
        //...
    }
}

上記の例では、セキュリティルール hasRole(’ROLE_ADMIN’)getSystemYearメソッドとgetSystemDateメソッドの両方に適用されます。

3.7. メソッド上の複数のセキュリティアノテーション

1つのメソッドで複数のセキュリティアノテーションを使用することもできます。

@PreAuthorize("#username == authentication.principal.username")
@PostAuthorize("returnObject.username == authentication.principal.nickName")
public CustomUser securedLoadUserDetail(String username) {
    return userRoleRepository.loadUserByUserName(username);
}

このようにして、SpringはsecuredLoadUserDetailメソッドの実行前と実行後の両方で承認を検証します。

4. 重要な考慮事項

メソッドのセキュリティに関して覚えておきたい点が2つあります。

  • デフォルトでは、SpringAOPプロキシを使用してメソッドセキュリティを適用します。 保護されたメソッドAが同じクラス内の別のメソッドによって呼び出された場合、Aのセキュリティは完全に無視されます。 これは、メソッドAがセキュリティチェックなしで実行されることを意味します。 同じことがプライベートメソッドにも当てはまります。
  • SpringSecurityContextはスレッドにバインドされています。 デフォルトでは、セキュリティコンテキストは子スレッドに伝播されません。 詳細については、 Spring Security ContextPropagationの記事を参照してください。

5. テスト方法のセキュリティ

5.1. 構成

JUnitを使用してSpringSecurityをテストするには、spring-security-testの依存関係が必要です

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
</dependency>

Spring Bootプラグインを使用しているため、依存関係のバージョンを指定する必要はありません。 この依存関係の最新バージョンは、 MavenCentralにあります。

次に、ランナーと ApplicationContext 構成を指定して、簡単なSpring統合テストを構成しましょう。

@RunWith(SpringRunner.class)
@ContextConfiguration
public class MethodSecurityIntegrationTest {
    // ...
}

5.2. ユーザー名と役割のテスト

構成の準備ができたので、 @Secured( “ROLE_VIEWER”)アノテーションで保護したgetUsernameメソッドをテストしてみましょう。

@Secured("ROLE_VIEWER")
public String getUsername() {
    SecurityContext securityContext = SecurityContextHolder.getContext();
    return securityContext.getAuthentication().getName();
}

ここでは@Securedアノテーションを使用しているため、メソッドを呼び出すにはユーザーを認証する必要があります。 それ以外の場合は、AuthenticationCredentialsNotFoundExceptionが発生します。

そう、 安全な方法をテストするためのユーザーを提供する必要があります。

これを実現するために、テストメソッドを@WithMockUserで装飾し、ユーザーとロールを提供します

@Test
@WithMockUser(username = "john", roles = { "VIEWER" })
public void givenRoleViewer_whenCallGetUsername_thenReturnUsername() {
    String userName = userRoleService.getUsername();
    
    assertEquals("john", userName);
}

ユーザー名がjohnで、役割がROLE_VIEWERの認証済みユーザーを提供しました。 usernameまたはroleを指定しない場合、デフォルトのusernameuserであり、デフォルトのroleROLE_USER

ここでROLE_プレフィックスを追加する必要はないことに注意してください。これは、Springセキュリティがそのプレフィックスを自動的に追加するためです。

そのプレフィックスを付けたくない場合は、roleの代わりにauthorityを使用することを検討できます。

たとえば、getUsernameInLowerCaseメソッドを宣言しましょう。

@PreAuthorize("hasAuthority('SYS_ADMIN')")
public String getUsernameLC(){
    return getUsername().toLowerCase();
}

当局を使用してそれをテストすることができます:

@Test
@WithMockUser(username = "JOHN", authorities = { "SYS_ADMIN" })
public void givenAuthoritySysAdmin_whenCallGetUsernameLC_thenReturnUsername() {
    String username = userRoleService.getUsernameInLowerCase();

    assertEquals("john", username);
}

便利なことに、多くのテストケースで同じユーザーを使用したい場合は、テストクラスで@WithMockUserアノテーションを宣言できます。

@RunWith(SpringRunner.class)
@ContextConfiguration
@WithMockUser(username = "john", roles = { "VIEWER" })
public class MockUserAtClassLevelIntegrationTest {
    //...
}

匿名ユーザーとしてテストを実行する場合は、@WithAnonymousUserアノテーションを使用できます

@Test(expected = AccessDeniedException.class)
@WithAnonymousUser
public void givenAnomynousUser_whenCallGetUsername_thenAccessDenied() {
    userRoleService.getUsername();
}

上記の例では、匿名ユーザーにロールROLE_VIEWERまたは権限SYS_ADMINが付与されていないため、AccessDeniedExceptionが予想されます。

5.3. カスタムUserDetailsServiceを使用したテスト

ほとんどのアプリケーションでは、認証プリンシパルとしてカスタムクラスを使用するのが一般的です。この場合、カスタムクラスはorg.springframework.security.core.userdetails。[X204Xを実装する必要があります。 ]UserDetailsインターフェース。

この記事では、UserDetailsの既存の実装を拡張するCustomUserクラス、つまりorg.springframework.security.core.userdetails。を宣言します。ユーザー

public class CustomUser extends User {
    private String nickName;
    // getter and setter
}

セクション3の@PostAuthorizeアノテーションを使用した例を振り返ってみましょう。

@PostAuthorize("returnObject.username == authentication.principal.nickName")
public CustomUser loadUserDetail(String username) {
    return userRoleRepository.loadUserByUserName(username);
}

この場合、返されたCustomUserusernameが現在の認証プリンシパルのニックネームと等しい場合にのみ、メソッドは正常に実行されます。

そのメソッドをテストしたい場合は、ユーザー名に基づいてCustomUserをロードできるUserDetailsServiceの実装を提供できます。

@Test
@WithUserDetails(
  value = "john", 
  userDetailsServiceBeanName = "userDetailService")
public void whenJohn_callLoadUserDetail_thenOK() {
 
    CustomUser user = userService.loadUserDetail("jane");

    assertEquals("jane", user.getNickName());
}

ここで、 @WithUserDetails アノテーションは、UserDetailsServiceを使用して認証済みユーザーを初期化することを示しています。 サービスはによって参照されます userDetailsServiceBeanName 財産これ UserDetailsService テスト目的の実際の実装または偽物である可能性があります。

さらに、サービスはプロパティvalueの値をユーザー名として使用してUserDetailsをロードします。

便利なことに、 @WithMockUser アノテーションで行ったのと同様に、クラスレベルで@WithUserDetailsアノテーションを使用して装飾することもできます。

5.4. メタアノテーションを使用したテスト

さまざまなテストで、同じユーザー/ロールを何度も再利用することがよくあります。

このような状況では、メタアノテーションを作成すると便利です。

前の例@WithMockUser(username =” john”、roles = {“ VIEWER”})をもう一度見ると、メタアノテーションを宣言できます。

@Retention(RetentionPolicy.RUNTIME)
@WithMockUser(value = "john", roles = "VIEWER")
public @interface WithMockJohnViewer { }

次に、テストで@WithMockJohnViewerを使用するだけです。

@Test
@WithMockJohnViewer
public void givenMockedJohnViewer_whenCallGetUsername_thenReturnUsername() {
    String userName = userRoleService.getUsername();

    assertEquals("john", userName);
}

同様に、メタアノテーションを使用して、@WithUserDetailsを使用してドメイン固有のユーザーを作成できます。

6. 結論

この記事では、Springセキュリティでメソッドセキュリティを使用するためのさまざまなオプションについて説明しました。

また、メソッドのセキュリティを簡単にテストするためのいくつかの手法を試し、モックされたユーザーをさまざまなテストで再利用する方法を学びました。

この記事のすべての例は、GitHubにあります。