1. 简介
简而言之, Spring Security 支持方法级别的授权语义.
通常, 我们可以通过限制哪些角色能够执行特定方法来保护我们的服务层 - 并使用专用的方法级安全测试支持对其进行测试.
在本文中, 我们将首先回顾一些安全注释的使用. 然后, 我们将专注于使用不同的策略测试我们的方法安全性.
2. 启用方法级别的安全授权配置
首先, 要使用 Spring Method Security, 我们需要添加 spring-security-config 依赖项:
- <dependency>
- <groupId>org.springframework.security</groupId>
- <artifactId>spring-security-config</artifactId>
- </dependency>
如果我们想使用 Spring Boot, 我们可以使用包含 spring-security-config 的 spring-boot-starter-security 依赖项:
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-security</artifactId>
- </dependency>
接下来, 我们需要启用全局方法级别授权安全性:
- @Configuration
- @EnableGlobalMethodSecurity(
- prePostEnabled = true,
- securedEnabled = true,
- jsr250Enabled = true)
- public class MethodSecurityConfig
- extends GlobalMethodSecurityConfiguration {
- }
prePostEnabled 属性启用 Spring Security 前 / 后注释
securedEnabled 属性确定是否应启用 @Secured 注释
jsr250Enabled 属性允许我们使用 @RoleAllowed 注释
我们将在下一节中详细探讨这些注释.
3. 应用方法级别安全性
3.1. 使用 @Secured Annotation
@Secured 注释用于指定方法上的角色列表. 因此, 如果用户至少具有一个指定的角色, 则用户能访问该方法.
我们定义一个 getUsername 方法:
- @Secured("ROLE_VIEWER")
- public String getUsername() {
- SecurityContext securityContext = SecurityContextHolder.getContext();
- return securityContext.getAuthentication().getName();
- }
这里,@ Secure("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 角色之一时, 用户才能调用 isValidUsername2.
3.3. 使用 @PreAuthorize 和 @PostAuthorize 注释
@PreAuthorize 和 @PostAuthorize 注释都提供基于表达式的访问控制. 因此, 可以使用 SpEL(Spring Expression Language) 编写.
@PreAuthorize 注释在进入方法之前检查给定的表达式, 而 @PostAuthorize 注释在执行方法后验证它并且可能改变结果.
现在, 让我们声明一个 getUsernameInUpperCase 方法, 如下所示:
- @PreAuthorize("hasRole('ROLE_VIEWER')")
- public String getUsernameInUpperCase() {
- return getUsername().toUpperCase();
- }
@PreAuthorize("hasRole('ROLE_VIEWER')") 与我们在上一节中使用的 @Secured("ROLE_VIEWER") 具有相同的含义. 您可以在以前的文章中发现更多安全表达式详细信息.
因此, 注释 @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);
- }
在此示例中, 如果返回的 CustomUser 的用户名等于当前身份验证主体的昵称, 则 loadUserDetail 方法会成功执行.
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 将遍历返回的列表并删除与主体用户名匹配的任何值.
3.5.Method Security 元注释
我们发现经常有使用相同安全配置保护不同方法的情况.
在这种情况下, 我们可以定义一个 Security 元注释:
- @Target(ElementType.METHOD)
- @Retention(RetentionPolicy.RUNTIME)
- @PreAuthorize("hasRole('VIEWER')")
- public @interface IsViewer {
- }
接下来, 我们可以直接使用 @IsViewer 注释来保护我们的方法:
Security 元注释是一个好主意, 因为它们添加了更多语义并将我们的业务逻辑与安全框架分离.
3.6. 类级别 Security 注释
如果我们发现对一个类中的每个方法使用相同的 Security 注释, 我们可以考虑将该注释放在类级别:
- @Service
- @PreAuthorize("hasRole('ROLE_ADMIN')")
- public class SystemService {
- public String getSystemYear(){
- //...
- }
- public String getSystemDate(){
- //...
- }
- }
在上面的示例中, 安全规则 hasRole('ROLE_ADMIN') 将应用于 getSystemYear 和 getSystemDate 方法.
3.7. 方法上有的多重 Security 注释
我们还可以在一个方法上使用多个 Security 注释:
- @PreAuthorize("#username == authentication.principal.username")
- @PostAuthorize("returnObject.username == authentication.principal.nickName")
- public CustomUser securedLoadUserDetail(String username) {
- return userRoleRepository.loadUserByUserName(username);
- }
因此, Spring 将在执行 securedLoadUserDetail 方法之前和之后验证授权.
4. 重要考虑因素
我们想提醒两点方法 Security:
默认情况下, Spring AOP 代理用于应用方法安全性 - 如果安全方法 A 由同一类中的另一个方法调用, 则 A 中的安全性将被完全忽略. 这意味着方法 A 将在没有任何安全检查的情况下执行, 这同样适用于私有方法
Spring SecurityContext 是线程绑定的 - 默认情况下, 安全上下文不会传播到子线程
5. 测试方法 Security
5.1. 配置
要使用 JUnit 测试 Spring Security, 我们需要 spring-security-test 依赖项:
- <dependency>
- <groupId>org.springframework.security</groupId>
- <artifactId>spring-security-test</artifactId>
- </dependency>
我们不需要指定依赖版本, 因为我们使用的是 Spring Boot 插件.
接下来, 让我们通过指定 runner 和 ApplicationContext 配置来配置一个简单的 Spring Integration 测试:
- @RunWith(SpringRunner.class)
- @ContextConfiguration
- public class TestMethodSecurity {
- // ...
- }
5.2. 测试用户名和角色
现在我们的配置准备好了, 让我们尝试测试我的 getUsername 方法, 该方法由注释 @Secured("ROLE_VIEWER") 保护:
- @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. 如果我们不指定用户名或角色, 则默认用户名为 user, 默认角色为 ROLE_USER.
请注意, 此处不必添加 ROLE_前缀, Spring Security 将自动添加该前缀.
如果我们不想拥有该前缀, 我们可以考虑使用权限而不是角色.
例如, 让我们声明一个 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();
- }
在上面的示例中, 我们期望 AccessDeniedException, 因为匿名用户未被授予角色 ROLE_VIEWER 或权限 SYS_ADMIN.
5.3. 使用 Custom UserDetailsService 进行测试
对于大多数应用程序, 通常使用自定义类作为身份验证主体. 在这种情况下, 自定义类需要实现 org.springframework.security.core.userdetails.UserDetails 接口.
在本文中, 我们声明了一个 CustomUser 类, 它扩展了 UserDetails 的现有实现, 即 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 的用户名等于当前身份验证主体的昵称时, 该方法才会成功执行.
如果我们想测试该方法, 我们可以提供 UserDetailsService 的实现, 它可以根据用户名加载我们的 CustomUser:
- @Test
- @WithUserDetails(
- value = "john",
- userDetailsServiceBeanName = "userDetailService")
- public void whenJohn_callLoadUserDetail_thenOK() {
- CustomUser user = userService.loadUserDetail("jane");
- assertEquals("jane", user.getNickName());
- }
这里,@ WithUserDetails 注释声明我们将使用 UserDetailsService 来初始化我们经过身份验证的用户. 该服务由 userDetailsServiceBeanName 属性引用. 这个 UserDetailsService 可能是一个真正的实现, 或者用于测试目的.
此外, 该服务将使用属性值的值作为加载 UserDetails 的用户名.
方便的是, 我们也可以在类级别使用 @WithUserDetails 注释进行修饰, 类似于我们对 @WithMockUser 注释所做的操作.
5.4. 使用 Meta 注释进行测试
我们经常发现自己在各种测试中一遍又一遍地重复使用相同的用户 / 角色.
对于这些情况, 创建元注释很方便.
修改前面的示例 @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 创建特定于域的用户.
六, 结论
在本教程中, 我们探讨了在 Spring Security 中使用 Method Security 的各种选项.
我们还经历了一些技术来轻松测试方法安全性, 并学习如何在不同的测试中重用模拟用户.
可以在 GitHub 上找到本教程的所有示例.
来源: https://www.cnblogs.com/xjknight/p/10945825.html