电子书 – Spring Cloud 指南 – NPI EA (分类=Spring Cloud)
announcement - icon

让我们开始使用 Spring Cloud 的微服务架构

>> 加入 Pro 并下载电子书

电子书 – Mockito – NPI EA (标签 = Mockito)
announcement - icon

模拟是单元测试的重要组成部分,Mockito 库使编写 清晰直观的单元测试 变得容易,用于您的 Java 代码。

通过我们的 Mockito 指南 开始模拟,并改进您的应用程序测试

下载电子书

电子书 – Java 并发 – NPI EA (分类=Java 并发)
announcement - icon

在应用程序中处理并发可能是一个棘手的过程,其中包含许多 潜在的陷阱。 扎实的掌握基本知识将有助于最大程度地减少这些问题。

通过我们的 Java 并发 指南开始了解多线程应用程序

>> 下载电子书

电子书 – 响应式 – NPI EA (分类=响应式)
announcement - icon

Spring 5 增加了对使用 Spring WebFlux 模块进行响应式编程的支持,此支持自那时起不断改进。 开始使用 Reactor 项目基础知识和 Spring Boot 中的响应式编程

>> 加入 Pro 并下载电子书

电子书 – Java Streams – NPI EA (分类=Java Streams)
announcement - icon

自从 Java 8 引入以来,Stream API 已成为 Java 开发的基础。 基本操作,例如迭代、过滤、映射元素序列,使用起来看似很简单。

但这些也可能被过度使用并陷入一些常见陷阱。

更好地了解 Stream 的工作方式 以及如何将其与其他语言功能结合使用,请查看我们关于 Java Streams 的指南

>> 加入 Pro 并下载电子书

电子书 – Jackson – NPI EA (分类=Jackson)
announcement - icon

用 Jackson 正确处理 JSON

下载电子书

电子书 – HTTP 客户端 – NPI EA (分类=Http 客户端)
announcement - icon

充分利用 Apache HTTP 客户端

下载电子书

电子书 – Maven – NPI EA (分类 = Maven)
announcement - icon

开始使用 Apache Maven

下载电子书

电子书 – 持久化 – NPI EA (分类=持久化)
announcement - icon

您在努力实现正确的持久化层 Spring 吗?

探索电子书

电子书 – RwS – NPI EA (分类=Spring MVC)
announcement - icon

使用 Spring 构建 REST API 吗?

下载电子书

课程 – LS – NPI EA (分类=Jackson)
announcement - icon

通过 Learn Spring 课程开始学习 Spring 和 Spring Boot

>> 学习 SPRING
课程 – RWSB – NPI EA (分类=REST)
announcement - icon

通过构建一个完整的 REST API,深入了解 Spring Boot 3 和 Spring 6,使用该框架

>> 全新的“REST With Spring Boot”

课程 – LSS – NPI EA (分类=Spring Security)
announcement - icon

是的,Spring Security 可能很复杂,从核心内的更高级功能到框架中深入的 OAuth 支持。

我将安全材料构建为 两个完整的课程 - 核心和 OAuth,以针对这些更复杂的场景进行实践。 我们探索何时以及如何使用每个功能,并 在后台项目中对其进行编码

您可以在这里探索该课程

>> 学习 Spring Security

课程 – LSD – NPI EA (标签=Spring Data JPA)
announcement - icon

Spring Data JPA 是处理 JPA 复杂性的绝佳方式,它具有 Spring Boot 的强大简洁性

通过引导式参考课程开始使用 Spring Data JPA

>> 查看课程

合作伙伴 – Moderne – NPI EA (类别=Spring Boot)
announcement - icon

使用 OpenRewrite 安全且自动地重构 Java 代码。

手动重构大型代码库既缓慢、有风险,又容易拖延。OpenRewrite 应运而生。这个用于大规模、自动化代码转换的开源框架可以帮助团队安全、一致地进行现代化改造。

每个月,OpenRewrite 的创建者和维护者 Moderne 都会举办现场、实践培训课程——一个面向初学者,一个面向经验丰富的用户。您将了解配方的运作方式、如何将其应用于项目,以及如何自信地进行代码现代化改造。

参加下一次课程,带来您的问题,并学习如何自动化通常会占用您 sprint 时间的工作。

合作伙伴 – LambdaTest – NPI EA (类别=测试)
announcement - icon

回归测试是发布流程中的重要步骤,以确保新代码不会破坏现有功能。随着代码库的不断发展,我们希望频繁运行这些测试,以便尽早发现任何问题。

确保这些测试以自动化的方式频繁运行的最佳方法当然是将其包含在 CI/CD 管道中。 这样,每次向仓库提交代码时,回归测试将自动执行。

在本教程中,我们将学习如何使用 Selenium 创建回归测试,然后使用 GitHub Actions 将它们包含在我们的管道中,在 LambdaTest 云网格上运行

>> 如何使用 GitHub Actions 运行 Selenium 回归测试

课程 – LJB – NPI EA (类别 = Core Java)
announcement - icon

通过编码方式构建 Java 的坚实、实用的基础

>> 学习 Java 基础

课程 – LSS – NPI (类别=Spring Security)
announcement - icon

如果您正在使用 Spring Security (特别是 OAuth) 进行实现,请务必查看《学习 Spring Security》课程

>> 学习 SPRING SECURITY

1. 概述

简单来说,Spring Security 支持方法级别的授权语义。

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

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

更多阅读

Spring 表达式语言指南

本文探讨了 Spring 表达式语言 (SpEL),一种强大的表达式语言,支持在运行时查询和操作对象图。

使用 Spring Security 的自定义安全表达式

一份关于使用 Spring Security 创建新的自定义安全表达式,然后使用 Pre 和 Post 授权注解的指南。

2. 启用方法安全

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

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

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

如果我们要使用 Spring Boot,可以使用 spring-boot-starter-security 依赖,它包括 spring-security-config

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

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

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

@Configuration
@EnableGlobalMethodSecurity(
  prePostEnabled = true, 
  securedEnabled = true, 
  jsr250Enabled = true)
public class MethodSecurityConfig 
  extends GlobalMethodSecurityConfiguration {
}
  • prePostEnabled 属性启用 Spring Security pre/post 注解。
  • securedEnabled 属性确定是否应启用 @Secured 注解。
  • jsr250Enabled 属性允许我们使用 @RoleAllowed 注解。

我们将在下一节中进一步了解这些注解。

3. 应用方法安全

3.1. 使用 @Secured 注解

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

让我们定义一个 getUsername 方法

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

这里,@Secured(“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 表达式语言 (SpEL)。

3.2. 使用 @RolesAllowed 注解

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

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

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

@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 表达式语言) 编写谓词。

@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) {
    //...
}

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

@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);
}

这里,loadUserDetail 方法只有当返回的 CustomUserusername 等于当前身份验证主体的 nickname 时才能成功执行。

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

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 将迭代返回的列表并删除与主体用户名匹配的任何值。

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

3.5. 方法安全元注解

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

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

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

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

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

安全元注解是一个好主意,因为它们增加了更多的语义,并将我们的业务逻辑从安全框架中解耦。

3.6. 类级别的安全注解

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

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

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

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

3.7. 方法上的多个安全注解

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

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

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

4. 重要注意事项

关于方法安全,我们想回顾两点

  • 默认情况下,Spring AOP 代理用于应用方法安全。 如果一个受保护的方法 A 被同一类中的另一个方法调用,则 A 中的安全性将完全被忽略。 这意味着方法 A 将在没有任何安全检查的情况下执行。 同样适用于私有方法。
  • Spring SecurityContext 是线程绑定的。 默认情况下,安全上下文不会传播到子线程。有关更多信息,请参阅我们的 Spring Security 上下文传播 文章。

5. 测试方法安全

5.1. 配置

要使用 JUnit 测试 Spring Security,我们需要 spring-security-test 依赖项:

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

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

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

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

5.2. 测试用户名和角色

现在我们的配置已经准备好,让我们尝试测试我们使用 @Secured(“ROLE_VIEWER”) 注解保护的 getUsername 方法

@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。如果未指定 usernamerole,则默认 usernameuser,默认 roleROLE_USER

请注意,这里不需要添加 ROLE_ 前缀,因为 Spring Security 会自动添加该前缀。

如果我们不想使用该前缀,可以考虑使用 authority 代替 role

例如,让我们声明一个 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 MockUserAtClassLevelIntegrationTest {
    //...
}

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

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

在上面的示例中,我们预计会收到 AccessDeniedException ,因为匿名用户未被授予 ROLE_VIEWER 角色或 SYS_ADMIN 权限。

5.3. 使用自定义 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);
}

在这种情况下,该方法只有当返回的 CustomUserusername 等于当前身份验证主体的 nickname 时才会成功执行。

如果我们要测试该方法,我们可以提供一个 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 可能是真实实现或用于测试目的的模拟实现。

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

方便的是,我们也可以在类级别使用@WithUserDetails 注解,类似于我们对@WithMockUser 注解所做的那样

5.4. 使用元注解进行测试

我们经常发现自己在各种测试中重复使用相同的用户/角色。

对于这些情况,创建一个元注解会很方便。

再次查看之前的示例@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

6. 结论

在本文中,我们探讨了在 Spring Security 中使用方法安全性的各种选项。

我们还介绍了一些易于测试方法安全性的技术,并学习了如何在不同的测试中重用模拟用户。

支持本文的代码可在 GitHub 上获取。 一旦你Baeldung Pro 会员 身份登录,就开始学习并在项目上进行编码。
Baeldung Pro – NPI EA (类别 = Baeldung)
announcement - icon

Baeldung Pro 具有完全无广告以及最终具有深色模式,提供干净的学习体验

>> 探索干净的 Baeldung

一旦早期采用者的席位全部用完,价格将上涨并保持在每年 33 美元。

电子书 – HTTP 客户端 – NPI EA (类别=HTTP 客户端)
announcement - icon

Apache HTTP Client 是一个非常强大的库,适用于简单和高级用例,在测试 HTTP 端点时尤其适用。 查看我们的指南,涵盖基本请求和响应处理,以及安全性、Cookie、超时等。

>> 下载电子书

电子书 – Java 并发 – NPI EA (分类=Java 并发)
announcement - icon

在应用程序中处理并发可能是一个棘手的过程,其中包含许多 潜在的陷阱。 扎实的掌握基本知识将有助于最大程度地减少这些问题。

通过我们的 Java 并发 指南开始了解多线程应用程序

>> 下载电子书

电子书 – Java Streams – NPI EA (分类=Java Streams)
announcement - icon

自从 Java 8 引入以来,Stream API 已成为 Java 开发的基础。 基本操作,例如迭代、过滤、映射元素序列,使用起来看似很简单。

但这些也可能被过度使用并陷入一些常见陷阱。

更好地了解 Stream 的工作方式 以及如何将其与其他语言功能结合使用,请查看我们关于 Java Streams 的指南

>> 加入 Pro 并下载电子书

电子书 – 持久化 – NPI EA (分类=持久化)
announcement - icon

您在努力实现正确的持久化层 Spring 吗?

探索电子书

课程 – LS – NPI EA (类别=REST)

announcement - icon

从 Spring Boot 开始,通过 Learn Spring 课程了解核心 Spring。

>> 查看课程

合作伙伴 – Moderne – NPI EA (标签=重构)
announcement - icon

现代 Java 团队行动迅速——但代码库并不总是跟上。 框架会发生变化,依赖关系会漂移,技术债务会累积,直到它开始拖慢交付速度。 OpenRewrite 就是为此而构建的:一个开源重构引擎,可在保持开发人员意图不变的同时自动化重复的代码更改。

由 Moderne 的 OpenRewrite 创建者和维护者领导的每月培训系列,将介绍实际的迁移和现代化模式。 无论您是重构配方的新手,还是准备编写自己的配方,您都将学习以安全且可扩展的方式进行重构的实用方法。

如果您曾经希望重构感觉像编写代码一样自然——并且一样快速——这是一个很好的起点

课程 – LSS – NPI (类别=安全/Spring Security)
announcement - icon

我刚刚宣布了新的Learn Spring Security 课程,其中包含完全关注 Spring Security 中新的 OAuth2 堆栈的材料。

>> 查看课程

电子书 Jackson – NPI EA – 3 (类别 = Jackson)
© .