A Custom Security Expression with Spring Security – 使用Spring Security的自定义安全表达式

最后修改: 2016年 7月 31日

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

1. Overview

1.概述

In this tutorial, we’ll focus on creating a custom security expression with Spring Security.

在本教程中,我们将重点讨论用Spring Security创建一个自定义的安全表达式

Sometimes, the expressions available in the framework are simply not expressive enough. And, in these cases, it’s relatively simple to built up a new expression that is semantically richer than the existing ones.

有时,框架中可用的表达式根本没有足够的表现力。在这种情况下,建立一个在语义上比现有表达式更丰富的新表达式是相对简单的。

We’ll first discuss how to create a custom PermissionEvaluator, then a fully custom expression – and finally how to override one of the built-in security expression.

我们将首先讨论如何创建一个自定义的PermissionEvaluator,然后是一个完全自定义的表达式–最后是如何覆盖一个内置的安全表达式。

2. A User Entity

2.一个用户实体

First, let’s prepare the foundation for creating the new security expressions.

首先,让我们为创建新的安全表达式准备基础。

Let’s have a look at our User entity – which has a Privileges and an Organization:

让我们看看我们的User实体–它有一个Privileges和一个Organization

@Entity
public class User{
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(nullable = false, unique = true)
    private String username;

    private String password;

    @ManyToMany(fetch = FetchType.EAGER) 
    @JoinTable(name = "users_privileges", 
      joinColumns = 
        @JoinColumn(name = "user_id", referencedColumnName = "id"),
      inverseJoinColumns = 
        @JoinColumn(name = "privilege_id", referencedColumnName = "id")) 
    private Set<Privilege> privileges;

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "organization_id", referencedColumnName = "id")
    private Organization organization;

    // standard getters and setters
}

And here is our simple Privilege:

这里是我们简单的Privilege

@Entity
public class Privilege {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(nullable = false, unique = true)
    private String name;

    // standard getters and setters
}

And our Organization:

而我们的组织

@Entity
public class Organization {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(nullable = false, unique = true)
    private String name;

    // standard setters and getters
}

Finally – we’ll use a simpler custom Principal:

最后–我们将使用一个更简单的自定义Principal

public class MyUserPrincipal implements UserDetails {

    private User user;

    public MyUserPrincipal(User user) {
        this.user = user;
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
        for (Privilege privilege : user.getPrivileges()) {
            authorities.add(new SimpleGrantedAuthority(privilege.getName()));
        }
        return authorities;
    }
    
    ...
}

With all of these classes ready, we’re going to use our custom Principal in a basic UserDetailsService implementation:

在所有这些类准备就绪后,我们将在一个基本的UserDetailsService实现中使用我们的自定义Principal

@Service
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) {
        User user = userRepository.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException(username);
        }
        return new MyUserPrincipal(user);
    }
}

As you can see, there’s nothing complicated about these relationships – the user has one or more privileges, and each user belong to one organization.

正如你所看到的,这些关系并不复杂–用户有一个或多个权限,每个用户属于一个组织。

3. Data Setup

3.数据设置[/strong]

Next – let’s initialize our database with simple test data:

接下来–让我们用简单的测试数据初始化我们的数据库。

@Component
public class SetupData {
    @Autowired
    private UserRepository userRepository;

    @Autowired
    private PrivilegeRepository privilegeRepository;

    @Autowired
    private OrganizationRepository organizationRepository;

    @PostConstruct
    public void init() {
        initPrivileges();
        initOrganizations();
        initUsers();
    }
}

Here is our init methods:

这里是我们的init方法。

private void initPrivileges() {
    Privilege privilege1 = new Privilege("FOO_READ_PRIVILEGE");
    privilegeRepository.save(privilege1);

    Privilege privilege2 = new Privilege("FOO_WRITE_PRIVILEGE");
    privilegeRepository.save(privilege2);
}
private void initOrganizations() {
    Organization org1 = new Organization("FirstOrg");
    organizationRepository.save(org1);
    
    Organization org2 = new Organization("SecondOrg");
    organizationRepository.save(org2);
}
private void initUsers() {
    Privilege privilege1 = privilegeRepository.findByName("FOO_READ_PRIVILEGE");
    Privilege privilege2 = privilegeRepository.findByName("FOO_WRITE_PRIVILEGE");
    
    User user1 = new User();
    user1.setUsername("john");
    user1.setPassword("123");
    user1.setPrivileges(new HashSet<Privilege>(Arrays.asList(privilege1)));
    user1.setOrganization(organizationRepository.findByName("FirstOrg"));
    userRepository.save(user1);
    
    User user2 = new User();
    user2.setUsername("tom");
    user2.setPassword("111");
    user2.setPrivileges(new HashSet<Privilege>(Arrays.asList(privilege1, privilege2)));
    user2.setOrganization(organizationRepository.findByName("SecondOrg"));
    userRepository.save(user2);
}

Note that:

请注意,。

  • User “john” has only FOO_READ_PRIVILEGE
  • User “tom” has both FOO_READ_PRIVILEGE and FOO_WRITE_PRIVILEGE

4. A Custom Permission Evaluator

4.一个自定义的许可评估器

At this point we’re ready to start implementing our new expression – through a new, custom permission evaluator.

在这一点上,我们准备开始实现我们的新表达式–通过一个新的、自定义的权限评估器。

We are going to use the user’s privileges to secure our methods – but instead of using hard coded privilege names, we want to reach a more open, flexible implementation.

我们将使用用户的权限来保护我们的方法–但不是使用硬编码的权限名称,而是要达到一个更加开放、灵活的实现。

Let’s get started.

让我们开始吧。

4.1. PermissionEvaluator

4.1.许可评估器

In order to create our own custom permission evaluator we need to implement the PermissionEvaluator interface:

为了创建我们自己的自定义权限评估器, 我们需要实现PermissionEvaluator接口:

public class CustomPermissionEvaluator implements PermissionEvaluator {
    @Override
    public boolean hasPermission(
      Authentication auth, Object targetDomainObject, Object permission) {
        if ((auth == null) || (targetDomainObject == null) || !(permission instanceof String)){
            return false;
        }
        String targetType = targetDomainObject.getClass().getSimpleName().toUpperCase();
        
        return hasPrivilege(auth, targetType, permission.toString().toUpperCase());
    }

    @Override
    public boolean hasPermission(
      Authentication auth, Serializable targetId, String targetType, Object permission) {
        if ((auth == null) || (targetType == null) || !(permission instanceof String)) {
            return false;
        }
        return hasPrivilege(auth, targetType.toUpperCase(), 
          permission.toString().toUpperCase());
    }
}

Here is our hasPrivilege() method:

这里是我们的hasPrivilege()方法。

private boolean hasPrivilege(Authentication auth, String targetType, String permission) {
    for (GrantedAuthority grantedAuth : auth.getAuthorities()) {
        if (grantedAuth.getAuthority().startsWith(targetType) && 
          grantedAuth.getAuthority().contains(permission)) {
            return true;
        }
    }
    return false;
}

We now have a new security expression available and ready to be used: hasPermission.

我们现在有了一个新的安全表达式,可以随时使用。hasPermission.

And so, instead of using the more hardcoded version:

因此,与其使用更加硬编码的版本。

@PostAuthorize("hasAuthority('FOO_READ_PRIVILEGE')")

We can use use:

我们可以使用使用。

@PostAuthorize("hasPermission(returnObject, 'read')")

or

@PreAuthorize("hasPermission(#id, 'Foo', 'read')")

Note: #id refers to method parameter and ‘Foo‘ refers to target object type.

注意:#id指的是方法参数,’Foo‘指的是目标对象类型。

4.2. Method Security Configuration

4.2.方法安全配置

It’s not enough to define the CustomPermissionEvaluator – we also need to use it in our method security configuration:

仅仅定义CustomPermissionEvaluator是不够的,我们还需要在我们的方法安全配置中使用它。

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {

    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
        DefaultMethodSecurityExpressionHandler expressionHandler = 
          new DefaultMethodSecurityExpressionHandler();
        expressionHandler.setPermissionEvaluator(new CustomPermissionEvaluator());
        return expressionHandler;
    }
}

4.3. Example in Practice

4.3.实践中的例子

Let’s now start making use of the new expression – in a few simple controller methods:

现在让我们开始使用新的表达式–在几个简单的控制器方法中。

@Controller
public class MainController {
    
    @PostAuthorize("hasPermission(returnObject, 'read')")
    @GetMapping("/foos/{id}")
    @ResponseBody
    public Foo findById(@PathVariable long id) {
        return new Foo("Sample");
    }

    @PreAuthorize("hasPermission(#foo, 'write')")
    @PostMapping("/foos")
    @ResponseStatus(HttpStatus.CREATED)
    @ResponseBody
    public Foo create(@RequestBody Foo foo) {
        return foo;
    }
}

And there we go – we’re all set and using the new expression in practice.

就这样–我们都准备好了,并在实践中使用新的表达式。

4.4. The Live Test

4.4.现场测试

Let’s now write a simple live tests – hitting the API and making sure everything’s in working order:

现在让我们写一个简单的实时测试–点击API并确保一切都在工作状态。

@Test
public void givenUserWithReadPrivilegeAndHasPermission_whenGetFooById_thenOK() {
    Response response = givenAuth("john", "123").get("http://localhost:8082/foos/1");
    assertEquals(200, response.getStatusCode());
    assertTrue(response.asString().contains("id"));
}

@Test
public void givenUserWithNoWritePrivilegeAndHasPermission_whenPostFoo_thenForbidden() {
    Response response = givenAuth("john", "123").contentType(MediaType.APPLICATION_JSON_VALUE)
                                                .body(new Foo("sample"))
                                                .post("http://localhost:8082/foos");
    assertEquals(403, response.getStatusCode());
}

@Test
public void givenUserWithWritePrivilegeAndHasPermission_whenPostFoo_thenOk() {
    Response response = givenAuth("tom", "111").contentType(MediaType.APPLICATION_JSON_VALUE)
                                               .body(new Foo("sample"))
                                               .post("http://localhost:8082/foos");
    assertEquals(201, response.getStatusCode());
    assertTrue(response.asString().contains("id"));
}

And here is our givenAuth() method:

这里是我们的givenAuth()方法。

private RequestSpecification givenAuth(String username, String password) {
    FormAuthConfig formAuthConfig = 
      new FormAuthConfig("http://localhost:8082/login", "username", "password");
    
    return RestAssured.given().auth().form(username, password, formAuthConfig);
}

5. A New Security Expression

5.一种新的安全表达方式

With the previous solution, we were able to define and use the hasPermission expression – which can be quite useful.

在之前的解决方案中,我们能够定义和使用hasPermission表达式–这可能相当有用。

However, we’re still somewhat limited here by the name and semantics of the expression itself.

然而,我们在这里仍然受到表达式本身的名称和语义的一些限制。

And so, in this section, we’re going to go full custom – and we’re going to implement a security expression called isMember() – checking if the principal is a member of a Organization.

因此,在这一节中,我们将完全自定义–我们将实现一个名为isMember()的安全表达式–检查校长是否是某个组织的成员。

5.1. Custom Method Security Expression

5.1.自定义方法安全表达式

In order to create this new custom expression, we need start by implementing the root note where the evaluation of all security expressions starts:

为了创建这个新的自定义表达式,我们需要从实现根注开始,所有安全表达式的评估都从这里开始。

public class CustomMethodSecurityExpressionRoot 
  extends SecurityExpressionRoot implements MethodSecurityExpressionOperations {

    public CustomMethodSecurityExpressionRoot(Authentication authentication) {
        super(authentication);
    }

    public boolean isMember(Long OrganizationId) {
        User user = ((MyUserPrincipal) this.getPrincipal()).getUser();
        return user.getOrganization().getId().longValue() == OrganizationId.longValue();
    }

    ...
}

Now how we provided this new operation right in the root note here; isMember() is used to check if current user is a member in given Organization.

现在我们如何在这里提供这个新的操作;isMember()用于检查当前用户是否是给定的Organization中的成员。

Also note how we extended the SecurityExpressionRoot to include the built-in expressions as well.

还要注意我们是如何扩展SecurityExpressionRoot以包括内置表达式的。

5.2. Custom Expression Handler

5.2.自定义表达式处理程序

Next, we need to inject our CustomMethodSecurityExpressionRoot in our expression handler:

接下来,我们需要在表达式处理程序中注入我们的CustomMethodSecurityExpressionRoot

public class CustomMethodSecurityExpressionHandler 
  extends DefaultMethodSecurityExpressionHandler {
    private AuthenticationTrustResolver trustResolver = 
      new AuthenticationTrustResolverImpl();

    @Override
    protected MethodSecurityExpressionOperations createSecurityExpressionRoot(
      Authentication authentication, MethodInvocation invocation) {
        CustomMethodSecurityExpressionRoot root = 
          new CustomMethodSecurityExpressionRoot(authentication);
        root.setPermissionEvaluator(getPermissionEvaluator());
        root.setTrustResolver(this.trustResolver);
        root.setRoleHierarchy(getRoleHierarchy());
        return root;
    }
}

5.3. Method Security Configuration

5.3.方法安全配置

Now, we need to use our CustomMethodSecurityExpressionHandler in the method security configuration:

现在,我们需要在方法安全配置中使用我们的CustomMethodSecurityExpressionHandler

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
        CustomMethodSecurityExpressionHandler expressionHandler = 
          new CustomMethodSecurityExpressionHandler();
        expressionHandler.setPermissionEvaluator(new CustomPermissionEvaluator());
        return expressionHandler;
    }
}

5.4. Using the New Expression

5.4.使用新表达式

Here is a simple example to secure our controller method using isMember():

下面是一个简单的例子,使用isMember()来保护我们的控制器方法。

@PreAuthorize("isMember(#id)")
@GetMapping("/organizations/{id}")
@ResponseBody
public Organization findOrgById(@PathVariable long id) {
    return organizationRepository.findOne(id);
}

5.5. Live Test

5.5.现场测试

Finally, here is a simple live test for user “john“:

最后,这里是对用户”john“的一个简单实时测试。

@Test
public void givenUserMemberInOrganization_whenGetOrganization_thenOK() {
    Response response = givenAuth("john", "123").get("http://localhost:8082/organizations/1");
    assertEquals(200, response.getStatusCode());
    assertTrue(response.asString().contains("id"));
}

@Test
public void givenUserMemberNotInOrganization_whenGetOrganization_thenForbidden() {
    Response response = givenAuth("john", "123").get("http://localhost:8082/organizations/2");
    assertEquals(403, response.getStatusCode());
}

6. Disable a Built-in Security Expression

6.禁用一个内置的安全表达式

Finally, let’s see how to override a built-in security expression – we’ll discuss disabling hasAuthority().

最后,让我们看看如何覆盖一个内置的安全表达式–我们将讨论禁用hasAuthority()

6.1. Custom Security Expression Root

6.1.自定义安全表达式根基

We’ll start similarly by writing our own SecurityExpressionRoot – mainly because the built-in methods are final and so we can’t override them:

我们将以类似的方式开始编写我们自己的SecurityExpressionRoot–主要是因为内置的方法是final,所以我们不能重写它们。

public class MySecurityExpressionRoot implements MethodSecurityExpressionOperations {
    public MySecurityExpressionRoot(Authentication authentication) {
        if (authentication == null) {
            throw new IllegalArgumentException("Authentication object cannot be null");
        }
        this.authentication = authentication;
    }

    @Override
    public final boolean hasAuthority(String authority) {
        throw new RuntimeException("method hasAuthority() not allowed");
    }
    ...
}

After defining this root note, we’ll have to inject it into the expression handler and then wire that handler into our configuration – just as we did above in Section 5.

在定义了这个根音符之后,我们必须将它注入表达式处理程序,然后将该处理程序接入我们的配置中–就像我们在第5节中做的那样。

6.2. Example – Using the Expression

6.2.示例–使用表达式

Now, if we want to use hasAuthority() to secure methods – as follows, it will throw RuntimeException when we try to access method:

现在,如果我们想使用hasAuthority()来保护方法–如下所示,当我们试图访问方法时,它将抛出RuntimeException

@PreAuthorize("hasAuthority('FOO_READ_PRIVILEGE')")
@GetMapping("/foos")
@ResponseBody
public Foo findFooByName(@RequestParam String name) {
    return new Foo(name);
}

6.3. Live Test

6.3.现场测试

Finally, here is our simple test:

最后,这里是我们的简单测试。

@Test
public void givenDisabledSecurityExpression_whenGetFooByName_thenError() {
    Response response = givenAuth("john", "123").get("http://localhost:8082/foos?name=sample");
    assertEquals(500, response.getStatusCode());
    assertTrue(response.asString().contains("method hasAuthority() not allowed"));
}

7. Conclusion

7.结论

In this guide, we did a deep-dive into the various ways we can implement a custom security expression in Spring Security, if the existing ones aren’t enough.

在本指南中,我们深入探讨了在Spring Security中实现自定义安全表达式的各种方式,如果现有的表达式还不够的话。

And, as always, the full source code can be found over on GitHub.

而且,像往常一样,完整的源代码可以在GitHub上找到over。