Introduction to Spring Method Security – Spring方法安全介绍

最后修改: 2018年 1月 20日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

Simply put, Spring Security supports authorization semantics at the method level.

简单地说,Spring Security在方法层面支持授权语义。

Typically, we could secure our service layer by, for example, restricting which roles are able to execute a particular method — and test it using dedicated method-level security test support.

通常情况下,我们可以通过限制哪些角色能够执行特定的方法来保护我们的服务层–并使用专门的方法级安全测试支持进行测试。

In this tutorial, we’re going to review the use of some security annotations. Then we’ll focus on testing our method security with different strategies.

在本教程中,我们将回顾一些安全注释的使用。然后,我们将专注于用不同的策略测试我们的方法安全性。

2. Enabling Method Security

2.启用方法安全

First, to use Spring Method Security, we need to add the spring-security-config dependency:

首先,为了使用Spring方法安全,我们需要添加spring-security-config依赖。

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

We can find its latest version on Maven Central.

我们可以在Maven Central上找到其最新版本。

If we want to use Spring Boot, we can use the spring-boot-starter-security dependency, which includes spring-security-config:

如果我们想使用Spring Boot,我们可以使用spring-boot-starter-security依赖项,其中包括spring-security-config

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

Again, the latest version can be found on Maven Central.

同样,最新版本可以在Maven Central上找到。

Next, we need to enable global Method Security:

接下来,我们需要启用全局方法安全

@Configuration
@EnableGlobalMethodSecurity(
  prePostEnabled = true, 
  securedEnabled = true, 
  jsr250Enabled = true)
public class MethodSecurityConfig 
  extends GlobalMethodSecurityConfiguration {
}
  • The prePostEnabled property enables Spring Security pre/post annotations.
  • The securedEnabled property determines if the @Secured annotation should be enabled.
  • The jsr250Enabled property allows us to use the @RoleAllowed annotation.

We’ll explore more about these annotations in the next section.

我们将在下一节探讨更多关于这些注释的内容。

3. Applying Method Security

3.应用方法的安全性

3.1. Using @Secured Annotation

3.1.使用@Secured注释

The @Secured annotation is used to specify a list of roles on a method. So, a user only can access that method if she has at least one of the specified roles.

@Secured注解用于指定一个方法上的角色列表。因此,用户只有在拥有至少一个指定角色的情况下才能访问该方法。

Let’s define a getUsername method:

让我们定义一个getUsername方法。

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

Here the @Secured(“ROLE_VIEWER”) annotation defines that only users who have the role ROLE_VIEWER are able to execute the getUsername method.

这里@Secured(“ROLE_VIEWER”)注解定义了只有拥有ROLE_VIEWER角色的用户才能够执行getUsername方法。

Besides, we can define a list of roles in a @Secured annotation:

此外,我们可以在@Secured注解中定义一个角色列表。

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

In this case, the configuration states that if a user has either ROLE_VIEWER or ROLE_EDITOR, that user can invoke the isValidUsername method.

在这种情况下,配置规定,如果一个用户拥有ROLE_VIEWERROLE_EDITOR,该用户可以调用isValidUsername方法。

The @Secured annotation doesn’t support Spring Expression Language (SpEL).

@Secured注解不支持Spring表达式语言(SpEL)

3.2. Using @RolesAllowed Annotation

3.2.使用@RolesAllowed 注释

The @RolesAllowed annotation is the JSR-250’s equivalent annotation of the @Secured annotation.

@RolesAllowed注解是JSR-250对@Secured注解的等同注解。

Basically, we can use the @RolesAllowed annotation in a similar way as @Secured.

基本上,我们可以用类似于@RolesAllowed注解的方式使用@Secured

This way, we could redefine getUsername and isValidUsername methods:

这样,我们可以重新定义getUsernameisValidUsername方法。

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

Similarly, only the user who has role ROLE_VIEWER can execute getUsername2.

同样地,只有拥有角色ROLE_VIEWER的用户可以执行getUsername2

Again, a user is able to invoke isValidUsername2 only if she has at least one of the ROLE_VIEWER or ROLER_EDITOR roles.

同样,只有当一个用户至少拥有ROLE_VIEWERROLER_EDITOR角色之一时,她才能调用isValidUsername2

3.3. Using @PreAuthorize and @PostAuthorize Annotations

3.3.使用@PreAuthorize@PostAuthorize注解

Both @PreAuthorize and @PostAuthorize annotations provide expression-based access control. So, predicates can be written using SpEL (Spring Expression Language).

@PreAuthorize@PostAuthorize注解都提供了基于表达式的访问控制。因此,可以使用SpEL(Spring表达式语言)编写谓词。

The @PreAuthorize annotation checks the given expression before entering the method, whereas the @PostAuthorize annotation verifies it after the execution of the method and could alter the result.

@PreAuthorize注解在进入方法之前检查给定的表达式,而@PostAuthorize注解在方法执行之后进行验证,并可能改变结果。

Now let’s declare a getUsernameInUpperCase method as below:

现在让我们声明一个getUsernameInUpperCase方法,如下。

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

The @PreAuthorize(“hasRole(‘ROLE_VIEWER’)”) has the same meaning as @Secured(“ROLE_VIEWER”), which we used in the previous section. Feel free to discover more security expressions details in previous articles.

@PreAuthorize(“hasRole(‘ROLLE_VIEWER’)”)与我们在上一节中使用的@Secured(“ROLE_VIEWER”)具有相同的含义。欢迎在以前的文章中发现更多安全表达式的细节

Consequently, the annotation @Secured({“ROLE_VIEWER”,”ROLE_EDITOR”}) can be replaced with @PreAuthorize(“hasRole(‘ROLE_VIEWER’) or hasRole(‘ROLE_EDITOR’)”):

因此,注解@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) {
    //...
}

Moreover, we can actually use the method argument as part of the expression:

此外,我们实际上可以将方法参数作为表达式的一部分

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

Here a user can invoke the getMyRoles method only if the value of the argument username is the same as current principal’s username.

这里,只有当参数username的值与当前校长的用户名相同时,用户才能调用getMyRoles方法。

It’s worth noting that @PreAuthorize expressions can be replaced by @PostAuthorize ones.

值得注意的是,@PreAuthorize表达式可以被@PostAuthorize表达式所取代。

Let’s rewrite getMyRoles:

让我们重写getMyRoles

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

In the previous example, however, the authorization would get delayed after the execution of the target method.

然而,在前面的例子中,授权将在执行目标方法后被延迟。

Additionally, the @PostAuthorize annotation provides the ability to access the method result:

此外,@PostAuthorize注解提供了访问方法结果的能力

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

Here the loadUserDetail method would only execute successfully if the username of the returned CustomUser is equal to the current authentication principal’s nickname.

这里的loadUserDetail方法只有在返回的CustomUserusername等于当前认证委托人的nickname时才会成功执行。

In this section, we mostly use simple Spring expressions. For more complex scenarios, we could create custom security expressions.

在本节中,我们主要使用简单的Spring表达式。对于更复杂的场景,我们可以创建自定义安全表达式

3.4. Using @PreFilter and @PostFilter Annotations

3.4.使用@PreFilter@PostFilter 注释

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

Spring Security提供了@PreFilter注解,以便在执行方法之前过滤集合参数

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

In this example, we’re joining all usernames except for the one that is authenticated.

在这个例子中,我们要加入所有的用户名,除了经过验证的那个。

Here, in our expression, we use the name filterObject to represent the current object in the collection.

这里,在我们的表达式中,我们使用filterObject这个名字来表示集合中的当前对象。

However, if the method has more than one argument that is a collection type, we need to use the filterTarget property to specify which argument we want to filter:

然而,如果该方法有一个以上的参数是集合类型,我们需要使用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(";"));
}

Additionally, we can also filter the returned collection of a method by using the @PostFilter annotation:

此外,我们还可以通过使用@PostFilter注解来过滤一个方法的返回集合

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

In this case, the name filterObject refers to the current object in the returned collection.

在这种情况下,名字filterObject指的是返回集合中的当前对象。

With that configuration, Spring Security will iterate through the returned list and remove any value matching the principal’s username.

有了这个配置,Spring Security将遍历返回的列表,并删除任何与校长的用户名匹配的值。

Our Spring Security – @PreFilter and @PostFilter article describes both annotations in greater detail.

我们的Spring Security – @PreFilter 和 @PostFilter文章更详细地描述了这两个注释。

3.5. Method Security Meta-Annotation

3.5.方法安全元注解

We typically find ourselves in a situation where we protect different methods using the same security configuration.

我们通常会发现自己处于这样一种情况:我们使用相同的安全配置保护不同的方法。

In this case, we can define a security meta-annotation:

在这种情况下,我们可以定义一个安全元注释。

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

Next, we can directly use the @IsViewer annotation to secure our method:

接下来,我们可以直接使用@IsViewer注解来保护我们的方法。

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

Security meta-annotations are a great idea because they add more semantics and decouple our business logic from the security framework.

安全元注释是一个很好的想法,因为它增加了更多的语义,并将我们的业务逻辑与安全框架解耦。

3.6. Security Annotation at the Class Level

3.6.类级的安全注释

If we find ourselves using the same security annotation for every method within one class, we can consider putting that annotation at class level:

如果我们发现自己在一个类中的每个方法都使用相同的安全注解,我们可以考虑将该注解放在类的级别。

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

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

In above example, the security rule hasRole(‘ROLE_ADMIN’) will be applied to both getSystemYear and getSystemDate methods.

在上例中,安全规则hasRole(‘ROLE_ADMIN’)将应用于getSystemYeargetSystemDate方法。

3.7. Multiple Security Annotations on a Method

3.7.一个方法上的多个安全注释

We can also use multiple security annotations on one method:

我们也可以在一个方法上使用多个安全注释。

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

This way, Spring will verify authorization both before and after the execution of the securedLoadUserDetail method.

这样,Spring将在执行securedLoadUserDetail方法之前和之后都验证授权。

4. Important Considerations

4.重要的考虑因素

There are two points we’d like to recall regarding method security:

关于方法安全,有两点我们想回顾一下。

  • By default, Spring AOP proxying is used to apply method security. If a secured method A is called by another method within the same class, security in A is ignored altogether. This means method A will execute without any security checking. The same applies to private methods.
  • Spring SecurityContext is thread-bound. By default, the security context isn’t propagated to child threads. For more information, refer to our Spring Security Context Propagation article.

5. Testing Method Security

5.测试方法的安全性

5.1. Configuration

5.1.配置

To test Spring Security with JUnit, we need the spring-security-test dependency:

为了用JUnit测试Spring Security,我们需要spring-security-test依赖性

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

We don’t need to specify the dependency version because we’re using the Spring Boot plugin. We can find the latest version of this dependency on Maven Central.

我们不需要指定依赖版本,因为我们使用的是Spring Boot插件。我们可以在Maven Central上找到该依赖的最新版本。

Next, let’s configure a simple Spring Integration test by specifying the runner and the ApplicationContext configuration:

接下来,让我们通过指定运行器和ApplicationContext配置来配置一个简单的Spring集成测试。

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

5.2. Testing Username and Roles

5.2.测试用户名和角色

Now that our configuration is ready, let’s try to test our getUsername method that we secured with the @Secured(“ROLE_VIEWER”) annotation:

现在我们的配置已经准备好了,让我们试着测试我们的getUsername 方法,我们用@Secured(“ROLE_VIEWER”) 注解来保证这个方法。

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

Since we use the @Secured annotation here, it requires a user to be authenticated to invoke the method. Otherwise, we’ll get an AuthenticationCredentialsNotFoundException.

由于我们在这里使用了@Secured 注解,它要求用户经过认证才能调用该方法。否则,我们会得到一个AuthenticationCredentialsNotFoundException

So, 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:

为了实现这一点,我们用@WithMockUser装饰测试方法,并提供一个用户和角色

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

We’ve provided an authenticated user whose username is john and whose role is ROLE_VIEWER. If we don’t specify the username or role, the default username is user and default role is ROLE_USER.

我们提供了一个认证用户,其用户名是john,其角色是ROLE_VIEWER。如果我们不指定用户名角色,默认的用户名user,默认的角色ROLE_USER

Note that it isn’t necessary to add the ROLE_ prefix here because Spring Security will add that prefix automatically.

请注意,这里没有必要添加ROLE_前缀,因为Spring Security将自动添加该前缀。

If we don’t want to have that prefix, we can consider using authority instead of role.

如果我们不想有这个前缀,我们可以考虑用authority代替role

For example, let’s declare a getUsernameInLowerCase method:

例如,让我们声明一个getUsernameInLowerCase方法。

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

We could test that using authorities:

我们可以用当局来测试。

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

    assertEquals("john", username);
}

Conveniently, if we want to use the same user for many test cases, we can declare the @WithMockUser annotation at test class:

方便的是,如果我们想在许多测试案例中使用同一个用户,我们可以在测试类中声明@WithMockUser注解

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

If we wanted to run our test as an anonymous user, we could use the @WithAnonymousUser annotation:

如果我们想以匿名用户的身份运行我们的测试,我们可以使用@WithAnonymousUser 注解

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

In the example above, we expect an AccessDeniedException because the anonymous user isn’t granted the role ROLE_VIEWER or the authority SYS_ADMIN.

在上面的例子中,我们期待一个AccessDeniedException,因为匿名用户没有被授予角色ROLE_VIEWER或权限SYS_ADMIN

5.3. Testing With a Custom UserDetailsService

5.3.使用自定义的UserDetailsService测试

For most applications, it’s common to use a custom class as authentication principal. In this case, the custom class needs to implement the org.springframework.security.core.userdetails.UserDetails interface.

对于大多数应用程序来说,使用自定义类作为认证主体是很常见的。在这种情况下,自定义类需要实现org.springframework.security.core.userdetails.UserDetails接口。

In this article, we declare a CustomUser class that extends the existing implementation of UserDetails, which is org.springframework.security.core.userdetails.User:

在本文中,我们声明了一个CustomUser类,它扩展了现有的UserDetails的实现,即org.springframework.security.core.userdetails.User

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

Let’s look back at the example with the @PostAuthorize annotation in Section 3:

让我们回顾一下第3节中带有@PostAuthorize注解的例子。

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

In this case, the method would only execute successfully if the username of the returned CustomUser is equal to the current authentication principal’s nickname.

在这种情况下,只有当返回的CustomUserusername等于当前认证委托人的nickname时,该方法才能成功执行。

If we wanted to test that method, we could provide an implementation of UserDetailsService that could load our CustomUser based on the username:

如果我们想测试这个方法,我们可以提供一个UserDetailsService的实现,它可以根据用户名加载我们的CustomUser

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

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

Here the @WithUserDetails annotation states that we’ll use a UserDetailsService to initialize our authenticated user. The service is referred by the userDetailsServiceBeanName property. This UserDetailsService might be a real implementation or a fake for testing purposes.

这里的@WithUserDetails注解指出,我们将使用UserDetailsService来初始化我们的认证用户。该服务由userDetailsServiceBeanName属性引用。这个UserDetailsService可能是一个真正的实现,也可能是一个用于测试的假的。

Additionally, the service will use the value of the property value as the username to load UserDetails.

此外,服务将使用属性value的值作为用户名来加载UserDetails

Conveniently, we can also decorate with a @WithUserDetails annotation at the class level, similarly to what we did with the @WithMockUser annotation.

方便的是,我们也可以在类的层面上用@WithUserDetails 注解进行装饰,就像我们对@WithMockUser 注解所做的一样。

5.4. Testing With Meta Annotations

5.4.使用元注释进行测试

We often find ourselves reusing the same user/roles over and over again in various tests.

我们经常发现自己在各种测试中反复使用同一个用户/角色。

For these situations, it’s convenient to create a meta-annotation.

对于这些情况,创建一个meta-annotation很方便。

Looking again at the previous example @WithMockUser(username=”john”, roles={“VIEWER”}), we can declare a meta-annotation:

再看看前面的例子@WithMockUser(username=”john”, roles={“VIEWER”}),我们可以声明一个元注释。

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

Then we can simply use @WithMockJohnViewer in our test:

然后我们可以简单地在测试中使用@WithMockJohnViewer

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

    assertEquals("john", userName);
}

Likewise, we can use meta-annotations to create domain-specific users using @WithUserDetails.

同样地,我们可以使用元注释,使用@WithUserDetails创建特定领域的用户。

6. Conclusion

6.结论

In this article, we’ve explored various options for using Method Security in Spring Security.

在这篇文章中,我们已经探讨了在Spring Security中使用方法安全的各种选项。

We have also gone through a few techniques to easily test method security and learned how to reuse mocked users in different tests.

我们还经历了一些技术来轻松测试方法的安全性,并学习了如何在不同的测试中重用模拟用户。

All examples for this article can be found over on GitHub.

本文的所有例子都可以在GitHub上找到over