原文:
zh.annas-archive.org/md5/6DEAFFE8EE2C8DC4EDE2FE79BBA87B88译者:飞龙
第六章:REST API 安全
Spring Security 可以用于保护 REST API。本章首先介绍了有关 REST 和 JWT 的一些重要概念。
然后,本章介绍了 OAuth 概念,并通过实际编码示例,解释了在 Spring 框架的 Spring Security 和 Spring Boot 模块中利用简单和高级 REST API 安全。
我们将在示例中使用 OAuth 协议来保护暴露的 REST API,充分利用 Spring Security 功能。我们将使用 JWT 在服务器和客户端之间交换声明。
在本章中,我们将涵盖以下概念:
-
现代应用程序架构
-
响应式 REST API
-
简单的 REST API 安全
-
高级 REST API 安全
-
Spring Security OAuth 项目
-
OAuth2 和 Spring WebFlux
-
Spring Boot 和 OAuth2
重要概念
在进行编码之前,我们需要熟悉一些重要概念。本节旨在详细介绍一些这些概念。
REST
表述性状态转移(REST)是 Roy Fielding 于 2000 年提出的一种用于开发 Web 服务的架构风格。它建立在著名的超文本传输协议(HTTP)之上,可以以多种格式传输数据,最常见的是JavaScript 对象表示法(JSON)和可扩展标记语言(XML)。在 REST 中,请求的状态使用标准的 HTTP 状态码表示(200:OK,404:页面未找到!等)。基于 HTTP,安全性是通过已熟悉的安全套接字层(SSL)和传输层安全性(TLS)来处理的。
在编写此类 Web 服务时,您可以自由选择任何编程语言(Java,.NET 等),只要它能够基于 HTTP 进行 Web 请求(这是每种语言都支持的事实标准)。您可以使用许多知名的框架来开发服务器端的 RESTful API,这样做非常容易和简单。此外,在客户端,有许多框架可以使调用 RESTful API 和处理响应变得简单直接。
由于 REST 是基于互联网协议工作的,通过提供适当的 HTTP 头部(Cache-Control,Expires 等),可以很容易地实现对 Web 服务响应的缓存。PUT和DELETE方法在任何情况下都不可缓存。以下表格总结了 HTTP 方法的使用:
| HTTP 方法 | 描述 |
|---|---|
GET | 检索资源 |
POST | 创建新资源 |
PUT | 更新现有资源 |
DELETE | 删除现有资源 |
PATCH | 对资源进行部分更新 |
表 1:HTTP 方法使用
REST API 请求/响应(通过网络发送的数据)可以通过指定适当的 HTTP 头部进行压缩,类似于缓存。客户端发送 Accept-Encoding 的 HTTP 头部,让服务器知道它可以理解哪些压缩算法。服务器成功压缩响应并输出另一个 HTTP 头部 Content-Encoding,让客户端知道应该使用哪种算法进行解压缩。
JSON Web Token(JWT)
“JSON Web Tokens 是一种开放的、行业标准的 RFC 7519 方法,用于在两个当事方之间安全地表示声明。”
- jwt.io/
在过去,HTTP 的无状态性质在 Web 应用程序中被规避(大多数 Web 应用程序的性质是有状态的),方法是将每个请求与在服务器上创建的会话 ID 相关联,然后由客户端使用 cookie 存储。每个请求都以 HTTP 头部的形式发送 cookie(会话 ID),服务器对其进行验证,并将状态(用户会话)与每个请求相关联。在现代应用程序中(我们将在下一节中更详细地介绍),服务器端的会话 ID 被 JWT 替代。以下图表显示了 JWT 的工作原理:

图 1:JWT 在现代应用程序中的工作原理
在这种情况下,Web 服务器不会创建用户会话,并且对于需要有状态应用程序的用户会话管理功能被卸载到其他机制。
在 Spring 框架的世界中,Spring Session 模块可以用于将会话从 Web 服务器外部化到中央持久性存储(Redis、Couchbase 等)。每个包含有效令牌(JWT)的请求都会针对这个外部的真实性和有效性存储进行验证。验证成功后,应用程序可以生成有效令牌并将其作为响应发送给客户端。然后客户端可以将此令牌存储在其使用的任何客户端存储机制中(sessionStorage、localStorage、cookies 等,在浏览器中)。使用 Spring Security,我们可以验证此令牌以确定用户的真实性和有效性,然后执行所需的操作。本章的后续部分(简单 REST API 安全性)中有一个专门的示例,该示例使用基本身份验证机制,并在成功时创建 JWT。随后的请求使用 HTTP 标头中的令牌,在服务器上进行验证以访问其他受保护的资源。
以下几点突出了使用 JWT 的一些优点:
-
更好的性能:每个到达服务器的请求都必须检查发送的令牌的真实性。JWT 的真实性可以在本地检查,不需要外部调用(比如到数据库)。这种本地验证性能良好,减少了请求的整体响应时间。
-
简单性:JWT 易于实现和简单。此外,它是行业中已经建立的令牌格式。有许多知名的库可以轻松使用 JWT。
令牌的结构
与常见的安全机制(如加密、混淆和隐藏)不同,JWT 不会加密或隐藏其中包含的数据。但是,它确实让目标系统检查令牌是否来自真实来源。JWT 的结构包括标头、有效载荷和签名。如前所述,与其加密,JWT 中包含的数据被编码,然后签名。编码的作用是以一种可被各方接受的方式转换数据,签名允许我们检查其真实性,实际上是其来源:
JWT = header.payload.signature
让我们更详细地了解构成令牌的每个组件。
标头
这是一个 JSON 对象,采用以下格式。它提供有关如何计算签名的信息:
{
"alg": "HS256",
"typ": "JWT"
}
typ的值指定对象的类型,在这种情况下是JWT。alg的值指定用于创建签名的算法,在这种情况下是HMAC-SHA256。
有效载荷
有效载荷形成 JWT 中存储的实际数据(也称为声明)。根据应用程序的要求,您可以将任意数量的声明放入 JWT 有效载荷组件中。有一些预定义的声明,例如iss(发行人)、sub(主题)、exp(过期时间)、iat(发布时间)等,可以使用,但所有这些都是可选的:
{
"sub": "1234567890",
"username": "Test User",
"iat": 1516239022
}
签名
签名形成如下:
-
标头是base64编码的:base64(标头)。 -
有效载荷是base64编码的:base64(有效载荷)。 -
现在用中间的
"."连接步骤 1和步骤 2中的值:
base64UrlEncode(header) + "." +base64UrlEncode(payload)
- 现在,签名是通过使用标头中指定的算法对步骤 3中获得的值进行哈希,然后将其与您选择的秘密文本(例如
packtpub)附加而获得的:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
packtpub
)
最终的 JWT 如下所示:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IlRlc3QgVXNlciIsImlhdCI6MTUxNjIzOTAyMn0.yzBMVScwv9Ln4vYafpTuaSGa6mUbpwCg84VOhVTQKBg
网站jwt.io/是我在任何 JWT 需求中经常访问的地方。此示例中使用的示例数据来自该网站:

图 2:来自 https://jwt.io/的屏幕截图
现代应用程序架构
大多数现代应用的前端不是使用服务器端 Web 应用程序框架构建的,比如 Spring MVC、Java Server Faces(JSF)等。事实上,许多应用是使用完整的客户端框架构建的,比如 React(要成为完整的框架,必须与其他库结合使用)、Angular 等。前面的陈述并不意味着这些服务器端 Web 应用程序框架没有任何用处。根据您正在构建的应用程序,每个框架都有特定的用途。
在使用客户端框架时,一般来说,客户端代码(HTML、JS、CSS 等)并不安全。然而,渲染这些动态页面所需的数据是安全的,位于 RESTful 端点之后。
为了保护 RESTful 后端,使用 JWT 在服务器和客户端之间交换声明。JWT 实现了两方之间令牌的无状态交换,并通过服务器消除了会话管理的负担(不再需要多个服务器节点之间的粘性会话或会话复制),从而使应用程序能够以成本效益的方式水平扩展:

图 3:基于 API 的现代应用架构
SOFEA
面向服务的前端架构(SOFEA)是一种在过去获得了流行的架构风格,当时面向服务的架构(SOA)在许多企业中变得普遍。在现代,SOA 更多地采用基于微服务的架构,后端被减少为一堆 RESTful 端点。另一方面,客户端变得更厚,并使用客户端 MVC 框架,比如 Angular 和 React,只是举几个例子。然而,SOFEA 的核心概念,即后端只是端点,前端(UI)变得更厚,是现代 Web 应用程序开发中每个人都考虑的事情。
SOFEA 的一些优点如下:
-
这种客户端比我们过去看到的薄客户端 Web 应用程序更厚(类似于厚客户端应用程序)。在页面的初始视图/渲染之后,所有资产都从服务器下载并驻留/缓存在客户端(浏览器)上。此后,只有在客户端通过 XHR(Ajax)调用需要数据时,才会与服务器联系。
-
客户端代码下载后,只有数据从服务器流向客户端,而不是呈现代码(HTML、JavaScript 等),更好地利用带宽。由于传输的数据量较少,响应时间更快,使应用程序性能更好。
-
任意数量的客户端可以利用相同的 RESTful 服务器端点编写,充分重用 API。
-
这些端点可以外部化会话(在 Spring 框架中,有一个称为Spring Session的模块,可以用来实现这种技术能力),从而轻松实现服务器的水平扩展。
-
在项目中,通过由一个团队管理的 API 和由另一个团队管理的 UI 代码,更好地分离团队成员的角色。
响应式 REST API
在第四章中,使用 CAS 和 JAAS 进行身份验证,我们详细介绍了响应式 Spring WebFlux Web 应用程序框架。我们还深入研究了 Spring 框架和其他 Spring 模块提供的许多响应式编程支持。无论是有意还是无意,我们在上一章的示例部分创建了一个响应式 REST API。我们使用了处理程序和路由器机制来创建一个 RESTful 应用程序,并使用了BASIC身份验证机制进行了安全保护。
我们看到了WebClient(一种调用 REST API 的响应式方式,与使用阻塞的RestTemplate相对)和WebTestClient(一种编写测试用例的响应式方式)的工作原理。我们还以响应式方式使用 Spring Data,使用 MongoDB 作为持久存储。
我们不会在这里详细介绍这些方面;我们只会提到,如果你愿意,你可以通过阅读第四章中的部分来熟悉这个主题,使用 CAS 和 JAAS 进行身份验证。在本章中,我们将继续上一章的内容,介绍使用 JWT 进行 REST API 安全,然后介绍使用 OAuth 进行 REST API 安全(实现自定义提供者,而不是使用公共提供者,如 Google、Facebook 等)。
简单的 REST API 安全
我们将使用我们在第五章中创建的示例,与 Spring WebFlux 集成(spring-boot-spring-webflux),并通过以下方式进行扩展:
-
将 JWT 支持引入到已使用基本身份验证进行安全保护的现有 Spring WebFlux 应用程序中。
-
创建一个新的控制器(
路径/auth/**),将有新的端点,使用这些端点可以对用户进行身份验证。 -
使用基本身份验证或 auth REST 端点,我们将在服务器上生成 JWT 并将其作为响应发送给客户端。客户端对受保护的 REST API 的后续调用可以通过使用作为 HTTP 头部(授权,令牌)提供的 JWT 来实现。
我们无法详细介绍这个项目的每一个细节(我们在规定的页数内需要涵盖一个更重要的主题)。然而,在浏览示例时,重要的代码片段将被列出并进行详细解释。
Spring Security 配置
在 Spring Security 配置中,我们调整了springSecurityFilterChain bean,如下面的代码片段所示:
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http){
AuthenticationWebFilter authenticationJWT = new AuthenticationWebFilter(new
UserDetailsRepositoryReactiveAuthenticationManager(userDetailsRepository()));
authenticationJWT.setAuthenticationSuccessHandler(new
JWTAuthSuccessHandler());
http.csrf().disable();
http
.authorizeExchange()
.pathMatchers(WHITELISTED_AUTH_URLS)
.permitAll()
.and()
.addFilterAt(authenticationJWT, SecurityWebFiltersOrder.FIRST)
.authorizeExchange()
.pathMatchers(HttpMethod.GET, "/api/movie/**").hasRole("USER")
.pathMatchers(HttpMethod.POST, "/api/movie/**").hasRole("ADMIN")
.anyExchange().authenticated()
.and()
.addFilterAt(new JWTAuthWebFilter(), SecurityWebFiltersOrder.HTTP_BASIC);
return http.build();
}
正如你所看到的,我们配置了一个新的AuthenticationWebFilter和一个AuthenticationSuccessHandler。我们还有一个新的JWTAuthWebFilter类来处理基于 JWT 的身份验证。
我们将使用ReactiveUserDetailsService和硬编码的用户凭据进行测试,如下面的代码片段所示:
@Bean
public MapReactiveUserDetailsService userDetailsRepository() {
UserDetails user = User.withUsername("user").password("
{noop}password").roles("USER").build();
UserDetails admin = User.withUsername("admin").password("
{noop}password").roles("USER","ADMIN").build();
return new MapReactiveUserDetailsService(user, admin);
}
身份验证成功处理程序
我们在 Spring Security 配置类中设置了自定义的AuthenticationSuccessHandler(该类的源代码将在下面显示)。在成功验证后,它将生成 JWT 并设置 HTTP 响应头:
-
头部名称:
Authorization -
头部值:
Bearer JWT
让我们看一下下面的代码:
public class JWTAuthSuccessHandler implements ServerAuthenticationSuccessHandler{
@Override
public Mono<Void> onAuthenticationSuccess(WebFilterExchange
webFilterExchange, Authentication authentication) {
ServerWebExchange exchange = webFilterExchange.getExchange();
exchange.getResponse()
.getHeaders()
.add(HttpHeaders.AUTHORIZATION,
getHttpAuthHeaderValue(authentication));
return webFilterExchange.getChain().filter(exchange);
}
private static String getHttpAuthHeaderValue(Authentication authentication){
return String.join(" ","Bearer",tokenFromAuthentication(authentication));
}
private static String tokenFromAuthentication(Authentication authentication){
return new JWTUtil().generateToken(
authentication.getName(),
authentication.getAuthorities());
}
}
JWTUtil类包含许多处理 JWT 的实用方法,例如生成令牌、验证令牌等。JWTUtil类中的generateToken方法如下所示:
public static String generateToken(String subjectName, Collection<? extends GrantedAuthority> authorities) {
JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
.subject(subjectName)
.issuer("javacodebook.com")
.expirationTime(new Date(new Date().getTime() + 30 * 1000))
.claim("auths", authorities.parallelStream().map(auth -> (GrantedAuthority) auth).map(a ->
a.getAuthority()).collect(Collectors.joining(",")))
.build();
SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256), claimsSet);
try {
signedJWT.sign(JWTUtil.getJWTSigner());
} catch (JOSEException e) {
e.printStackTrace();
}
return signedJWT.serialize();
}
自定义 WebFilter,即 JWTAuthWebFilter
我们的自定义WebFilter,名为JWTAuthWebFilter,负责将接收到的 JWT 令牌转换为 Spring Security 理解的适当类。它使用了一个名为JWTAuthConverter的转换器,该转换器执行了许多操作,如下所示:
-
获取授权
payload -
通过丢弃
Bearer字符串来提取令牌 -
验证令牌
-
创建一个 Spring Security 理解的
UsernamePasswordAuthenticationToken类
下面的代码片段显示了JWTAuthWebFilter类及其上面列出的操作的重要方法。
public class JWTAuthConverter implements Function<ServerWebExchange,
Mono<Authentication>> {
@Override
public Mono<Authentication> apply(ServerWebExchange serverWebExchange) {
return Mono.justOrEmpty(serverWebExchange)
.map(JWTUtil::getAuthorizationPayload)
.filter(Objects::nonNull)
.filter(JWTUtil.matchBearerLength())
.map(JWTUtil.getBearerValue())
.filter(token -> !token.isEmpty())
.map(JWTUtil::verifySignedJWT)
.map(JWTUtil::getUsernamePasswordAuthenticationToken)
.filter(Objects::nonNull);
}
}
在此转换之后,使用 Spring Security 进行实际的身份验证,该身份验证在应用程序中设置了SecurityContext,如下面的代码片段所示:
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return this.getAuthMatcher().matches(exchange)
.filter(matchResult -> matchResult.isMatch())
.flatMap(matchResult -> this.jwtAuthConverter.apply(exchange))
.switchIfEmpty(chain.filter(exchange).then(Mono.empty()))
.flatMap(token -> authenticate(exchange, chain, token));
}
//..more methods
private Mono<Void> authenticate(ServerWebExchange exchange,
WebFilterChain chain, Authentication token) {
WebFilterExchange webFilterExchange = new WebFilterExchange(exchange, chain);
return this.reactiveAuthManager.authenticate(token)
.flatMap(authentication -> onAuthSuccess(authentication,
webFilterExchange));
}
private Mono<Void> onAuthSuccess(Authentication authentication, WebFilterExchange
webFilterExchange) {
ServerWebExchange exchange = webFilterExchange.getExchange();
SecurityContextImpl securityContext = new SecurityContextImpl();
securityContext.setAuthentication(authentication);
return this.securityContextRepository.save(exchange, securityContext)
.then(this.authSuccessHandler
.onAuthenticationSuccess(webFilterExchange, authentication))
.subscriberContext(ReactiveSecurityContextHolder.withSecurityContext(
Mono.just(securityContext)));
}
JWTAuthWebFilter类的过滤器方法进行必要的转换,然后authenticate方法进行实际的身份验证,最后调用onAuthSuccess方法。
新的控制器类
我们有两个控制器,分别是DefaultController(映射到/和/login路径)和AuthController(映射到/auth主路由和/token子路由)。/auth/token路径可用于检索令牌,该令牌可用于后续的 API 调用(Bearer <Token>)。AuthController的代码片段如下所示:
@RestController
@RequestMapping(path = "/auth", produces = { APPLICATION_JSON_UTF8_VALUE })
public class AuthController {
@Autowired
private MapReactiveUserDetailsService userDetailsRepository;
@RequestMapping(method = POST, value = "/token")
@CrossOrigin("*")
public Mono<ResponseEntity<JWTAuthResponse>> token(@RequestBody
JWTAuthRequest jwtAuthRequest) throws AuthenticationException {
String username = jwtAuthRequest.getUsername();
String password = jwtAuthRequest.getPassword();
return userDetailsRepository.findByUsername(username)
.map(user -> ok().contentType(APPLICATION_JSON_UTF8).body(
new JWTAuthResponse(JWTUtil.generateToken(user.getUsername(), user.getAuthorities()), user.getUsername())))
.defaultIfEmpty(notFound().build());
}
}
}
运行应用程序并进行测试
使用下面显示的 Spring Boot 命令运行应用程序:
mvn spring-boot:run
我将使用 Postman 执行 REST 端点。
您可以通过以下两种方法获得令牌,并在随后的调用中包含它:
- 如果使用基本身份验证凭据访问任何路由,在响应头中,您应该获得令牌。我将使用
/login路径与基本身份验证(授权头)获取令牌,如图所示:

图 4:在 Postman 中使用基本身份验证获取令牌
- 使用 JSON 形式的基本身份验证凭据(使用
JWTAuthRequest类),如图所示,在 Postman 中访问/auth/token端点:

图 5:使用 JSON 中的基本身份验证凭据使用/auth/token 端点获取令牌
使用检索到的令牌,如图所示,在 Postman 中调用电影端点:

图 6:在 Postman 中使用 JWT 令牌检索电影列表
这完成了我们正在构建的示例。在这个示例中,我们使用 JWT 保护了 REST API,并使用 Spring Security 进行了验证。如前所述,这是您可以使用 Spring Security 和 JWT 保护 REST API 的基本方法。
高级 REST API 安全性
REST API 可以通过您的 Web 应用程序中的另一种机制进行保护,即 OAuth。
OAuth 是一个授权框架,允许其他应用程序使用正确的凭据访问存储在 Google 和 Facebook 等平台上的部分/有限用户配置文件详细信息。认证部分被委托给这些服务,如果成功,适当的授权将被授予调用客户端/应用程序,这可以用来访问受保护的资源(在我们的情况下是 RESTful API)。
我们已经在第三章中看到了使用公共身份验证提供程序的 OAuth 安全性,使用 CAS 和 JAAS 进行身份验证(在OAuth 2 和 OpenID 连接部分)。但是,我们不需要使用这些公共提供程序;您可以选择使用自己的提供程序。在本章中,我们将涵盖一个这样的示例,我们将使用自己的身份验证提供程序并保护基于 Spring Boot 的响应式 REST 端点。
在进入示例之前,我们需要更多地了解 OAuth,并且需要了解它的各个组件。我们已经在第三章中详细介绍了 OAuth 的许多细节,使用 CAS 和 JAAS 进行身份验证。我们将在本节中添加这些细节,然后通过代码示例进行讲解。
OAuth2 角色
OAuth 为用户和应用程序规定了四种角色。这些角色之间的交互如下图所示:

图 7:OAuth 角色交互
我们将详细了解这些 OAuth 角色中的每一个。
资源所有者
这是拥有所需受保护资源的消费客户端应用程序的用户。如果我们以 Facebook 或 Google 作为身份验证提供程序,资源所有者就是在这些平台上保存数据的实际用户。
资源服务器
这是以托管 API 的形式拥有受保护资源的服务器。如果以 Google 或 Facebook 为例,它们以 API 的形式保存配置文件信息以及其他信息。如果客户端应用程序成功进行身份验证(使用用户提供的凭据),然后用户授予适当的权限,他们可以通过公开的 API 访问这些信息。
客户端
这是用于访问资源服务器上可用的受保护资源的应用程序。如果用户成功验证并且客户端应用程序被用户授权访问正确的信息,客户端应用程序可以检索数据。
授权服务器
这是一个验证和授权客户端应用程序访问资源所有者和资源服务器上拥有的受保护资源的服务器。同一个服务器执行这两个角色并不罕见。
要参与 OAuth,您的应用程序必须首先向服务提供商(如 Google、Facebook 等)注册,以便通过提供应用程序名称、应用程序 URL 和回调 URL 进行身份验证。成功注册应用程序与服务提供商后,您将获得两个应用程序唯一的值:client application_id和client_secret。client_id可以公开,但client_secret保持隐藏(私有)。每当访问服务提供商时,都需要这两个值。以下图显示了这些角色之间的交互:

图 8:OAuth 角色交互
前面图中的步骤在这里有详细介绍:
-
客户端应用程序请求资源所有者授权它们访问受保护资源
-
如果资源所有者授权,授权授予将发送到客户端应用程序
-
客户端应用程序请求令牌,使用资源所有者提供的授权以及来自授权服务器的身份验证凭据
-
如果客户端应用程序的凭据和授权有效,授权服务器将向客户端应用程序发放访问令牌
-
客户端应用程序使用提供的访问令牌访问资源服务器上的受保护资源
-
如果客户端应用程序发送的访问令牌有效,资源服务器将允许访问受保护资源
授权授予类型
如图所示,为了让客户端开始调用 API,它需要以访问令牌的形式获得授权授予。OAuth 提供了四种授权类型,可以根据不同的应用程序需求使用。关于使用哪种授权授予类型的决定留给了客户端应用程序。
授权码流程
这是一种非常常用的授权类型,它在服务器上进行重定向。它非常适用于服务器端应用程序,其中源代码托管在服务器上,客户端上没有任何内容。以下图解释了授权码授权类型的流程:

图 9:授权码流程
前面图中的步骤在这里有详细介绍:
- 受保护资源的资源所有者将在浏览器中呈现一个屏幕,以授权请求。这是一个示例授权链接:
https://<DOMAIN>/oauth/authorize?response_type=code&client_id=<CLIENT_ID>&redirect_uri=<CALLBACK_URL>&scope=<SCOPE>。
这是上述链接中的重要查询参数:
-
client_id:我们在向服务提供商注册应用程序时获得的客户端应用程序 ID
-
redirect_uri:成功授权后,服务器将重定向到提供的 URL -
response_type:客户端用来向服务器请求授权码的非常重要的参数 -
scope:指定所需的访问级别
-
如果资源所有者(用户)允许,他们点击授权链接,该链接被发送到授权服务器。
-
如果发送到授权服务器的授权请求经过验证并且成功,客户端将从授权服务器接收授权码授权,附加为回调 URL(
<CALLBACK_URL>?code=<AUTHORIZATION_CODE>)中的查询参数,指定在步骤 1中。 -
使用授权授予,客户端应用程序从授权服务器请求访问令牌(
https://<DOMAIN>/oauth/token?client_id=<CLIENT_ID>&client_secret=<CLIENT_SECRET>&grant_type=authorization_code&code=<AUTHORIZATION_CODE>&redirect_uri=CALLBACK_URL)。
在此 URL 中,还必须传递客户端应用程序的client_secret,以及声明传递的代码是授权代码的grant_type参数。
-
授权服务器验证凭据和授权授予,并向客户端应用程序发送访问令牌,最好以 JSON 形式。
-
客户端应用程序使用步骤 5中收到的访问令牌调用资源服务器上的受保护资源。
-
如果步骤 5中提供的访问令牌有效,则资源服务器允许访问受保护资源。
隐式流
这在移动和 Web 应用程序中通常使用,并且也基于重定向工作。以下图表解释了隐式代码授权类型的流程:

图 10:隐式流
前面图表中的步骤在这里进行了详细解释:
- 资源所有者被呈现一个屏幕(浏览器)来授权请求。这是一个示例授权链接:
https://<DOMAIN>/oauth/authorize?response_type=token&client_id=CLIENT_ID&redirect_uri=CALLBACK_URL&scope=<SCOPE>。
重要的是要注意,前面链接中指定的response_type是token。这表示服务器应该给出访问令牌(这是与前一节讨论的授权代码流授权类型的主要区别之一)。
-
如果资源所有者(用户)允许此操作,则点击授权链接,该链接将发送到授权服务器。
-
用户代理(浏览器或移动应用程序)在指定的
CALLBACK_URL中接收访问令牌(https://<CALLBACK_URL>#token=<ACCESS_TOKEN>)。 -
用户代理转到指定的
CALLBACK_URL,保留访问令牌。 -
客户端应用程序打开网页(使用任何机制),从
CALLBACK_URL中提取访问令牌。 -
客户端应用程序现在可以访问访问令牌。
-
客户端应用程序使用访问令牌调用受保护的 API。
客户端凭据
这是最简单的授权方式之一。客户端应用程序将凭据(客户端的服务帐户)与client_ID和client_secret一起发送到授权服务器。如果提供的值有效,授权服务器将发送访问令牌,该令牌可用于访问受保护的资源。
资源所有者密码凭据
这是另一种简单易用的类型,但被认为是所有类型中最不安全的。在这种授权类型中,资源所有者(用户)必须直接在客户端应用程序界面中输入他们的凭据(请记住,客户端应用程序可以访问资源所有者的凭据)。然后客户端应用程序使用这些凭据发送到授权服务器以获取访问令牌。只有在资源所有者完全信任他们提供凭据给服务提供者的应用程序时,这种授权类型才有效,因为这些凭据通过客户端应用程序的应用服务器传递(因此如果客户端应用程序决定的话,它们可以被存储)。
访问令牌和刷新令牌
客户端应用程序可以使用访问令牌从资源服务器检索信息,该信息在令牌被视为有效的规定时间内。之后,服务器将使用适当的 HTTP 响应错误代码拒绝请求。
OAuth 允许授权服务器在访问令牌的同时发送另一个令牌,即刷新令牌。当访问令牌过期时,客户端应用程序可以使用第二个令牌请求授权服务器提供新的访问令牌。
Spring Security OAuth 项目
目前在 Spring 生态系统中,OAuth 支持已扩展到许多项目,包括 Spring Security Cloud、Spring Security OAuth、Spring Boot 和 Spring Security(5.x+)的版本。这在社区内造成了很多混乱,没有单一的所有权来源。Spring 团队采取的方法是整合这一切,并开始维护与 Spring Security 有关的所有 OAuth 内容。预计将在 2018 年底之前将 OAuth 的重要组件,即授权服务器、资源服务器以及对 OAuth2 和 OpenID Connect 1.0 的下一级支持,添加到 Spring Security 中。Spring Security 路线图清楚地说明,到 2018 年中期,将添加对资源服务器的支持,并在 2018 年底之前添加对授权服务器的支持。
在撰写本书时,Spring Security OAuth 项目处于维护模式。这意味着将发布用于修复错误/安全性问题的版本,以及一些较小的功能。未来不计划向该项目添加重大功能。
各种 Spring 项目中提供的完整 OAuth2 功能矩阵可以在github.com/spring-projects/spring-security/wiki/OAuth-2.0-Features-Matrix找到。
在撰写本书时,我们需要实现 OAuth 的大多数功能都作为 Spring Security OAuth 项目的一部分可用,该项目目前处于维护模式。
OAuth2 和 Spring WebFlux
在撰写本书时,Spring Security 中尚未提供 Spring WebFlux 应用程序的全面 OAuth2 支持。但是,社区对此非常紧迫,并且许多功能正在逐渐加入 Spring Security。许多示例也正在 Spring Security 项目中展示使用 Spring WebFlux 的 OAuth2。在第五章中,与 Spring WebFlux 集成,我们详细介绍了一个这样的示例。在撰写本书时,Spring Security OAuth2 对 Spring MVC 有严格的依赖。
Spring Boot 和 OAuth2
在撰写本书时,Spring Boot 宣布不再支持 Spring Security OAuth 模块。相反,它将从现在开始使用 Spring Security 5.x OAuth2 登录功能。
一个名为 Spring Security OAuth Boot 2 Autoconfig 的新模块(其在 pom.xml 中的依赖如下代码片段所示),从 Spring Boot 1.5.x 移植而来,可用于将 Spring Security 与 Spring Boot 集成:
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
</dependency>
项目源代码可以在github.com/spring-projects/spring-security-oauth2-boot找到。此模块的完整文档可以在docs.spring.io/spring-security-oauth2-boot/docs/current-SNAPSHOT/reference/htmlsingle/找到。
示例项目
在我们的示例项目中,我们将设置自己的授权服务器,我们将对其进行授权,以授权通过我们的资源服务器公开的 API。我们在我们的资源服务器上公开了电影 API,并且客户端应用程序将使用应用程序进行身份验证(客户端应用程序受 Spring Security 保护),然后尝试访问其中一个电影 API,此时 OAuth 流程将启动。成功与授权服务器进行授权检查后,客户端将获得对所请求的电影 API 的访问权限。
我们将有一个包含三个 Spring Boot 项目的父项目:oauth-authorization-server、oauth-resource-server和oauth-client-app:

图 11:IntelliJ 中的项目结构
我们现在将在后续章节中查看每个单独的 Spring Boot 项目。完整的源代码可在书的 GitHub 页面上的spring-boot-spring-security-oauth项目下找到。
授权服务器
这是一个传统的 Spring Boot 项目,实现了授权服务器 OAuth 角色。
Maven 依赖
要包含在 Spring Boot 项目的pom.xml文件中的主要依赖项如下面的代码片段所示:
<!--Spring Boot-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--OAuth-->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.2.RELEASE</version>
</dependency>
<!--JWT-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.9.RELEASE</version>
</dependency>
Spring Boot 运行类
这个 Spring Boot 的run类并没有什么特别之处,如下面的代码片段所示:
@SpringBootApplication
public class OAuthAuthorizationServerRun extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(OAuthAuthorizationServerRun.class, args);
}
}
Spring 安全配置
Spring 安全配置类扩展了WebSecurityConfigurerAdapter。我们将重写三个方法,如下面的代码片段所示:
@Configuration
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Autowired
public void globalUserDetails(final AuthenticationManagerBuilder auth) throws
Exception {
auth
.inMemoryAuthentication()
.withUser("user").password(passwordEncoder.encode("password"))
.roles("USER")
.and()
.withUser("admin").password(passwordEncoder.encode("password"))
.roles("USER", "ADMIN");
}
//...
}
我们autowire密码编码器。然后我们重写以下方法:globalUserDetails、authenticationManagerBean和configure。这里没有什么特别要提到的。我们在内存中定义了两个用户(用户和管理员)。
授权服务器配置
这是这个 Spring Boot 项目中最重要的部分,我们将在其中设置授权服务器配置。我们将使用一个新的注解@EnableAuthorizationServer。我们的配置类将扩展AuthorizationServerConfigurerAdapter。我们将使用 JWT 令牌存储,并展示一个令牌增强器,使用它可以在需要时增强 JWT 令牌的声明。这个配置类中最重要的方法被提取为下面的代码片段:
@Override
public void configure(final ClientDetailsServiceConfigurer clients) throws
Exception {
clients.inMemory()
.withClient("oAuthClientAppID")
.secret(passwordEncoder().encode("secret"))
.authorizedGrantTypes("password", "authorization_code", "refresh_token")
.scopes("movie", "read", "write")
.accessTokenValiditySeconds(3600)
.refreshTokenValiditySeconds(2592000)
.redirectUris("http://localhost:8080/movie/",
"http://localhost:8080/movie/index");
}
这是我们设置与客户端相关的 OAuth 配置的地方。我们只设置了一个客户端,并使用内存选项使示例更容易理解。在整个应用程序中,我们将使用BCrypt作为我们的密码编码器。我们的客户端应用程序的客户端 ID 是oAuthClientAppID,客户端密钥是secret。我们设置了三种授权类型,访问客户端时需要指定必要的范围(movie、read 和 write)。执行成功后,授权服务器将重定向到指定的 URL(http://localhost:8080/movie/或http://localhost:8080/movie/index)。如果客户端没有正确指定 URL,服务器将抛出错误。
JWT 令牌存储和增强相关方法如下面的代码片段所示:
@Bean
@Primary
public DefaultTokenServices tokenServices() {
final DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
return defaultTokenServices;
}
@Override
public void configure(final AuthorizationServerEndpointsConfigurer endpoints)
throws Exception {
final TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer(),
accessTokenConverter()));
endpoints.tokenStore(tokenStore()).tokenEnhancer(tokenEnhancerChain)
.authenticationManager(authenticationManager);
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
final JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("secret");
return converter;
}
@Bean
public TokenEnhancer tokenEnhancer() {
return new CustomTokenEnhancer();
}
在这段代码中,我们指定了将在tokenStore方法中使用的令牌存储,并声明了一个tokenEnhancer bean。为了展示令牌增强器,我们将使用一个名为CustomTokenEnhancer的自定义类;该类如下面的代码片段所示:
public class CustomTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken,
OAuth2Authentication authentication) {
final Map<String, Object> additionalInfo = new HashMap<>();
additionalInfo.put("principalinfo",
authentication.getPrincipal().toString());
((DefaultOAuth2AccessToken)accessToken)
.setAdditionalInformation(additionalInfo);
return accessToken;
}
}
自定义令牌enhancer类实现了TokenEnhancer。我们只是将新信息(principalinfo)添加到包含principal对象的toString版本的 JWT 令牌中。
应用程序属性
由于我们在本地运行了所有三个服务器,我们必须指定不同的端口。此外,授权服务器运行在不同的上下文路径上是很重要的。下面的代码片段显示了我们在application.properties文件中的内容:
server.servlet.context-path=/oauth-server
server.port=8082
作为一个 Spring Boot 项目,可以通过执行mvn spring-boot:run命令来运行。
资源服务器
这是一个传统的 Spring Boot 项目,实现了资源服务器 OAuth 角色。
Maven 依赖
在我们的pom.xml中,我们不会添加任何新的内容。我们在授权服务器项目中使用的相同依赖项也适用于这里。
Spring Boot 运行类
这是一个典型的 Spring Boot run类,我们在其中放置了@SpringBootApplication注解,它在幕后完成了所有的魔术。同样,在我们的 Spring Boot 运行类中没有特定于这个项目的内容。
资源服务器配置
这是主要的资源服务器配置类,我们在其中使用@EnableResourceServer注解,并将其扩展自ResourceServerConfigurerAdapter,如下面的代码片段所示:
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private CustomAccessTokenConverter customAccessTokenConverter;
@Override
public void configure(final HttpSecurity http) throws Exception {
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.ALWAY)
.and()
.authorizeRequests().anyRequest().permitAll();
}
@Override
public void configure(final ResourceServerSecurityConfigurer config) {
config.tokenServices(tokenServices());
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
final JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setAccessTokenConverter(customAccessTokenConverter);
converter.setSigningKey("secret");
converter.setVerifierKey("secret");
return converter;
}
@Bean
@Primary
public DefaultTokenServices tokenServices() {
final DefaultTokenServices defaultTokenServices =
new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
return defaultTokenServices;
}
}
Spring 安全配置
作为资源服务器,我们启用了全局方法安全,以便每个暴露 API 的方法都受到保护,如下面的代码片段所示:
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringSecurityConfig extends GlobalMethodSecurityConfiguration {
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
return new OAuth2MethodSecurityExpressionHandler();
}
}
在这里,我们使用OAuth2MethodSecurityExpressionHandler作为方法安全异常处理程序,以便我们可以使用注解,如下所示:
@PreAuthorize("#oauth2.hasScope('movie') and #oauth2.hasScope('read')")
Spring MVC 配置类
我们在之前的章节中详细介绍了 Spring MVC 配置。在我们的示例中,这是一个非常基本的 Spring MVC config类,其中使用了@EnableWebMvc并实现了WebMvcConfigurer。
控制器类
我们有一个控制器类,只公开一个方法(我们可以进一步扩展以公开更多的 API)。这个方法列出了硬编码的电影列表中的所有电影,位于 URL/movie下,如下面的代码片段所示:
@RestController
public class MovieController {
@RequestMapping(value = "/movie", method = RequestMethod.GET)
@ResponseBody
@PreAuthorize("#oauth2.hasScope('movie') and #oauth2.hasScope('read')")
public Movie[] getMovies() {
initIt();//Movie list initialization
return movies;
}
//…
}
我们使用了一个Movie模型类,利用了lombok库的所有功能,如下面的代码片段所示:
@Data
@ToString
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Movie {
private Long id;
private String title;
private String genre;
}
它有三个属性,注解将完成所有的魔术并保持模型简洁。
应用程序属性
与授权服务器类似,application.properties只有上下文路径和端口分配。
作为一个 Spring Boot 项目,可以通过执行mvn spring-boot:run命令来运行。
客户端应用程序
这是一个传统的 Spring Boot 项目,实现了客户端 OAuth 角色。
Maven 依赖项
在我们的 Spring Boot pom.xml文件中,添加了Thymeleaf和lombok库的新 Maven 依赖项。其余部分都是典型的 Spring Boot pom.xml文件,你现在已经熟悉了。
Spring Boot 类
在我们的示例 Spring Boot run类中,没有什么值得一提的。这是一个简单的类,包含了至关重要的main方法和@SpringBootApplication注解。
OAuth 客户端配置
这是客户端应用程序中的主配置类,使用了@EnableOAuth2Client注解,如下面的代码片段所示:
@Configuration
@EnableOAuth2Client
public class OAuthClientConfig {
@Autowired
private OAuth2ClientContext oauth2ClientContext;
@Autowired
@Qualifier("movieAppClientDetails")
private OAuth2ProtectedResourceDetails movieAppClientDetails;
@ConfigurationProperties(prefix = "security.oauth2.client.movie-app-client")
@Bean
public OAuth2ProtectedResourceDetails movieAppClientDetails() {
return new AuthorizationCodeResourceDetails();
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public OAuth2RestTemplate movieAppRestTemplate() {
return new OAuth2RestTemplate(movieAppClientDetails, oauth2ClientContext);
}
}
在这个类中要注意的重要方面是,我们通过在application.yml文件中配置客户端详细信息来初始化 OAuth2 REST 模板。
Spring 安全配置
在 Spring 安全config类中,我们设置了可以用于登录到应用程序并访问受保护资源的用户凭据(内存中)。在configure方法中,一些资源被标记为受保护的,一些资源被标记为未受保护的。
控制器类
我们有两个控制器类,SecuredController和NonSecuredController。顾名思义,一个用于声明的受保护路由,另一个用于未受保护的路由。我们感兴趣的受保护控制器中的main方法如下面的代码片段所示:
@RequestMapping(value = "/movie/index", method = RequestMethod.GET)
@ResponseBody
public Movie[] index() {
Movie[] movies = movieAppRestTemplate
.getForObject(movieApiBaseUri, Movie[].class);
return movies;
}
我们将资源服务器项目中使用的model类复制到客户端应用程序项目中。在理想情况下,所有这些共同的东西都将被转换为可重用的 JAR,并设置为两个项目的依赖项。
模板
模板非常简单。应用程序的根上下文将用户重定向到一个未安全的页面。我们有自己的自定义登录页面,登录成功后,用户将被导航到一个包含指向受保护的 OAuth 支持的电影列表 API 的链接的受保护页面。
应用程序属性
在这个项目中,我们使用application.yml文件,代码如下:
server:
port: 8080
spring:
thymeleaf:
cache: false
security:
oauth2:
client:
movie-app-client:
client-id: oAuthClientAppID
client-secret: secret
user-authorization-uri: http://localhost:8082/oauth-server/oauth/authorize
access-token-uri: http://localhost:8082/oauth-server/oauth/token
scope: read, write, movie
pre-established-redirect-uri: http://localhost:8080/movie/index
movie:
base-uri: http://localhost:8081/oauth-resource/movie
这个 YML 文件的非常重要的方面是movie-app-client属性设置。同样,作为一个 Spring Boot 项目,可以通过执行mvn spring-boot:run命令来运行。
运行项目
使用 Spring Boot 的mvn spring-boot:run命令分别启动所有项目。我在 IntelliJ 中使用 Spring Dashboard,可以启动所有项目,如下面的截图所示:

图 12:IntelliJ 中的 Spring Dashboard
导航到http://localhost:8080,您将被重定向到客户端应用程序的未安全页面,如下所示:

图 13:客户端应用程序的未安全页面
点击链接,您将被带到自定义登录页面,如下所示:

图 14:客户端应用程序的自定义登录页面
根据页面上的要求输入用户名/密码;然后,点击登录将带您到安全页面,如下所示:

图 15:客户端应用程序中的安全页面
点击电影 API 链接,您将被带到 OAuth 流程,然后到授权服务器的默认登录页面以输入凭据,如下所示:

图 16:授权服务器登录页面
输入用户名/密码(我们将其保留为 user/password),然后点击登录按钮。您将被带到授权页面,如下面的截图所示:

图 17:授权服务器上的授权页面
点击授权,您将被带回客户端应用程序页面,显示来自资源服务器的所有电影,如下所示:

图 18:客户端应用程序中显示资源服务器上暴露的电影 API 的电影列表页面
通过这样,我们完成了我们的示例应用程序,其中我们实现了 OAuth 的所有角色。
总结
我们在本章开始时向您介绍了一些需要跟进的重要概念。然后我们介绍了现代 Web 应用程序所需的重要特征。我们迅速介绍了一个称为SOFEA的架构,它恰当地介绍了我们想要构建现代应用程序的方式。然后我们通过最简单的方式实现了 REST API 的安全性。
在接下来的部分中,我们介绍了如何以更高级的方式使用 OAuth 来保护 REST API,使用 JWT。我们通过介绍了许多关于 OAuth 的概念开始了本节,最后以使用 OAuth 和 JWT 的完整示例项目结束了本章。
阅读完本章后,您应该对 REST、OAuth 和 JWT 有清晰的理解。您还应该在下一章中,对在应用程序中暴露的 RESTful 端点使用 Spring Security 感到舒适。
第七章:Spring 安全附加组件
在之前的章节中,我们介绍了核心安全方面(如身份验证和授权)使用 Spring 安全的多种方式的实现细节。在这样做时,我们只是浅尝辄止了 Spring 安全可以实现的能力的一层薄薄的表面。在本章中,我们将简要介绍 Spring 安全提供的一些其他能力。
此外,本章介绍了许多产品(开源和付费版本),可以考虑与 Spring 安全一起使用。我不支持这些产品中的任何一个,但我确实认为它们是实现您所寻找的技术能力的强有力竞争者。我们将通过简要介绍产品的技术能力的要点来介绍一个产品,然后简要介绍给您。
在本章中,我们将涵盖以下主题:
-
记住我认证
-
会话管理
-
CSRF
-
CSP
-
通道安全
-
CORS 支持
-
加密模块
-
秘密管理
-
HTTP 数据完整性验证器
-
自定义 DSL
记住我认证
我们将重用并增强我们在第二章中构建的示例,深入 Spring 安全(jetty-db-basic-authentication),以解释 Spring 安全如何用于实现记住我或持久登录功能。在我们要重用的示例中,我们使用了基本身份验证,其中用户凭据存储在 MySQL 数据库中。
在 Spring 安全中,通过在用户选择在客户端记住他/她的凭据时向浏览器发送 cookie 来实现记住我功能。可以配置 cookie 在浏览器中存储一段时间。如果 cookie 存在且有效,用户下次访问应用程序时,将直接进入用户的主页,避免使用用户名/密码组合进行显式认证。
可以使用两种方法实现记住我功能:
-
基于哈希的令牌:用户名、过期时间、密码和私钥被哈希并作为令牌发送到客户端
-
持久令牌:使用持久存储机制在服务器上存储令牌
现在,我们将通过一个简单的持久令牌方法来详细解释这个概念。
在 MySQL 数据库中创建一个新表
我们将使用与我们在第二章中使用的 MySQL 数据库相同的模式。保持一切不变,然后在 MySQL 工作台中执行以下 DDL 语句来创建一个新表,用于存储持久令牌:
create table persistent_logins(
series varchar(64) not null primary key,
username varchar(75) not null,
token varchar(100) not null,
last_used timestamp not null
);
Spring 安全配置
在第二章中,深入 Spring 安全(在Sample应用程序部分的 Spring 安全设置子部分),我们看到了基本身份验证,我们在 Spring 安全配置类的 configure 方法中进行了配置。在这个例子中,我们将创建一个自定义登录页面,并将登录机制更改为基于表单的。打开SpringSecurityConfig类,并按照下面的代码片段更改 configure 方法。然后,添加我们将使用的tokenRepository bean 来实现记住我功能:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests().anyRequest().hasAnyRole("ADMIN", "USER")
.and()
.authorizeRequests().antMatchers("/login**").permitAll()
.and()
.formLogin()
.loginPage("/login").loginProcessingUrl("/loginProc").permitAll()
.and()
.logout().logoutSuccessUrl("/login").permitAll()
.and()
.rememberMe()
.rememberMeParameter("rememberme").tokenRepository(tokenRepository());
}
@Bean
public PersistentTokenRepository tokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepositoryImpl=new JdbcTokenRepositoryImpl();
jdbcTokenRepositoryImpl.setDataSource(dataSource);
return jdbcTokenRepositoryImpl;
}
自定义登录页面
在src/main/webapp/WEB-INF/view文件夹中创建一个新页面,名为login.jsp。页面的主要部分包含username、password和rememberme字段,如下面的代码片段所示:
<form action='<spring:url value="/loginProc"/>' method="post">
<table>
<tr>
<td>Username</td>
<td><input type="text" name="username"></td>
</tr>
<tr>
<td>Password</td>
<td><input type="password" name="password"></td>
</tr>
<tr>
<td><input type="checkbox" name="rememberme"></td>
<td>Remember me</td>
</tr>
<tr>
<td><button type="submit">Login</button></td>
</tr>
</table>
</form>
确保您将记住我复选框的名称与您在 Spring 安全配置中指定的名称相同。
运行应用程序并进行测试
通过执行以下命令来运行项目:
mvn jetty:run
等待控制台打印[INFO] Started Jetty Server。
打开浏览器(我在测试时使用 Firefox 的隐私模式)并导航到http://localhost:8080,你将看到你创建的自定义登录页面,如下截图所示:

图 1: 自定义登录页面
输入user/user@password作为用户名和密码。点击Remember me并点击Login按钮,你将被导航到用户主页,如下所示:

图 2: 用户主页
查询你的 MySQL 数据库的persistent_logins表,你将看到一个新记录,如下截图所示:

图 3: MySQLWorkbench 查询新的 persistent_logins 表
现在,打开浏览器中的开发者工具并检查 cookies。根据你使用的浏览器,你应该看到类似于这样的东西:

图 4: 浏览器 cookie 设置以实现记住我功能
这个示例的整个项目可以在书的 GitHub 页面的jetty-db-basic-authentication-remember-me项目中找到。
会话管理
Spring Security 允许你只需一些配置就可以管理服务器上的会话。以下是一些最重要的会话管理活动:
- 会话创建: 这决定了何时需要创建会话以及您可以与之交互的方式。在 Spring Security 配置中,输入以下代码:
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.ALWAYS);
有四种会话创建策略可供选择。它们如下:
-
ALWAYS: 如果不存在会话,总是创建一个会话。
-
IF_REQUIRED: 如果需要,会创建一个会话。 -
NEVER: 永远不会创建会话;相反,它将使用已存在的会话。 -
无状态: 不会创建或使用会话。 -
invalidSession: 这控制着服务器检测到无效会话时如何通知用户:
http.sessionManagement().invalidSessionUrl("/invalidSession");
-
会话超时: 这控制着用户在会话过期时如何被通知。
-
并发会话: 这允许控制用户在应用程序中可以启动多少个会话。如果最大会话数设置为
1,当用户第二次登录时,先前的会话将被失效,用户将被注销。如果指定的值大于1,则允许用户同时拥有这么多会话:
http.sessionManagement().maximumSessions(1);
下面的截图显示了默认的错误屏幕,当同一用户创建了超过所配置的期望数量的会话时弹出:

图 5: 用户访问多个会话时抛出的错误
-
会话固定: 这与并发会话控制非常相似。此设置允许我们控制用户启动新会话时会发生什么。我们可以指定以下三个值:
-
migrateSession: 在成功认证后创建新会话时,旧会话将被失效,并且所有属性将被复制到新会话:
http.sessionManagement().sessionFixation().migrateSession();
newSession: 创建一个新会话,而不复制先前有效会话的任何属性:
http.sessionManagement().sessionFixation().newSession();
无: 旧会话被重用并且不会失效:
http.sessionManagement().sessionFixation().none();
CSRF
跨站请求伪造(CSRF)(www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF))是一种攻击,它迫使最终用户在当前已认证的 Web 应用程序上执行不需要的操作。CSRF 攻击专门针对改变状态的请求,而不是数据的窃取,因为攻击者无法看到伪造请求的响应。
开放 Web 应用安全项目(OWASP)认为 CSRF 是 Web 应用程序中最常见的安全风险之一。OWASP 每年发布一个名为 OWASP 十大的列表,突出显示困扰 Web 应用程序的前 10 个安全风险,它认为 CSRF 位列第五位。
在 Spring Security 中,默认情况下启用 CSRF。如果需要(我们已在许多示例中禁用了这一点,以便能够集中精力关注示例应传达的主要概念),我们可以通过在 Spring Security 配置中添加以下代码片段来显式禁用它:
http
.csrf().disable();
即使 CSRF 默认启用,但为了使其正常工作,每个请求都需要提供 CSRF 令牌。如果未将 CSRF 令牌发送到服务器,服务器将拒绝请求并抛出错误。如果您将Java 服务器页面(JSP)作为视图,只需包含隐藏输入,如下面的代码片段所示,许多事情都会自动发生:
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />
如果您使用 AJAX 请求调用服务器,可以以 HTTP 标头的形式提供 CSRF 令牌,而不是隐藏输入。您可以将与 CSRF 相关的标头声明为元标记,如下面的代码片段所示:
<head>
<meta name="_csrf" content="${_csrf.token}"/>
<meta name="_csrf_header" content="${_csrf.headerName}"/>
<!-- ... -->
</head>
之后,在调用服务器时,将这些(_csrf和_csrf_header)作为标头包含进去,您将被允许调用所需的端点。
如果您想要持久保存 CSRF 令牌,Spring Security 允许您通过调整配置来实现,如下面的代码片段所示:
http
.csrf()
.csrfTokenRepository(new CookieCsrfTokenRepository());
在执行此操作时,CSRF 令牌将作为 cookie 持久保存,服务器可以读取并验证(所有这些都是自动完成的)。
CSP
内容安全策略(CSP)(developer.mozilla.org/en-US/docs/Web/HTTP/CSP)是一种增加的安全层,有助于检测和缓解某些类型的攻击,包括跨站脚本(XSS)和数据注入攻击。这些攻击用于从数据窃取到网站损坏或恶意软件分发等各种用途。
在应用程序中进行适当的 CSP 设置可以处理内容注入漏洞,并是减少 XSS 的好方法。XSS 在 OWASP 十大中排名第二。
CSP 并非处理所有注入漏洞的解决方案,但可以用作减少注入攻击的工具之一。
CSP 是一种声明性策略,使用 HTTP 标头实现。它可以在应用程序中以两种模式运行:
-
生产模式(声明为 CSP)
-
仅报告模式(用于测试并声明为Content-Security-Policy-Report-Only)
CSP 包含一组安全策略指令,负责对 Web 资源施加适当的限制,然后在违规时相应地通知客户端(用户代理)。例如,以下安全策略片段从定义的受信任域加载脚本:
Content-Security-Policy: script-src https://trusted-domain.com
如果发生违规行为,用户代理将阻止它,如果策略指定了report-uri参数,如下例所示,它将以 JSON 的形式向该 URI 报告违规行为:
Content-Security-Policy: script-src https://trusted-domain.com; report-uri /csp-report-api/
前面的示例展示了 CSP 在生产模式下的工作。如果您想首先测试安全策略,然后在一段时间后将这些策略转为生产模式,CSP 提供了一种机制,如下面的代码片段所示:
Content-Security-Policy-Report-Only: script-src https://trusted-domain.com; report-uri /csp-report-api/
在仅报告模式下,当检测到违规行为时,报告将以 JSON 格式发布到report-uri,如下例所示:
{"csp-report":
{"document-uri":"...",
"violated-directive":"script-src https://trusted-domain.com",
"original-policy":"...",
"blocked-uri":"https://untrusted-domain.com"}
}
除了前面示例中详细介绍的安全指令之外,还有一些安全指令可在设置 CSP 时使用。有关完整的指令列表,请参阅content-security-policy.com/。
与 CSRF 令牌类似,CSP 也可以用于确保在访问服务器时特定资源包含一个令牌。以下示例显示了使用这种 nonce 方法:
Content-Security-Policy: script-src 'self' 'nonce-<cryptographically generated random string>'
与 CSRF 令牌类似,这个 nonce 必须在服务器中的任何资源访问中包含,并且在加载页面时必须新生成。
CSP 还允许您仅在资源与服务器期望的哈希匹配时加载资源。以下策略用于实现这一点:
Content-Security-Policy: script-src 'self' 'sha256-<base64 encoded hash>'
CSP 受到几乎所有现代浏览器的支持。即使某些浏览器不支持某些安全指令,其他支持的指令也可以正常工作。处理这个问题的最佳方式是通过解密用户代理,只发送浏览器肯定支持的安全指令,而不是在客户端上抛出错误。
使用 Spring Security 的 CSP
使用 Spring Security 配置 CSP 非常简单。默认情况下,CSP 是未启用的。您可以在 Spring Security 配置中启用它,如下所示:
http
.headers()
.contentSecurityPolicy("script-src 'self' https://trusted-domain.com; report-uri /csp-report-api/");
Spring Security 配置中的报告仅 CSP 如下:
http
.headers()
.contentSecurityPolicy("script-src 'self' https://trusted-domain.com; report-uri /csp-report-api/")
.reportOnly();
通道安全
除了身份验证和授权之外,Spring Security 还可以用于检查每个到达服务器的请求是否具有任何额外的属性。它可以检查协议(传输类型、HTTP 或 HTTPS)、某些 HTTP 头的存在等。SSL 现在是任何 Web 应用程序(或网站)遵守的事实标准,并且许多搜索引擎(例如 Google)甚至会对您的网站不使用 HTTPS 进行惩罚。SSL 用于保护从客户端到服务器以及反之的数据流通道。
Spring Security 可以配置为显式检查 URL 模式,并在使用 HTTP 协议访问时显式将用户重定向到 HTTPS。
这可以通过在 Spring Security 配置中配置适当的 URL 模式来轻松实现,如下所示:
http.authorizeRequests()
.requiresChannel().antMatchers("/httpsRequired/**").requiresSecure();
当用户访问/httpsRequired/**URL 模式并且协议是 HTTP 时,Spring Security 将用户重定向到相同的 URL,使用 HTTPS 协议。以下配置用于保护所有请求:
http.authorizeRequests()
.requiresChannel().anyRequest().requiresSecure();
要明确指定某些 URL 为不安全的,请使用以下代码:
.requiresChannel().antMatchers("/httpRequired/**").requiresInsecure();
以下代码片段显示了如何指定任何请求为 HTTP(不安全):
.requiresChannel().anyRequest().requiresInsecure();
CORS 支持
跨域资源共享(CORS)(developer.mozilla.org/en-US/docs/Web/HTTP/CORS)是一种机制,它使用额外的 HTTP 头来告诉浏览器,让一个在一个源(域)上运行的 Web 应用程序有权限访问来自不同源服务器的选定资源。当 Web 应用程序请求具有不同源(域、协议和端口)的资源时,它会发出跨源 HTTP 请求。
在本节中,我们不会创建完整的项目来解释 CORS 的工作原理。我们将使用代码片段,并解释每一部分代码,以便本节简洁明了。
根据以下代码片段更改 Spring Security 配置:
@EnableWebSecurity
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors();
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource urlCorsConfigSrc = new
UrlBasedCorsConfigurationSource();
urlCorsConfigSrc.registerCorsConfiguration("/**",
new CorsConfiguration().applyPermitDefaultValues());
return urlCorsConfigSrc;
}
}
在上述代码中,我们在 Spring Security 的configure方法中配置了 CORS。然后我们创建了一个新的 bean,corsConfigurationSource,在其中启用了*/***路径以供其他域访问。在许多情况下,这并不是真正理想的,下面的代码片段显示了更加强化的CorsConfiguration类:
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(new ArrayList<String>(Arrays.asList("*")));
configuration.setAllowedHeaders(new ArrayList<String>
(Arrays.asList("Authorization", "Cache-Control", "Content-Type")));
configuration.setAllowedMethods(new ArrayList<String>(Arrays.asList("HEAD",
"GET", "POST", "PUT", "DELETE", "PATCH")));
configuration.setAllowCredentials(true);
如果是 Spring MVC 应用程序,可以通过创建一个 bean 来指定 CORS 映射,如下所示:
@Configuration
public class SpringMVCConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedMethods("HEAD", "GET", "PUT", "POST", "DELETE",
"PATCH","OPTIONS");
}
};
}
}
我从第二章中复制了一个先前的示例,深入 Spring 安全,并在本章中创建了一个新项目,其中包含spring-boot-in-memory-basic-authentication-with-cors的完整源代码。我们在这里所做的是通过声明CorsConfigurationSource bean 来设置 CORS 全局配置。
加密模块
Spring Security Crypto 模块允许您进行密码编码、对称加密和密钥生成。该模块作为核心 Spring Security 提供的一部分捆绑在一起,不依赖于其他 Spring Security 代码。
密码编码
现代化的密码编码是 Spring Security 5 的新功能之一。Spring Security 的PasswordEncoder接口是其核心,它使用各种算法对密码进行单向哈希处理,然后可以安全地存储。Spring Security 支持多种密码编码算法:
-
BcryptPasswordEncoder:这使用 Bcrypt 强哈希函数。您可以选择性地提供强度参数(默认值为 10);值越高,哈希密码所需的工作量就越大。 -
Pbkdf2PasswordEncoder:这使用基于密码的密钥派生函数 2(PKDF2),具有可配置的迭代次数和 8 字节的随机盐值。 -
ScryptPasswordEncoder:这使用 Scrypt 哈希函数。在哈希过程中,客户端可以提供 CPU 成本参数、内存成本参数和并行化参数。当前实现使用 Bouncy Castle 库。
加密
Spring Security 的org.springframework.security.crypto.encrypt.Encryptors类有工厂方法,可以用来创建对称加密器。该类支持两种加密器:
-
BytesEncryptor:用于对原始字节数组形式的数据进行对称数据加密的服务接口。 -
TextEncryptor:用于对文本字符串进行对称数据加密的服务接口:

密钥生成
如前面加密部分所示,Spring Security 有一个类,即org.springframework.security.crypto.keygen.KeyGenerators,它有许多工厂方法,可以用来构造应用程序所需的许多密钥。
以下是支持的两种密钥生成器类型:
-
BytesKeyGenerator:用于生成唯一基于字节数组的密钥的生成器。 -
StringKeyGenerator:用于生成唯一字符串密钥的生成器:

图 7:BytesKeyGenerator 和 StringKeyGenerator 工厂方法
秘密管理
在应用程序中,我们需要处理各种形式的 API 密钥、其他应用程序密码等秘密/安全数据。通常情况下,对于部署和运行在生产环境中的应用程序,将这些数据以明文形式存储可能导致安全漏洞。如今,自动化技术变得非常便宜,对于现代应用程序来说,安全地存储这些数据并进行访问控制是必不可少的。
加密是被广泛接受的,但是对于解密,需要传播一个密钥,而这个密钥的传播通常是一个大问题。如果一个人决定将密钥带出组织,就会出现严重问题。
HashiCorp 的 Vault 是解决此问题的一个非常有力的竞争者,并且可以帮助轻松管理这些秘密,并具有非常严格的控制。它提供了基于设置策略的访问 API。它还具有提供访问控制的功能,并且还具有开箱即用的加密功能。此外,它具有各种持久后端支持,例如 Consul(来自 HashiCorp)等,使企业可以轻松采用它。Vault 是用 Go 编写的,并且可以在许多平台上下载到二进制文件,并且可以从其网站下载。在本节中,我们将快速介绍 Vault 产品本身,然后通过一个示例,我们将创建一个 Spring Boot 项目,并安全地访问 Vault 中存储的一些秘密。言归正传,让我们开始实际的代码。
从解封 Vault 开始
从 Vault 项目的网站(www.vaultproject.io/downloads.html)下载最新的二进制文件,根据您的操作系统进行安装。要启动 Vault,您需要一个文件vault.conf,其中我们将指定 Vault 启动所需的一些选项。以下是一个示例vault.conf文件,您可以使用:
backend "inmem" {
}
listener "tcp" {
address = "0.0.0.0:8200"
tls_disable = 1
}
disable_mlock = true
在vault.conf文件中,我们明确设置它将监听的地址,并且还禁用了 TLS/SSL(以便以纯文本模式运行)。
通过以下命令指定vault.conf文件的位置启动 Vault:
./vault server -config vault.conf
如下面的屏幕截图所示,Vault 以纯文本模式运行(已禁用 TLS/SSL):

图 8:启动和配置 Vault
打开一个新的命令提示符,我们现在将开始管理 Vault。通过执行以下命令设置一个环境变量,让客户端知道他们必须使用纯文本连接到 Vault(因为我们已禁用了 TLS/SSL):
export VAULT_ADDR=http://127.0.0.1:8200
之后,通过执行以下命令初始化 Vault 密钥生成:

图 9:初始化 Vault
我们使用的命令给了我们五个密钥份额和两个密钥阈值。重要的是要注意,一旦 Vault 被初始化,就无法更改这些值(输出仅显示一次)。务必收集必要的信息;否则,您将无法检索存储在 Vault 中的任何数据。如前面的屏幕截图所示,Vault 的init命令为我们提供了解封 Vault 所需的密钥和令牌。在我们可以使用 Vault 之前,必须对其进行解封。
解封 (www.vaultproject.io/docs/concepts/seal.html) 是构建读取解密密钥以解密数据所必需的主密钥的过程,从而允许访问 Vault。在解封之前,几乎无法对 Vault 进行任何操作。
您可以通过执行以下命令并提供在 Vault 初始化过程中生成的任何密钥来解封 Vault:
./vault unseal <any key generated using initialization>
以下屏幕截图显示了上述命令的成功执行:

图 10:解封 Vault
一旦解封,您的 Vault 现在已准备好存储您可能想在应用程序中使用的秘密数据。
成功解封 Vault 后,要存储任何数据,您首先需要进行身份验证。当我们初始化 Vault 时,会显示一个令牌(在屏幕上),此令牌用于进行身份验证。使用此令牌进行身份验证的最简单方法之一是设置一个新的环境变量(VAULT_TOKEN)。执行以下命令,当 Vault 启动时,它将使用此环境变量并进行身份验证:
export VAULT_TOKEN=ee60f275-7b16-48ea-0e74-dc48b4b3729c
执行上述命令后,现在可以通过执行以下命令编写您的秘密:
./vault write secret/movie-application password=randomstring
输入命令后,您应该收到以下输出:

图 11:将秘密写入 Vault
令牌是 Vault 中进行身份验证的主要方式。除此之外,还有其他机制,如 LDAP 和用户名/密码,可以进行身份验证。
Spring Boot 项目
Spring 有一个专门的模块,称为 Spring Cloud Vault,可以轻松地在应用程序中使用 Vault。Spring Cloud Vault 非常易于使用,我们将在本节中介绍如何使用它。
Spring Cloud Vault Config(cloud.spring.io/spring-cloud-vault/)为分布式系统中的外部化配置提供了客户端支持。使用 HashiCorp 的 Vault,您可以管理应用程序在所有环境中的外部秘密属性的中心位置。Vault 可以管理静态和动态秘密,如远程应用程序/资源的用户名/密码,并为 MySQL、PostgreSQL、Apache Cassandra、MongoDB、Consul、AWS 等外部服务提供凭据。
我们将使用 Spring Boot 项目(使用 Spring Initializr 生成,start.spring.io)。在应用程序启动时,Vault 会启动并获取所有的秘密:

图 12:创建一个空的 Spring Initializr 项目
通过执行以下命令解压下载的 Spring Initializr 项目:
unzip -a spring-boot-spring-cloud-vault.zip
在您喜欢的 IDE 中导入项目(我在使用 IntelliJ)。
Maven 依赖项
确保您的项目的pom.xml中添加了以下 Maven 依赖项:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-vault-config</artifactId>
<version>2.0.0.RELEASE</version>
</dependency>
当 Spring Boot 项目启动时,如果 Vault 服务器运行在端口8200上,它将选择默认的 Vault 配置。如果您想自定义这些属性,可以指定bootstrap.yml或bootstrap.properties。在我们的示例中,我们将明确设置bootstrap.yml文件,内容如下:
spring:
application:
name: movie-application
spring.cloud.vault:
host: localhost # hostname of vault server
port: 8200 # vault server port
scheme: http # connection scheme http or https
uri: http://localhost:8200 # vault endpoint
connection-timeout: 10000 # connection timeout in milliseconds
read-timeout: 5000 # read timeout in milliseconds
config:
order: -10 # order for property source
token: ee60f275-7b16-48ea-0e74-dc48b4b3729c
health.vault.enabled: true # health endpoint enabled using spring actuator
我们将使用 HTTP 方案,因为我们以纯文本模式启动了 Vault。如果您想使用 HTTPS,这也很容易做到,因为大多数操作都是通过提供的脚本完成的。这是 Vault 运行的默认方案,在生产设置中必须是这样。在实现实际用例时,让我们先了解这个概念,然后再深入一点。
如果您想在 HTTPS 方案中运行 Vault,Spring Cloud Vault 在其源代码的src/test/bash目录下提供了许多脚本(github.com/spring-cloud/spring-cloud-vault/tree/master/src/test/bash),可以用于创建必要的证书,然后在此方案下运行 Vault。为了简洁起见,我们不会在这里详细介绍这个方面。
在.yml文件中,我们使用了作为 Vault 初始化的一部分创建的根令牌。如果需要,可以通过执行以下命令获取新令牌:
./vault token create
以下截图显示了token create命令的成功执行:

图 13:创建新的 Vault 令牌
在您的 Spring Boot 项目中,在应用程序运行类SpringBootSpringCloudVaultApplication中添加以下代码片段:
@Value("${password}")
String password;
@PostConstruct
private void postConstruct() {
System.out.println("Secret in Movie application password is: " + password);
}
在此代码中,password字段将由 Spring Cloud Vault 填充,如果您运行应用程序(使用命令mvn spring-boot:run),您应该看到 Spring Cloud Vault 连接到运行的 Vault(使用bootstrap.yml文件中的配置)并检索我们为movie-application写入 Vault 的值。
这结束了我们对使用 Spring Boot 和 Spring Cloud Vault 的基本应用程序的介绍。您可以在本书的 GitHub 页面中的本章项目中查看完整的源代码,名称为spring-boot-spring-cloud-vault。
HTTP 数据完整性验证器
Spring Security 帮助我们通过简单的方式和最少的代码来丰富我们的应用程序的常见安全功能。然而,Spring Security 正在逐渐赶上现代应用程序中需要的许多额外安全功能。这些应用程序大多部署在云上,并且每天都有大量的变更推送到生产环境。HTTP 数据完整性验证器(HDIV)是一个可以用来进一步丰富您的应用程序安全性的产品。
什么是 HDIV?
HDIV 最初是作为一个开源项目诞生的,当时由 Roberto Velasco、Gotzon Illarramendi 和 Gorka Vicente 开发,以应对在生产环境中检测到的安全问题。第一个稳定版本 1.0 于 2008 年发布,以安全库的形式集成到 Web 应用程序中。2011 年,HDIV 正式与 Spring MVC 集成,这是最常用的 Java 解决方案,用于 Web 应用程序开发。2012 年,HDIV 与 Grails 集成。2015 年,HDIV 被包含在 Spring Framework 官方文档中,作为与 Web 安全相关的解决方案。基于全球的兴趣和对高市场需求的回应,创始人成立了HDIV Security(hdivsecurity.com/)公司,并于 2016 年推出了 HDIV 的商业版本。HDIV 解决方案在开发过程中构建到应用程序中,以提供最强大的运行时应用程序自我保护(RASP)来抵御 OWASP 十大威胁。
HDIV 诞生的目的是保护应用程序免受参数篡改攻击。它的第一个目的(从首字母缩写来看)是保证服务器生成的所有数据的完整性(不进行数据修改)。HDIV 通过添加安全功能来扩展 Web 应用程序的行为,同时保持 API 和框架规范。HDIV 逐渐增加了 CSRF、SQL 注入(SQLi)和 XSS 保护等功能,从而提供了更高的安全性,不仅仅是一个 HTTP 数据完整性验证器。
攻击成本越来越低,自动化程度越来越高。手动安全测试变得成本高昂。Spring Security 通过轻松实现最重要的安全方面(如身份验证和授权)来保护应用程序,但不能保护应用程序代码中常见的安全漏洞和设计缺陷。这就是集成已经使用 Spring Security 进行保护的 Spring 应用程序可以引入 HDIV 的地方。我们将通过一个非常简单的示例来展示 HDIV 的一些亮点。以下是他们网站上详细介绍的一些优势:
-
HDIV 在利用源代码之前检测安全漏洞,使用运行时数据流技术报告漏洞的文件和行号。开发人员在开发过程中可以立即在 Web 浏览器或集中式 Web 控制台中进行报告。
-
它可以保护免受业务逻辑缺陷的影响,无需学习应用程序,并提供检测和保护免受安全漏洞的影响,而无需更改源代码。
-
HDIV 使得渗透测试工具(Burp Suite)与应用程序之间的集成成为可能,向渗透测试人员传递有价值的信息。它避免了许多手工编码的步骤,将渗透测试人员的注意力和努力集中在最脆弱的入口点上。
有关更多信息,您可以查看以下链接:hdivsecurity.com/。
让我们开始构建一个简单的示例,展示 HDIV 在保护应用程序中链接和表单数据方面的保护。
Bootstrap 项目
我们将使用通过 Spring Initializr 创建的基础项目来创建我们的 HDIV 示例,如下所示:

图 14:基本的 Spring Initializr 项目设置
Maven 依赖项
在以下代码中,我们正在调用我们需要作为项目一部分的显式依赖项,即 HDIV:
<!--HDIV dependency-->
<dependency>
<groupId>org.hdiv</groupId>
<artifactId>spring-boot-starter-hdiv-thymeleaf</artifactId>
<version>1.3.1</version>
<type>pom</type>
</dependency>
HDIV 支持多种 Web 应用程序框架。在我们的示例中,我们将使用 Spring MVC 以及 Thymeleaf 和上述依赖项来处理这个问题。
Spring Security 配置
到目前为止,您已经知道 Spring Security 配置文件中包含什么。我们将进行内存身份验证,并配置两个用户(与本书中一直在做的类似)。我们将进行基于表单的登录,并将创建自己的登录页面。
Spring MVC 配置
到目前为止,我们一直在看的 Spring MVC 配置非常基本。这里没有什么值得特别提及的。我们只需要确保附加到登录页面的控制器被明确定义。
HDIV 配置
这个神奇的类将在不太麻烦的情况下为您的应用程序带来 HDIV 功能。完整的类如下所示:
@Configuration
@EnableHdivWebSecurity
public class HdivSecurityConfig extends HdivWebSecurityConfigurerAdapter {
@Override
public void addExclusions(final ExclusionRegistry registry) {
registry.addUrlExclusions("/login");
}
}
这个类的重要工作是由我们正在扩展的类HdivWebSecurityConfigurerAdapter完成。此外,@EnableHdivWebSecurity注解确保大部分设置会自动处理。我们只需要确保我们的登录页面 URL 的配置通过覆盖addExclusions方法来排除 HDIV 安全。
模型类
我们将使用本书中一直在使用的相同模型类Movie。为了简化编码,我们将使用 Lombok 库,该库通过查看类中配置的各种注释来完成所有魔术。
控制器类
我们只会有一个控制器类,我们将在这个示例中映射所有要创建的页面。为展示 HDIV 的功能,我们将看到 HDIV 在两种情况下的运行:
-
一个电影创建页面(电影 bean),显示在包含表单的页面中 HDIV 的工作
-
一个显示 HDIV 拦截并在有人操纵实际链接时抛出错误的链接页面
该类非常简单,这里不需要详细说明。
页面
如前所述,我们将在我们的示例中创建以下页面:
-
login.html:我们将用于用户登录应用程序的自定义登录页面 -
main.html:成功登录后用户导航到的页面,包含指向电影创建和链接页面的链接 -
links.html:用户单击链接 URL 时导航到的页面 -
movie.html:电影创建页面,包含两个字段——标题和类型
运行应用程序
通过执行以下命令运行应用程序,就像运行任何其他 Spring Boot 项目一样:
mvn spring-boot:run
转到浏览器并导航到http://localhost:8080,您将看到一个登录页面,如下所示:

图 15:登录页面
如前面的截图所示,输入用户名/密码并单击“登录”按钮,您将被导航到主页:

图 16:成功登录后呈现给用户的主页
单击链接导航到创建新电影的页面。您将被导航到以下截图中显示的页面。仔细观察 URL,您将看到已添加新的查询参数_HDIV_STATE_。服务器通过查看该值来验证并确保提交的表单是真实的:

图 17:创建电影屏幕,展示 HDIV_STATE 查询字符串
现在返回主页并单击链接页面。您将被导航到以下页面:

图 18:链接页面,显示 HDIV_STATE 查询字符串
如页面所述,尝试操纵链接(更改_HDIV_STATE_值),您将被带到 HDIV 错误页面:

图 19:HDIV 错误页面,在错误条件下显示
此示例展示了 HDIV 在与 Spring Security 一起工作时显示其价值的两种情况。有关更多详细信息,请查看 HDIV 网站和文档,网址如下:
自定义 DSL
Spring Security 允许您编写自己的领域特定语言(DSL),该语言可用于配置应用程序中的安全性。当我们使用 OKTA 实现 SAML 身份验证时,我们已经看到了自定义 DSL 的实际应用。我们使用了由 OKTA 提供的自定义 DSL 来配置 Spring Security。
要编写自己的自定义 DSL,您可以扩展AbstractHttpConfigurer 类并覆盖其中的一些方法,如下所示:
public class CustomDSL extends AbstractHttpConfigurer<CustomDSL, HttpSecurity> {
@Override
public void init(HttpSecurity builder) throws Exception {
// Any configurations that you would like to do (say as default) can be
configured here
}
@Override
public void configure(HttpSecurity builder) throws Exception {
// Can add anything specific to your application and this will be honored
}
}
在您的 Spring Security 配置类(configure 方法)中,您可以使用自定义 DSL,如下所示:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.apply(<invoke custom DSL>)
...;
}
当 Spring Security 看到自定义 DSL 设置时,代码的执行顺序如下:
-
调用 Spring Security 配置类的
configure方法 -
调用自定义 DSL 的
init方法 -
调用自定义 DSL 的
configure方法
Spring Security 使用这种方法来实现authorizeRequests()。
摘要
本章向您介绍了 Spring Security 的一些其他功能,这些功能可以在您的应用程序中使用。通过示例,我们介绍了如何在应用程序中实现记住我功能。我们还简要涉及了 CSRF、CORS、CSP、通道安全和会话管理等概念。我们还简要介绍了 Spring Security 中的加密模块。
我们通过介绍了两种可以与 Spring Security 一起使用的产品来结束了本章——HashiCorp Vault(用于秘密管理)和 HDIV(用于附加安全功能)。
阅读完本章后,您应该清楚地了解了一些可以使用 Spring Security 实现的附加功能。您还应该对可以与 Spring Security 一起使用以实现现代应用程序所需的一些最重要的技术功能有很好的理解。
现在,如果您正在阅读本章,那么请为自己鼓掌,因为通过本章,我们完成了本书。我希望您已经享受了本书的每一部分,并且希望您已经学到了一些可以用于创建精彩和创新的新应用程序的新知识。
谢谢阅读!
1019

被折叠的 条评论
为什么被折叠?



