3年前 (2022-04-29)  微服务 |   抢沙发  831 
文章评分 0 次,平均分 0.0

Spring Security添加到现有应用程序中可能是一个令人望而生畏的前景。仅仅向项目中添加所需的依赖项就会引发一系列事件,这些事件可能会破坏应用程序和测试。

也许你会突然看到一个登录提示,要求在启动时输入生成的密码。

也许你的测试现在得到了可怕的401未经授权,或随后的403被禁止。

在尝试使用身份验证时,可能会遇到ClassCastException#getPrincipal()

不管怎样,这篇文章都是来帮助你的!

我们将向您介绍如何向现有应用程序添加Spring Security,解释首次添加依赖项时会发生什么,接下来要做什么,以及如何修复测试。

我们最初的应用

为了展示如何添加Spring Security,我们开发了一个小应用程序,并对其功能进行了一些测试。想象一下,人力资源部门有一个小应用程序来跟踪和批准/拒绝休假请求。最初,该应用程序仅在人力资源部门内部使用,该部门通过电话或电子邮件接收请求。

现在我们想向所有员工开放这个应用程序,这样他们就可以自己申请休假了。人力资源部的任何人都可以批准或拒绝休假请求。很简单!

要添加哪个依赖项?

向应用程序添加安全性的第一步是选择要添加到项目中的正确依赖项。然而,如今,即使想清楚要添加哪个依赖项也很困难!看看开头。春天io我们可以看到,已经有5种不同的依赖关系与Spring安全性和/或OAuth2有关。部分原因是对OAuth2支持进行了重组,OAuth2资源服务器和客户机支持现在转移到了Spring Security 5.2+。

简而言之,我们现在建议不要使用Spring Cloud Starter依赖项,并推动对OAuth2使用Spring Security支持。

如果您的服务将充当OAuth2资源服务器,通过接受从网关传入的JSON Web令牌,您可以使用spring-boot-starter-OAuth2-resource-server。我们希望这是微服务领域中最常见的形式,在微服务领域中,中央网关承担OAuth2客户端角色。这也是我们将在这篇博文中使用的内容,尽管下面的许多细节也适用于其他形式。

如果您的服务将充当OAuth2客户端来获取JSON Web令牌,那么您很可能希望使用spring-boot-starter-OAuth2-client

这两个初学者都将为您提供最常见的安全方面可能需要的任何可传递依赖项。

添加依赖项时会发生什么?

我们在初始应用程序中添加了对spring-boot-starter-oauth2-resource-server的依赖。然后我们通过运行LeaveRequestApplication启动应用程序,并将浏览器指向http://localhost:8080/view/all.现在,在添加依赖项之前,该端点将显示所有员工的所有休假请求。添加依赖项后,该端点现在会弹出一个登录对话框,我们当然从未配置过它!

如何在项目中使用Spring Security

如果我们试图从命令行打开同一个端点,就会立即得到HTTP/1.1 401响应。

我们通过应用程序日志来了解应用程序中发生了什么。事实证明,UserDetailsServiceAutoConfiguration中有一条奇怪的新记录线:

INFO --- [main] .s.s.UserDetailsServiceAutoConfiguration :
  Using generated security password: fcf786f4-797b-499b-9abc-2cee4037edb3

当没有提供其他安全配置时,会触发此自动配置。它使用默认用户和生成的密码来设置我们的应用程序,作为某种回退。毕竟,如果您要将Spring Security添加到类路径中,您将需要某种形式的安全性。至少日志行和对话框起到了提醒作用,可以准确地配置应用程序中需要的内容。

OAuth2资源服务器配置

由于我们希望将应用程序配置为OAuth2资源服务器,因此我们可以提供所需的配置,使生成的安全密码消失。如文件所示,配置采用以下形式:

application.yml

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://localhost:8090/auth/realms/spring-cloud-gateway-realm

我们的应用程序现在将在启动期间调用已配置的issuer-uri,以配置JwtDecoder。在开发过程中,我们将使用KeyClopeWireMock为配置的issuer-uri端点提供服务。

一旦配置到位,我们就可以重新启动应用程序。查看日志,我们不再看到生成的密码;太棒了以前的UserDetailsServiceAutoConfiguration已被OAuth2ResourceServerJwtConfiguration取代,它为我们提供了一个JwtDecoder来处理传入令牌。除非您提供自己的WebSecurity配置适配器,否则这会将应用程序设置为每个请求都需要一个JSON Web令牌。

现在,当我们调用任何应用程序端点时,我们都会得到401个未经授权的响应,因为我们的请求缺少带有承载JSON Web令牌的授权头。为了解决这个问题,我们必须在请求的同时传递一个承载令牌。下面是一个令牌和HTTPie命令,用于通过HTTP/1.1 200完成您的请求:

# Save the token in an environment variable
$ export token=eyJhbGc...
# Create a leave request for a specific user and time window
$ http POST ':8080/request/tim?from=2020-03-21&to=2020-04-11' "Authorization: Bearer ${token}"
# Retrieve all leave requests
$ http :8080/view/all "Authorization: Bearer ${token}"

完美的现在,我们已经具备了将身份验证传递给应用程序的基础。

修正我们的测试;第一部分

现在,我们有了一个应用程序,它在启动时需要一个OpenID Connect提供程序,任何请求都需要一个有效的JWT。两者都不能很好地配合我们的测试,所以我们必须修复我们的每个不同测试风格。

我们添加了四种不同类型的测试,每种测试都使用完整或部分Spring应用程序上下文,以模拟您可能在现有应用程序中找到的内容。下面使用引导测试应用程序上下文的注释和参数来识别它们。我们将检查每个测试以及使其再次通过所需的更改。

@WebMvcTest with controller

首先,我们希望再次通过LeaveRequestControllerWebMVC测试;我们在这里单独测试控制器,使用@WebMvcMockMvc。运行我们看到的测试GET请求现在得到401个未授权响应,而POST请求得到403个禁止响应。

  • 401对GET请求的未授权响应是因为我们尚未传递授权:Bearer eyJ…​ 在我们的测试中。
  • 403禁止回应需要更多的投入;调试日志为我们指明了正确的方向:
DEBUG --- [main] o.s.security.web.csrf.CsrfFilter :
  Invalid CSRF token found for http://localhost/approve/4570ec01-9640-4873-a75b-59a8b4983d9e

默认情况下,Spring Security为POST请求添加跨站点请求伪造保护。这可以保护我们的资源服务器免受恶意请求;我们现在还没有选择禁用它。因此,我们必须将csrf令牌添加到POST请求中,这通常是通过Spring Security测试中的CsrfRequestPostProcessor提供的。

我们希望对我们的web请求进行身份验证,这可以通过将Spring Security测试库添加到我们的测试类路径来实现。该库为我们提供了SecurityMockMvcRequestPostProcessors#jwt(),以及其他功能,我们将其添加到测试方法中,让它们在每个请求中传递有效的jwt。

mockmvc.perform(get("/view/id/{id}", UUID.randomUUID())
  .with(jwt().jwt(builder -> builder.subject("Alice"))))
  .andExpect(status().isOk())

巧合的是,这也解决了POST请求的CSRF问题。如果现在重新运行测试,您将再次找到LeaveRequestControllerWebMvcTest pass中的所有测试!

@SpringBootTest with WebEnvironment.MOCK

接下来是我们的LeaveRequestControllersSpringBootWebEnvMockTest,它现在需要KeyClope来启动应用程序上下文。虽然这在本地是可以管理的,但对于CI/CD环境和一般测试稳定性来说,这还远远不够理想。因此,我们希望在不需要配置的颁发者uri可用的情况下通过此测试。我们可以通过提供@MockBean JwtDecoder来相当容易地实现这一点,它绕过了对我们的发行者uri的调用,正如文档中提到的那样。有了MockBean,我们可以看到测试应用程序上下文现在再次启动,即使OpenID连接提供程序不可用。

此时,我们看到了与之前相同的故障模式:GET请求得到401响应,wehereas POST请求得到403响应。我们添加了与之前应用于LeaveRequestControllerWebMvcTest相同的修复程序,以使所有测试再次通过。

@SpringBootTest with WebEnvironment.RANDOM_PORT

我们的LeaveRequestControllersSpringBootWebEnvRandomPortTest将as置于一个相当困难的位置;对于这种方法,没有明确的途径来提供简单的测试支持,我们不得不怀疑这是否是编写应用程序测试的最佳方式。但是,由于我们不会轻易放弃,仍然有办法让事情运转起来,即使这表明你可能不应该这样做;这和你之前看到的一样。接下来,我们将spring cloud contract wiremock添加到我们的测试类路径中,并将其连接起来,以便在测试期间为记录的KeyClope OpenID Connect映射提供服务。最后,我们添加了一个RestTemplateCustomizer,它将JWT附加到任何测试请求中。

这种方法对于OpenID Connect提供商可能不可行,可能无法扩展,因为您需要不同的角色和令牌,而且通常看起来是个坏主意。不过,除了进一步的测试应用程序上下文攻击之外,几乎没有其他替代方案,这些攻击会挫败您对令牌处理的测试。

角色和授权

我们的应用程序现在需要一个包含每个请求的JWT,并将其解码为身份验证。虽然这一切都是好的,也是需要的;它在安全方面还没有取得多大成就;任何人都可以验证、提交和查看休假请求,并在他们认为合适的情况下批准或拒绝休假请求。我们需要用一些常识性的角色和限制来配置我们的应用程序。例如:

  • 我们希望用户只提交和查看自己的请求;
  • 只有HR员工可以批准或拒绝请求,并查看所有请求。

配置方法安全性

由于所有请求都会通过我们的LeaveRequestService,因此这里似乎是添加安全限制的最佳场所。我们将添加各种安全注释和表达式,所有这些都在Spring security的授权章节中有详细的说明。安全注释需要通过@EnableGlobalMethodSecurity(preprestenabled=true,jsr250Enabled=true)启用,因为没有这个注释,它们无法执行任何操作!

@PreAuthorize("#employee == authentication.name or hasRole('HR')")
public List<LeaveRequest> retrieveFor(String employee) {
  return repo.findByEmployee(employee);
}

@RolesAllowed("HR")
public List<LeaveRequest> retrieveAll() {
  return repo.findAll();
}

修正我们的测试;第二部分

现在我们已经添加了安全限制,我们必须进行另一轮测试修复,以匹配我们的安全限制。我们的测试用户名现在应该与提交或查看的休假请求的员工姓名匹配。要批准或拒绝请假请求,活动用户应具有“HR”角色。

@SpringBootTest with WebEnvironment.NONE

我们的LeaveRequestServiceTest方法现在还需要一个活动用户,这在以前是不需要的。我们使用的@WithMockUser与前面看到的相同,添加了参数以匹配所需的用户名或所需的“HR”角色。

@Test
@WithMockUser(username = "Alice")
void testRequest() {
  LeaveRequest leaveRequest = service.request("Alice", of(2019, 11, 30), of(2019, 12, 03));
  verify(repository).save(leaveRequest);
}

@Test
@WithMockUser(roles = "HR")
void testApprove() {
  LeaveRequest saved = repository.save(new LeaveRequest("Alice", of(2019, 11, 30), of(2019, 12, 03), PENDING));
  Optional<LeaveRequest> approved = service.approve(saved.getId());
  [...]
}

@SpringBootTest with WebEnvironment.MOCK

LeaveRequestServiceTest类似,我们还需要在这里定义与@WebMVCTTest相同的模拟用户属性。

@SpringBootTest with WebEnvironment.RANDOM_PORT

事实证明,更麻烦的是,我们必须对LeaveRequestControllersSpringBootWebEnvRandomPortTest进行一些更实质性的更改。我们需要为具有HR角色的用户提供第二个令牌,以便能够测试批准/拒绝流。因此,我们再次操纵KeyClope,为我们提供另一个具有指定角色的令牌,并通过相关测试。我们不能再使用以前的RestTemplateCustomizer,而是必须在每个测试中使用TestRestTemplate#exchange()。随着测试的更新和重新运行,我们发现令牌处理不完整!

默认情况下,我们的令牌被解码为具有UUID用户名,并且KeyClope领域访问角色还没有映射。其他测试方法都没有出现这种遗漏,所以这些测试毕竟有一定的价值。

为了提供缺少的功能,我们添加了usernamesubclaimeradapterkeydepeartrealmrolonverter,并将其连接到SecurityConfig中。每个OpenID连接提供程序的这些详细信息以及任何配置的角色都会有所不同。

结论

我们现在在应用程序中添加了Spring Security,允许任何用户进行身份验证,同时只授予一些用户特权。当然,这个应用程序可以扩展到包括针对漏洞攻击、审计和更多安全方面的进一步保护,但我们将这些留到下次使用。

源码地址:https://github.com/timtebeek/spring-security-samples/

 

除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/2628.html

关于

发表评论

表情 格式

暂无评论

登录

忘记密码 ?

切换登录

注册