vlambda博客
学习文章列表

读书笔记《building-a-restful-web-service-with-spring》应对安全问题

第 7 章。处理安全性

安全性跨越 IT 系统的每一个边界;从物理访问数据中心和服务器机架,到加密通信,一直到验证 Web 服务端点的输入。在本章中,我们将重点关注直接影响 Web 服务的安全措施。我们将涵盖以下主题:

  • 我们的示例 RESTful Web 服务的预订组件用于说明如何使用 Spring 解决安全问题

  • 认证技术

  • 授权技术

  • 输入验证

  • 加密的使用

预订服务


在深入研究如何使用 Spring 处理安全性之前,让我们首先讨论我们将在本章中使用的示例物业管理系统的组件:预订服务。

顾名思义,这个 组件将提供在我们的示例物业管理系统中接受和管理预订的必要功能。让我们考虑以下 Java 接口:

public interface BookingService {
  /**
  * Looks up the booking with the given identifier.
  *
  * @param bookingId the booking identifier to look up
  * @return the booking with the given ID
  */
  public Booking getBooking(long bookingId);

  /**
  * Answers all bookings for the given date range.
  *
  * @param dateRange the date range to retrieve bookings for
  * @return the bookings in the given date range
  */
  public List<Booking> getBookings(DateRange dateRange);

  /**
  * Processes the given booking
  *
  * @param request the booking request
  * @return the result of the request
  */
  public BookingResponse book(BookingRequest request);
}

这种抽象允许 我们进行和检索预订。假设此接口的实现可用,我们现在可以将注意力转向构建必要的 RESTful 端点以公开此功能。

REST 资源

正如我们在 第 2 章中看到的,使用 Maven 和 Gradle 构建 RESTful Web 服务,我们可以公开检索的能力带有以下代码的标识符预订:

@RestController
@RequestMapping("/bookings")
public class BookingsResource {

  @Autowired
  private BookingService bookingService;

  @RequestMapping(value = "/{bookingId}", method = RequestMethod.GET)
  public BookingDTO getBooking(@PathVariable("bookingId") long bookingId) {
    return new BookingDTO(bookingService.getBooking(bookingId));
  }
}

在以下部分中,我们将了解如何将安全性应用于此端点。

验证


身份验证涉及确保用户是他们所说的那样。有多种方法可以 对用户进行身份验证。本节将描述 HTTP 提供的一些机制。

HTTP 基本身份验证

这是 HTTP 规范中最简单的身份验证形式。它依赖于将用户名和密码组合作为 Authorization 标头传递给任何要求 身份验证的 HTTP 请求.

当客户端向需要身份验证的端点发出请求时,服务器将以 HTTP 401 Not Authorized 响应进行响应。响应将包含以下标头:

WWW-Authenticate: Basic realm="myRealm"

此标头指示客户端必须使用基本方案对用户进行身份验证。现代浏览器会在收到这样的响应时自动提示用户提供他们的凭据,并使用 Authorization 标头重新发出请求。此标头应包含在< 中编码的用户名和密码组合(格式为username:password)的方案/a> Base64。例如,让我们考虑一个包含以下标头的请求:

Authorization: Basic cmVzdDpyb2Nrcw==

解码后,服务器将需要使用用户名 rest 和密码 rocks 检查用户。

小费

虽然这个方案很容易实施,但没有提供保密措施来保护凭证。实际上,凭证只是经过编码而不是加密,并且可以很容易地访问。因此,此方案必须与 HTTPS 一起使用才能被认为是安全的。

使用 Spring 的基本身份验证

让我们看看我们如何 设置一个 Spring RESTful Web 服务来支持基本身份验证。首先,我们需要为我们的项目添加一些新的依赖项:

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

</dependency>
<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-web</artifactId>
  <version>4.0.1.RELEASE</version>
</dependency>

这将导入 Spring 的 web 安全模块,以及它的配置支持。下一步是 配置安全性。我们可以在 Java 中通过声明以下类来做到这一点:

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

  @Autowired
  public void configureGlobal(AuthenticationManagerBuilder auth)
    throws Exception {
    auth.inMemoryAuthentication()
    .withUser("rest").password("rocks").roles("USER");
  }
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
 .anyRequest().authenticated()
 .and().httpBasic();
  }
}

这是一个非常简单的配置,不是生产就绪的,但足以简单地说明 Spring Security 的配置。我们使用用户名 rest 和密码 rocks 定义一个用户。我们还指示 Spring 使用 HTTP Basic 身份验证方案对所有请求进行身份验证。

小费

在现实世界的系统中,用户不会被存储在内存中(如本例中的 auth.inMemoryAuthentication() 所示),而是在密码加密的数据库中 使用例如 Bcrypt 加密(https://en.wikipedia.org/wiki/Bcrypt)。

剩下的最后一步是让我们的 Web 服务使用这个安全配置。我们可以通过扩展 org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer 来做到这一点,如下所示:

public class SecurityWebApplicationInitializer extends AbstractSecurityWebApplicationInitializer {

  public SecurityWebApplicationInitializer() {
    super(SecurityConfig.class);
  }
}

将我们的 security 配置类传递给超类将确保对我们示例 Web 服务的所有请求都是安全的。

HTTP 摘要式身份验证

与 HTTP Basic 身份验证方案类似,HTTP Digest 身份验证方案使用用户名和密码来对用户进行身份验证。但是,与之前的方案不同, 凭证不是以易于解码的格式通过网络发送的。取而代之的是用户名、密码和一些额外部分的MD5散列 的信息被发送。当服务器收到请求时,它会使用相同的算法生成另一个哈希值,并比较 这两个值。如果它们匹配,则用户输入了正确的密码。如果不是,则身份验证失败,将返回相应的状态码。

要在 Spring 中设置 Digest 身份验证,必须配置 org.springframework.security.web.authentication.www.DigestAuthenticationFilter 过滤器.此过滤器将发出身份验证标头,例如:

WWW-Authenticate: Digest realm="My Realm", qop="auth", 
nonce="MTQzNDUzMjIyNTE3MDplZjRmYzFmYzZkNDZkNDE4NzE2ZmRkNzAzMmM2YmM0ZQ=="

过滤器还将处理请求中的任何 Authorization 标头。例如:

Authorization: Digest username="rest", realm="My Realm", 
nonce="MTQzNDUzMjM0OTk2MzoyNjQwOTA0MDI0MTEzN2E2ZjIzOGMxZDU0ZTlkY2MxYQ==", uri="/bookings/3", response="1bc4974dd8ca156568149f3944cf42c8", qop=auth, nc=00000001, cnonce="6ae950660ea900bf"

在访问安全资源时,服务会在 401 响应中发送带有方案、领域和额外信息的 WWW-Authenticate 标头。浏览器将通过提示用户并使用包含 < 的 MD5 哈希的 Authorization 标头重新发出请求来处理此问题/a>用户的凭据。

要启用 Digest 身份验证,让我们修改我们的 SecurityConfig 类:

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
 .anyRequest().authenticated()
 .exceptionHandling()
 .authenticationEntryPoint(digestEntryPoint())
 .and()
 .addFilter(digestAuthenticationFilter());
  }
  // rest of the code omitted for clarity
}

通过这个更新的实现,我们将 基本身份验证替换为更安全的Digest 身份验证方案。

笔记

虽然 HTTP Digest 身份验证提供了一种通过其他 非安全网络发送加密凭据的机制,但阅读器应该知道 MD5 算法有已知的限制,应该仔细考虑。更多信息可以在 https://en.wikipedia.org/wiki/Digest_access_authentication< /a>。

基于令牌的身份验证

我们将简要讨论的最后一个方案是基于令牌的身份验证。这种方法不是 HTTP 规范的一部分,而是一种常见的 身份验证方法。该方案依赖于服务器发布的加密令牌,然后在客户端的每个请求上发送。令牌可以使用现代、强大的加密算法进行加密。 Spring 使用 org.springframework.security.core.token.TokenServiceorg.springframework.security 为这种方案提供了很好的支持。 core.token.Token

比如服务开发者可以看看org.springframework.security.core.token.KeyBasedPersistenceTokenService有关此身份验证方法的示例使用。

其他认证方式

如果以前的身份验证方法不适合,Web 服务设计人员可能有兴趣研究 使用以下内容:

  • OAuth2:这是一种开源授权标准,可为客户端应用程序提供 代表 用户安全访问服务器资源。当用户希望通过第三方应用 登录服务时,这一点尤其受欢迎。查看 http://oauth.net/2/ 了解更多详情。

  • JWT:JSON Web Token 是一个用于检查请求中发送的信息是否可以使用数字签名进行验证和信任。更多关于JWT的信息可以在http找到://jwt.io

介绍了身份验证之后,我们现在可以专注于如何管理授权。

授权


身份验证的必然结果是授权。这两个概念通常一起处理,但它们指的是保护 Web 服务的两个不同要求。身份验证验证用户的身份,而授权管理用户有权 执行哪些操作。授权通常依赖于将用户与角色相关联并控制允许哪些用户角色执行特定操作。

使用 Spring 进行授权

有两种 方法来管理 Spring 授权:

  • 网址映射

  • 资源注释

以下部分提供了这两种方法的说明。

网址映射

扩展我们之前的 示例,我们可以修改 SecurityConfig 以声明细粒度 URL 映射,如下所示:

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
 .antMatchers(HttpMethod.GET, "/bookings/**").hasRole("ADMIN")
 .anyRequest().authenticated();
  }
}

在这个新版本中,我们指示 Spring 只允许管理员访问读取预订。所有其他端点都接受来自具有任何角色的经过身份验证的用户的请求。为了能够测试这个安全配置,让我们添加一个管理员用户。我们可以通过如下修改我们的类来做到这一点:

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
  @Autowired
  public void configureGlobal(AuthenticationManagerBuilder auth)
  throws Exception {
    auth.inMemoryAuthentication()
    .withUser("rest").password("rocks").roles("USER")
    .and()
 .withUser("admin").password("admin").roles("ADMIN");
  }
}

除了我们的 rest 用户,我们现在还声明了一个管理员用户,用户名 admin 和密码 管理员

使用这个新配置,如果我们以 USER 身份登录并尝试访问预订,服务器将生成 403 Forbidden 响应。在本地,您可以通过在 Web 浏览器中打开 http://localhost:8080/bookings/1 来测试此行为。

笔记

由于浏览器使用 HTTP Basic 或 HTTP Digest 身份验证方案缓存凭据,因此注销有点棘手,并且通常需要关闭浏览器.值得庆幸的是,现代浏览器允许隐私浏览。此功能可以帮助加快开发和测试 Web 服务的安全性。

这种方法有助于在全球范围内保护 Web 服务,但在需要细粒度授权时可能会很麻烦。实际上,配置(Java 或 XML)可能会变得难以 维护。下一节将描述一种更具可扩展性的方法。

资源注释

与上一节中讨论的集中式配置不同,使用注释允许直接在资源类中控制资源访问。 Spring 提供 使用org.springframework.security.access.prepost.PreAuthorize< /代码>。

让我们修改本章开头描述的端点:

@RestController
@RequestMapping("/bookings")
public class BookingsResource {

  @PreAuthorize("hasRole('ROLE_ADMIN')")
  @RequestMapping(value = "/{bookingId}", method = RequestMethod.GET)
    public BookingDTO getBooking(@PathVariable("bookingId") long bookingId) {
    // omitted
  }
}

@PreAuthorize("hasRole('ADMIN')") 添加到我们的端点声明中会指示 Spring仅向管理员授予对该资源的访问权限的安全性。如果用户尝试调用此端点,服务器将生成 403 Forbidden 响应。

小费

如果资源必须可供多个角色访问,则可以使用以下表达式:hasAnyRole('role1, role2')

现在让我们将注意力转向安全的另一个方面,它也起着重要的作用:输入验证。下一节将介绍如何使用 Spring 实现输入验证。

输入验证


除了身份验证和授权之外,构建安全 Web 服务的一个重要领域是确保始终验证输入。除了维护数据完整性之外,这样做还可以防止 SQL 注入等安全漏洞。

Java Bean 注解

为了实现输入验证,我们可以使用 JavaEE 6 中引入的 Java Bean 验证注解。为了说明它们的使用,让我们在 中实现端点以接受预订示例网络服务。我们的预订服务接受以下 Java 类形式的 请求:

public class BookingRequest {

  @Min(1)
  private final long roomId;

  @NotNull
  private final DateRange dateRange;

  @Size(min = 1, max = 128)
  private final String customerName;

  @NotNull
  private CreditCardDetails creditCardDetails;
}

您可以在这里看到 @javax.validation.constraints.Min、@javax.validation.constraints.NotNull@javax.validation 的使用。约束。大小@Min 注释允许定义 roomId 的最小有效值。 @NotNull 注释确保该字段具有值。最后,@Size 注释有助于确保客户姓名不大于数据库字段的大小。

常用表达

另一个非常有用的验证注解是@javax.validation.constraints.Pattern。此注释允许基于正则表达式验证字段。例如,让我们看一下 CreditCardDetails 类:

public class CreditCardDetails {
  @NotNull
  private String cardOwner;
  @Pattern(regexp = "\\b(?:4[0-9]{12}(?:[0-9]{3})?|" +
 "5[12345][0-9]{14}|3[47][0-9]{13}|" +
 "3(?:0[012345]|[68][0-9])[0-9]{11}|" +
 "6(?:011|5[0-9]{2})[0-9]{12}|" +
 "(?:2131|1800|35[0-9]{3})[0-9]{11})\\b")
  private String cardNumber;
  @Pattern(regexp = "[0-9]{2}/[0-9]{2}")
  private String expiration;
  @Pattern(regexp = "[0-9]{3,4}")
  private String cvv;
}

我们已经用验证注释声明了这个类中的每个字段。例如,验证 CVV 编号 根据一个正则表达式来检查该值是否由三位或四位数字组成。

小费

在具有大量请求的系统中,使用正则表达式进行输入验证可能会增加不必要的延迟。因此,服务设计者在使用正则表达式时应该考虑利弊。

验证预订

我们的 BookingsResource 应该在处理传入的预订请求之前对其进行验证。让我们添加以下端点:

@RestController
@RequestMapping("/bookings")
public class BookingsResource {
  @RequestMapping(method = RequestMethod.POST)
  public ApiResponse book(@Valid @RequestBody BookingRequest request) {
    BookingResponse response = bookingService.book(request);
    return new ApiResponse(Status.OK, response);
  }
}

当 POST 请求发送到 /bookings/ 时,将调用此方法。通过简单地将 @Valid 注释添加到方法中,Spring 将确保传入的预订请求首先通过我们定义的验证规则运行。

例如,使用 Postman,我们可以发送一个 POST 请求,如下所示:

{
  "roomId":1,
  "dateRange": {"from":"2017-01-01","to":"2017-01-02"},
  "customerName":"Jane Doe",
  "creditCardDetails": {
    "cardNumber": "0111-1111-1111-1111",
    "cardOwner": "John Doe",
    "expiration": "01/20",
    "cvv": "020"
  }
}

由于信用卡卡号无效(以0开头),服务器将响应400错误响应 错误。

加密


用于保护 Web 服务和一般 Web 的最常见的加密形式是 HTTPS。与 HTTP 不同,HTTP 在服务器和客户端之间以纯文本形式交换数据,HTTPS 对内容进行加密 a> 的请求和响应,以使它们对任何在网络上收听的人来说都是不透明的。

有关 HTTPS 的文献非常丰富且随时可用。此外,在 Web 服务部署中通常使用的软件包和硬件中对 HTTPS 的支持也很丰富。由于这些原因,本节将不深入探讨使用 HTTPS 的细节。除了构建用于重定向的 URL 之外,安全通信协议的使用对 RESTful Web 服务的实现几乎没有影响。

存储敏感数据

如果系统受到威胁,另一个重要的加密点是持久性 层。正如本章前面提到的,在数据​​库中加密密码是一种很好的做法,这样即使数据库被未经授权的人出于恶意而访问,最敏感的信息仍然(在某种程度上)是安全的。

Spring 为加密数据库中的数据提供了很好的支持。 API 设计者可以利用 org.springframework.security.crypto.password.PasswordEncoder。例如,org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder 是散列密码的一个很好的 选择在数据库中。

概括


服务设计者可能需要实施的安全措施的广度比本章所涵盖的要广泛得多。从认证用户到授权他们的操作,以及使用加密来防止窃听敏感信息,我们已经介绍了使用 Spring 保护 Web 服务的基本原则。

在本章中考虑了安全问题之后,评估 RESTful Web 服务的生产就绪性的另一个关键属性是测试。

在下一章中,我们将研究可以利用哪些工具和技术来验证我们的示例 RESTful Web 服务是否表现出预期的行为。