1. 概述
在这个教程中,我们将专注于使用Spring Security创建自定义安全表达式。
有时,框架提供的表达式可能不够灵活。在这种情况下,我们可以相对轻松地构建一个比现有表达式语义更丰富的新表达式。
首先,我们将讨论如何创建一个自定义的PermissionEvaluator
,然后是完全自定义的表达式,最后是覆盖内置的安全表达式。
2. 用户实体
首先,让我们为创建新的安全表达式打下基础。
看一下我们的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
}
这是我们的简单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
}
以及我们的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
}
最后,我们使用一个简单的自定义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;
}
...
}
有了这些类,我们将使用自定义的Principal
在基本的UserDetailsService
实现中:
@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);
}
}
如您所见,这些关系很简单:用户有一个或多个权限,每个用户属于一个组织。
3. 数据设置
接下来,让我们用一些简单的测试数据初始化数据库:
@Component
public class SetupData {
@Autowired
private UserRepository userRepository;
@Autowired
private PrivilegeRepository privilegeRepository;
@Autowired
private OrganizationRepository organizationRepository;
@PostConstruct
public void init() {
initPrivileges();
initOrganizations();
initUsers();
}
}
这是我们的初始化方法:
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);
}
注意:
- 用户“john”只有
FOO_READ_PRIVILEGE
。 - 用户“tom”有
FOO_READ_PRIVILEGE
和FOO_WRITE_PRIVILEGE
。
4. 自定义权限评估器
现在,我们准备好开始实现新的表达式——通过一个新的自定义权限评估器。
我们将使用用户的权限来保护我们的方法,但不使用硬编码的权限名称,而是寻求更开放、灵活的实现。
让我们开始吧。
4.1. PermissionEvaluator
为了创建自己的自定义权限评估器,我们需要实现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());
}
}
这是我们的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;
}
现在我们有了一个新的可用的安全表达式:hasPermission
。
因此,我们可以使用:
@PostAuthorize("hasPermission(returnObject, 'read')")
或者
@PreAuthorize("hasPermission(#id, 'Foo', 'read')")
注意:#id
指的是方法参数,'Foo'
指的是目标对象类型。
4.2. 方法安全配置
仅仅定义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. 实践中的示例
现在,让我们开始在几个简单的控制器方法中使用新的表达式:
@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;
}
}
好了,我们已经准备就绪,并在实践中使用了新的表达式。
4.4. 实例测试
现在,我们编写一个简单的现场测试,访问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"));
}
这是我们的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. 完全自定义的安全表达式
使用前面的方法,我们能够定义并使用hasPermission
表达式,这很有用。
然而,我们仍然受限于表达式的名称和语义。
因此,在这一部分,我们将全面定制——我们将实现一个名为isMember()
的安全表达式,检查主体是否属于某个组织。
5.1. 自定义方法安全表达式
为了创建这个新的自定义表达式,我们需要首先实现所有安全表达式评估的根节点:
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();
}
...
}
在这里,我们为新的操作提供了支持;isMember()
用于检查当前用户是否属于给定的Organization
。
请注意,我们扩展了SecurityExpressionRoot
以包括内置表达式。
5.2. 自定义表达式处理器
接下来,我们需要将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. 方法安全配置
现在,我们需要在方法安全配置中使用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. 使用新表达式
这是一个简单的示例,用于使用isMember()
保护控制器方法:
@PreAuthorize("isMember(#id)")
@GetMapping("/organizations/{id}")
@ResponseBody
public Organization findOrgById(@PathVariable long id) {
return organizationRepository.findOne(id);
}
5.5. 实例测试
最后,这是用户“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. 禁用内置安全表达式
最后,我们将看看如何覆盖内置的安全表达式——这里我们将讨论禁用hasAuthority()
。
6.1. 自定义安全表达式根
我们将从编写自己的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");
}
...
}
定义了这个根节点后,我们需要将其注入到表达式处理器中,并像第5节那样将处理器连接到配置。
6.2. 示例 - 使用表达式
现在,如果我们想使用hasAuthority()
来保护方法,如下所示,当我们尝试访问方法时,它会抛出RuntimeException
:
@PreAuthorize("hasAuthority('FOO_READ_PRIVILEGE')")
@GetMapping("/foos")
@ResponseBody
public Foo findFooByName(@RequestParam String name) {
return new Foo(name);
}
6.3. 实例测试
最后,这是我们简单的测试:
@Test
public void givenDisabledSecurityExpression_whenGetFooByName_thenError() {
Response response = givenAuth("john", "123").get("http://localhost:8082/foos?name=sample");
assertEquals(500, response.getStatusCode());
}
7. 总结
在这篇指南中,我们深入探讨了在Spring Security中实现自定义安全表达式的各种方式,如果现有的表达式不够用。
如往常一样,完整的源代码可以在GitHub上找到。