1前書き
簡単に言うと、Spring Securityはメソッドレベルで認可セマンティクスをサポートしています。
通常、たとえば特定のメソッドを実行できるロールを制限することによってサービス層を保護し、専用のメソッドレベルのセキュリティテストサポートを使用してテストすることができます。
この記事では、最初にいくつかのセキュリティアノテーションの使用方法について説明します。次に、さまざまな戦略を使用してメソッドのセキュリティをテストすることに焦点を当てます。
2メソッドセキュリティを有効にする
まず第一に、Spring Method Securityを使用するには、 spring-security-config 依存関係を追加する必要があります。
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
最新版はhttps://search.maven.org/classic/#search%7Cga%7C1%7C%22spring-security-config%22[Maven Central]にあります。
Spring Bootを使用したい場合は、 spring-security-config を含む spring-boot-starter-security 依存関係を使用できます。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
繰り返しますが、最新版はhttps://search.maven.org/classic/#search%7Cga%7C1%7C%22spring-boot-starter-security%22[Maven Central]で見つけることができます。
次に、グローバルなメソッドセキュリティを有効にする必要があります。
@Configuration
@EnableGlobalMethodSecurity(
prePostEnabled = true,
securedEnabled = true,
jsr250Enabled = true)
public class MethodSecurityConfig
extends GlobalMethodSecurityConfiguration {
}
-
prePostEnabled プロパティはSpring Securityのプリ/ポストを有効にする
注釈 ** 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 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 アノテーションを使用する
-
@ PreAuthorize と @ PostAuthorize の両方のアノテーションは、式に基づいたアクセス制御を提供します。
-
@ PreAuthorize アノテーションはメソッドに入る前に与えられた式をチェックします 、 @ PostAuthorize アノテーションはメソッドの実行後にそれを検証し結果を変更することができます
それでは、次のように getUsernameInUpperCase メソッドを宣言しましょう。
@PreAuthorize("hasRole('ROLE__VIEWER')")
public String getUsernameInUpperCase() {
return getUsername().toUpperCase();
}
@ PreAuthorize(“ hasRole( "ROLE VIEWER”))) は、前のセクションで使用した @ Secured(" ROLE VIEWER ") と同じ意味です。より多くのリンクを発見すること自由に感じなさい:/spring-security-expressions-basic[前の記事のセキュリティ表現の詳細]。
したがって、アノテーション @ 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) {
//...
}
さらに、** 実際には式の一部としてmethod引数を使用できます。
@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__アノテーションはメソッドの結果にアクセスする機能を提供します。
@PostAuthorize
("returnObject.username == authentication.principal.nickName")
public CustomUser loadUserDetail(String username) {
return userRoleRepository.loadUserByUserName(username);
}
この例では、 loadUserDetail メソッドは、返された CustomUser の username が現在の認証プリンシパルの__nicknameと等しい場合にのみ正常に実行されます。
このセクションでは、単純な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(";"));
}
この例では、認証されたユーザーを除くすべてのユーザー名に参加しています。
ここで、 式は filterObject という名前を使用して、コレクション内の現在のオブジェクトを表します 。
ただし、メソッドにコレクション型の引数が複数ある場合は、 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は返されたリストを繰り返し処理し、プリンシパルのユーザ名と一致する値をすべて削除します。
@ PreFilter と @ PostFilter の詳細はリンクにあります:/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点指摘しておきたいことがあります。
-
デフォルトでは、Spring AOPプロキシはメソッドセキュリティを適用するために使用されます -
保護されたメソッドAが同じクラス内の別のメソッドによって呼び出された場合 Aのセキュリティはまったく無視されます。これはメソッドAが実行することを意味します セキュリティチェックなし。プライベートメソッドにも同じことが当てはまります。 Spring SecurityContext はスレッドバウンドです - デフォルトでは** セキュリティ
コンテキストは子スレッドに伝播されません。詳細については、 Spring Security Context Propagation 記事
5試験方法セキュリティ
5.1. 構成
JUnitでSpring Securityをテストするには、 spring-security-test 依存関係が必要です。**
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
</dependency>
Spring Bootプラグインを使用しているため、依存関係のバージョンを指定する必要はありません。この依存関係の最新版はhttps://search.maven.org/classic/#search%7Cga%7C1%7C%22spring-security-test%22[Maven Central]で見つけることができます。
次に、ランナーと ApplicationContext 設定を指定して、簡単なSpring Integrationテストを設定しましょう。
@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が発生します。
したがって、 私たちは私たちの安全な方法をテストするためにユーザーを提供する必要があります。これを達成するために、テストメソッドを @ WithMockUser で装飾し、ユーザーとロールを提供します。
@Test
@WithMockUser(username = "john", roles = { "VIEWER" })
public void givenRoleViewer__whenCallGetUsername__thenReturnUsername() {
String userName = userRoleService.getUsername();
assertEquals("john", userName);
}
ユーザー名が john 、ロールが ROLE VIEWER の認証済みユーザーを指定しました。 username または role を指定しない場合、デフォルトの username は user 、デフォルトの role は ROLE 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);
}
便利なことに、** 多くのテストケースで同じユーザーを使いたい場合は、テストクラスで @ WithMockUser アノテーションを宣言することができます。
@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 を使用したテスト
-
ほとんどのアプリケーションでは、認証プリンシパルとしてカスタムクラスを使用するのが一般的です** 。この場合、カスタムクラスは org.springframework.security.core.userdetails.UserDetails インターフェースを実装する必要があります。
この記事では、 UserDetails の既存の実装を拡張する CustomUser クラスを宣言します。これは、__org.springframework.security.core.userdetails.Userです。
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);
}
この場合、返される CustomUser の username が現在の認証プリンシパルの nickname と等しい場合にのみ、メソッドは正常に実行されます。
そのメソッド をテストしたい場合は、ユーザー名 に基づいて 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 プロパティによって参照されます。
さらに、サービスは UserDetails をロードするためのユーザー名としてプロパティ value の値を使用します。
便利なことに、 @ 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. 結論
このチュートリアルでは、Spring Securityでメソッドセキュリティを使用するためのさまざまなオプションについて説明しました。
また、メソッドセキュリティを簡単にテストするためのいくつかの手法を実行し、モックされたユーザーをさまざまなテストで再利用する方法を学習しました。
このチュートリアルのすべての例はhttps://github.com/eugenp/tutorials/tree/master/spring-security-core[over on Github]にあります。