Springメソッドセキュリティの紹介

Spring Method Securityの概要

1. 前書き

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

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

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

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

まず、Springメソッドセキュリティを使用するには、spring-security-configの依存関係を追加する必要があります。


    org.springframework.security
    spring-security-config

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

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


    org.springframework.boot
    spring-boot-starter-security

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

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

@Configuration
@EnableGlobalMethodSecurity(
  prePostEnabled = true,
  securedEnabled = true,
  jsr250Enabled = true)
public class MethodSecurityConfig
  extends GlobalMethodSecurityConfiguration {
}
  • prePostEnabledプロパティは、SpringSecurityの事前/事後注釈を有効にします

  • securedEnabledプロパティは、@Secured注釈を有効にする必要があるかどうかを決定します

  • jsr250Enabledプロパティを使用すると、@RoleAllowedアノテーションを使用できます

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

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

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

The @Secured annotation is used to specify a list of roles on a method.したがって、ユーザーは、指定されたロールの少なくとも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 Expression Language(SpEL)をサポートしていません。

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

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

基本的に、@Securedと同様の方法で@RoleAllowedアノテーションを使用できます。 したがって、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注釈の使用

Both @PreAuthorize and @PostAuthorize annotations provide expression-based access control.したがって、述語はSpEL (Spring Expression Language)を使用して記述できます。

The @PreAuthorize annotation checks the given expression before entering the methodに対して、the @PostAuthorize annotation verifies it after the execution of the method and could alter the result

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

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

@PreAuthorize(“hasRole(‘ROLE_VIEWER')”)は、前のセクションで使用した@Secured(“ROLE_VIEWER”)と同じ意味です。 より多くのsecurity expressions details in previous articlesを自由に発見してください。

したがって、注釈@Secured(\{“ROLE_VIEWER”,”ROLE_EDITOR”})@PreAuthorize(“hasRole(‘ROLE_VIEWER') or hasRole(‘ROLE_EDITOR')”):に置き換えることができます。

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

さらに、we can actually use the method argument as part of the expression

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

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

It’s worth to note that @PreAuthorize expressions can be replaced by @PostAuthorize ones

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

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

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

さらに、the @PostAuthorize annotation provides the ability to access the method result

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

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

このセクションでは、主に単純なSpring式を使用します。 より複雑なシナリオでは、custom security expressionsを作成できます。

3.4. @PreFilterおよび@PostFilter注釈の使用

Spring Security provides the @PreFilter annotation to filter a collection argument before executing the method

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

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

ここでは、our expression uses the name filterObject to represent the current object in the collectionです。

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

@PreFilter
  (value = "filterObject != authentication.principal.username",
  filterTarget = "usernames")
public String joinUsernamesAndRoles(
  List usernames, List roles) {

    return usernames.stream().collect(Collectors.joining(";"))
      + ":" + roles.stream().collect(Collectors.joining(";"));
}

さらに、we can also filter the returned collection of a method by using @PostFilter annotation

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

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

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

@PreFilter@PostFilterの詳細については、Spring Security – @PreFilter and @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つあります。

  • By default, Spring AOP proxying is used to apply method security –保護されたメソッドAが同じクラス内の別のメソッドによって呼び出された場合、Aのセキュリティは完全に無視されます。 これは、メソッドAがセキュリティチェックなしで実行されることを意味します。 同じことがプライベートメソッドにも当てはまります

  • Spring SecurityContext is thread-bound –はデフォルトで、セキュリティコンテキストは子スレッドに伝播されません。 詳細については、Spring Security Context Propagationの記事を参照してください。

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

5.1. 設定

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


    org.springframework.security
    spring-security-test

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

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

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

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

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

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

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

したがって、we need to provide a user to test our secured method. To achieve this, we decorate the test method with @WithMockUser and provide a user and roles:

@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_プレフィックスを追加する必要はないことに注意してください。SpringSecurityはそのプレフィックスを自動的に追加します。

そのプレフィックスを付けたくない場合は、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);
}

便利なことに、if we want to use the same user for many test cases, we can declare the @WithMockUser annotation at test class

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

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

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

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

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

For most applications, it’s common to use a custom class as authentication principal。 この場合、カスタムクラスはorg.springframework.security.core.userdetails.UserDetailsインターフェイスを実装する必要があります。

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

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が現在の認証プリンシパルのnicknameと等しい場合にのみ、メソッドは正常に実行されます。

そのメソッドをテストしたい場合, we could provide an implementation of UserDetailsService which could load our CustomUser based on the username

@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. メタアノテーションを使用したテスト

多くの場合、さまざまなテストで同じユーザー/ロールを何度も再利用しています。

このような状況では、meta-annotationを作成すると便利です。

前の例@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. 結論

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

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

このチュートリアルのすべての例は、over on Githubにあります。