1. 概述
在本教程中,我們將重點介紹使用 Spring Security 創建自定義安全表達式。
有時,框架中提供的表達式可能不夠表達力。在這種情況下,構建一個比現有表達式語義更豐富的自定義表達式相對簡單。
我們首先將討論如何創建自定義PermissionEvaluator,然後創建一個完全自定義的表達式——最後將討論如何覆蓋內置的安全表達式。
2. 用户實體
首先,讓我們為創建新的安全表達式做好準備。
讓我們看一下我們的 User實體——它具有 權限和組織:
@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();
}
}
以下是我們的 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);
}
注意:
- 用户 “john” 僅擁有 FOO_READ_PRIVILEGE
- 用户 “tom” 同時擁有 FOO_READ_PRIVILEGE 和 FOO_WRITE_PRIVILEGE
為了創建我們自己的自定義權限評估器,我們需要實現 接口:
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());
}
}
這是我們的 方法:
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;
}
.
而且,我們現在可以使用更硬編碼的版本:
@PostAuthorize("hasAuthority('FOO_READ_PRIVILEGE')")
我們可以使用:
@PostAuthorize("hasPermission(returnObject, 'read')")
或者
@PreAuthorize("hasPermission(#id, 'Foo', 'read')")
注意: 指的是方法參數, 指的是目標對象類型。
還不夠——我們需要在方法安全配置中使用它:
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
DefaultMethodSecurityExpressionHandler expressionHandler =
new DefaultMethodSecurityExpressionHandler();
expressionHandler.setPermissionEvaluator(new CustomPermissionEvaluator());
return expressionHandler;
}
}
@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;
}
}
而且,我們已經準備好,並且在實踐中使用新的表達式。
@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"));
}
而且,我們有 方法:
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
通過之前的解決方案,我們能夠定義和使用 hasPermission 表達式——這在很大程度上很有用。
然而,我們仍然受到表達式本身的名稱和語義的限制。
因此,在這一部分,我們將完全自定義——我們將實現一個名為 isMember() 的安全表達式,檢查當前用户是否是組織成員。
5.1. Custom Method Security Expression
為了創建這個新的自定義表達式,我們需要首先實現根節點,其中所有安全表達式的評估都從這裏開始:
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. Custom 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
現在,我們需要在方法安全配置中使用我們的 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
這是一個簡單的示例,用於使用 isMember() 來安全我們的控制器方法:
@PreAuthorize("isMember(#id)")
@GetMapping("/organizations/{id}")
@ResponseBody
public Organization findOrgById(@PathVariable long id) {
return organizationRepository.findOne(id);
}
5.5. Live Test
最後,這是一個用於用户“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 中實現自定義安全表達式的各種方法,如果現有的方法不足以滿足需求。
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
DefaultMethodSecurityExpressionHandler expressionHandler =
new DefaultMethodSecurityExpressionHandler();
expressionHandler.setPermissionEvaluator(new CustomPermissionEvaluator());
return expressionHandler;
}
}而且,我們已經準備好,並且在實踐中使用新的表達式。
@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"));
}
而且,我們有 方法:
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
通過之前的解決方案,我們能夠定義和使用 hasPermission 表達式——這在很大程度上很有用。
然而,我們仍然受到表達式本身的名稱和語義的限制。
因此,在這一部分,我們將完全自定義——我們將實現一個名為 isMember() 的安全表達式,檢查當前用户是否是組織成員。
5.1. Custom Method Security Expression
為了創建這個新的自定義表達式,我們需要首先實現根節點,其中所有安全表達式的評估都從這裏開始:
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. Custom 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
現在,我們需要在方法安全配置中使用我們的 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
這是一個簡單的示例,用於使用 isMember() 來安全我們的控制器方法:
@PreAuthorize("isMember(#id)")
@GetMapping("/organizations/{id}")
@ResponseBody
public Organization findOrgById(@PathVariable long id) {
return organizationRepository.findOne(id);
}
5.5. Live Test
最後,這是一個用於用户“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 中實現自定義安全表達式的各種方法,如果現有的方法不足以滿足需求。
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);
}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();
}
...
}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;
}
}@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
CustomMethodSecurityExpressionHandler expressionHandler =
new CustomMethodSecurityExpressionHandler();
expressionHandler.setPermissionEvaluator(new CustomPermissionEvaluator());
return expressionHandler;
}
}@PreAuthorize("isMember(#id)")
@GetMapping("/organizations/{id}")
@ResponseBody
public Organization findOrgById(@PathVariable long id) {
return organizationRepository.findOne(id);
}@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());
}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");
}
...
}@PreAuthorize("hasAuthority('FOO_READ_PRIVILEGE')")
@GetMapping("/foos")
@ResponseBody
public Foo findFooByName(@RequestParam String name) {
return new Foo(name);
}@Test
public void givenDisabledSecurityExpression_whenGetFooByName_thenError() {
Response response = givenAuth("john", "123").get("http://localhost:8082/foos?name=sample");
assertEquals(500, response.getStatusCode());
}