领先一步
VMware 提供培训和认证,以加快您的进度。
了解更多注意:此博客的源代码和测试仍在不断发展,但此处未维护文本的更改。请参阅教程版本以获取最新的内容。
在本文中,我们将继续讨论如何在“单页应用程序”中将Spring Security与AngularJS一起使用。在这里,我们将展示如何构建一个API网关来控制使用Spring Cloud对后端资源的身份验证和访问。这是系列文章的第四篇,您可以阅读第一篇文章来了解应用程序的基本构建块或从头开始构建它,或者您可以直接访问Github上的源代码。在上一篇文章中,我们构建了一个简单的分布式应用程序,该应用程序使用Spring Session来验证后端资源。在这篇文章中,我们将UI服务器转换为后端资源服务器的反向代理,修复了上一个实现的问题(自定义令牌身份验证带来的技术复杂性),并为我们提供了许多控制浏览器客户端访问的新选项。
提醒:如果您正在使用示例应用程序阅读本文,请务必清除浏览器缓存中的cookie和HTTP Basic凭据。在Chrome中,为单个服务器执行此操作的最佳方法是打开一个新的隐身窗口。
API 网关是前端客户端的单一入口点(和控制点),可以是基于浏览器的(如本文中的示例)或移动端的。客户端只需要知道一个服务器的 URL,而无需更改后端即可随意重构,这是一个显著的优势。在集中化和控制方面还有其他优势:速率限制、身份验证、审计和日志记录。使用Spring Cloud实现简单的反向代理非常简单。
如果您一直在关注代码,您会知道上一篇文章结尾处的应用程序实现有点复杂,因此不是一个很好的迭代起点。但是,有一个中间点我们可以更容易地开始,那时后端资源尚未使用 Spring Security 保护。此代码的源代码是一个单独的项目在Github上,所以我们将从那里开始。它有一个UI服务器和一个资源服务器,它们相互通信。资源服务器还没有 Spring Security,所以我们可以先让系统运行起来,然后再添加这一层。
为了将其转换为 API 网关,UI 服务器需要一个小的调整。在 Spring 配置中的某个位置,我们需要添加一个@EnableZuulProxy
注解,例如在主要的(唯一的)应用程序类中
@SpringBootApplication
@RestController
@EnableZuulProxy
public class UiApplication {
...
}
并且在一个外部配置文件中,我们需要将 UI 服务器中的本地资源映射到外部配置(“application.yml”)中的远程资源
security:
...
zuul:
routes:
resource:
path: /resource/**
url: https://127.0.0.1:9000
这意味着“将此服务器中模式为 /resource/** 的路径映射到 localhost:9000 远程服务器中的相同路径”。简单而有效(好吧,包括 YAML 在内是 6 行,但您并不总是需要这些)!
要使它工作,我们只需要类路径上的正确内容。为此,我们在 Maven POM 中添加了几行新代码
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-parent</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
...
</dependencies>
注意使用了“spring-cloud-starter-zuul”——它是一个启动器 POM,就像 Spring Boot 启动器一样,但它控制了我们为此 Zuul 代理所需的相关依赖项。我们还使用<dependencyManagement>
,因为我们希望能够依赖所有传递依赖项的正确版本。
有了这些更改,我们的应用程序仍然可以工作,但是在我们修改客户端之前,我们实际上还没有使用新的代理。幸运的是,这很简单。我们只需要将“home”控制器的这个实现
angular.module('hello', [ 'ngRoute' ])
...
.controller('home', function($scope, $http) {
$http.get('https://127.0.0.1:9000/').success(function(data) {
$scope.greeting = data;
})
});
更改为本地资源
angular.module('hello', [ 'ngRoute' ])
...
.controller('home', function($scope, $http) {
$http.get('resource/').success(function(data) {
$scope.greeting = data;
})
});
现在,当我们启动服务器时,一切正常,请求通过 UI(API 网关)代理到资源服务器。
更好的是:我们不再需要资源服务器中的 CORS 过滤器。无论如何,我们很快就将它组合在一起,并且应该意识到我们必须手动执行任何技术性强的操作(尤其是在涉及安全性的情况下)是一个危险信号。幸运的是,它现在是多余的,所以我们可以直接丢弃它,晚上安心睡觉!
您可能还记得,在我们开始的中间状态下,资源服务器没有安全措施。
旁注:如果您的网络架构反映了应用程序架构(您可以只使资源服务器在物理上对除 UI 服务器以外的任何人不可访问),那么缺乏软件安全性甚至可能不是问题。作为对此的简单演示,我们可以使资源服务器仅在本地主机上可访问。只需将此添加到资源服务器中的
application.properties
server.address: 127.0.0.1
哇,太容易了!使用仅在您的数据中心可见的网络地址执行此操作,您就拥有一个适用于所有资源服务器和所有用户桌面的安全解决方案。
假设我们决定确实需要软件级别的安全性(由于多种原因,这很可能)。这不会成为问题,因为我们只需要将 Spring Security 添加为依赖项(在资源服务器POM中)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
这足以让我们获得一个安全的资源服务器,但它还无法让我们获得一个工作的应用程序,原因与第三部分相同:两个服务器之间没有共享身份验证状态。
我们可以使用与上一篇文章中相同的机制来共享身份验证(和 CSRF)状态,即Spring Session。我们像以前一样将依赖项添加到两个服务器
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session</artifactId>
<version>1.0.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
</dependency>
但是这一次配置要简单得多,因为我们只需要将相同的Filter
声明添加到两者中。首先是 UI 服务器(添加@EnableRedisHttpSession
)
@SpringBootApplication
@RestController
@EnableZuulProxy
@EnableRedisHttpSession
public class UiApplication {
...
}
然后是资源服务器。需要进行三个小的更改:一个是将@EnableRedisHttpSession
添加到ResourceApplication
@SpringBootApplication
@RestController
@EnableRedisHttpSession
class ResourceApplication {
...
}
另一个是在资源服务器中显式禁用 HTTP Basic(以防止浏览器弹出身份验证对话框)
@SpringBootApplication
@RestController
@EnableRedisHttpSession
class ResourceApplication extends WebSecurityConfigurerAdapter {
...
@Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic().disable()
http.authorizeRequests().anyRequest().authenticated()
}
}
旁注:另一种方法也可以防止身份验证对话框,那就是保留 HTTP Basic,但将 401 挑战更改为除“Basic”以外的其他内容。你可以在 `HttpSecurity` 配置回调中使用一行代码实现 `AuthenticationEntryPoint` 来做到这一点。
最后一个方法是在 `application.properties` 中显式请求非无状态会话创建策略。
security.sessions: NEVER
只要 redis 仍在后台运行(如果你想启动它,可以使用 fig.yml
),系统就能正常工作。在 https://127.0.0.1:8080 加载 UI 的主页,登录后,你将看到在主页上渲染的来自后端的邮件。
幕后发生了什么?首先,我们可以查看 UI 服务器(和 API 网关)中的 HTTP 请求。
方法 | 路径 | 状态 | 响应 |
---|---|---|---|
GET | / | 200 | index.html |
GET | /css/angular-bootstrap.css | 200 | Twitter Bootstrap CSS |
GET | /js/angular-bootstrap.js | 200 | Bootstrap 和 Angular JS |
GET | /js/hello.js | 200 | 应用程序逻辑 |
GET | /user | 302 | 重定向到登录页面 |
GET | /login | 200 | 白标登录页面(忽略) |
GET | /resource | 302 | 重定向到登录页面 |
GET | /login | 200 | 白标登录页面(忽略) |
GET | /login.html | 200 | Angular 登录表单部分 |
POST | /login | 302 | 重定向到主页(忽略) |
GET | /user | 200 | JSON 身份验证用户 |
GET | /resource | 200 | (代理)JSON 问候 |
这与 第二部分结尾的序列相同,只是 Cookie 名称略有不同(“SESSION”而不是“JSESSIONID”),因为我们使用的是 Spring Session。但架构不同,对“/resource”的最后一个请求很特殊,因为它被代理到了资源服务器。
我们可以通过查看 UI 服务器中的“/trace”端点(来自 Spring Boot Actuator,我们使用 Spring Cloud 依赖项添加了它)来查看反向代理的运行情况。在新浏览器中访问 https://127.0.0.1:8080/trace,然后滚动到底部(如果你还没有,请为你的浏览器获取一个 JSON 插件,以便使其易于阅读)。你需要使用 HTTP Basic 进行身份验证(浏览器弹出窗口),但与你的登录表单相同的凭据有效。在结尾处或附近,你应该看到一对类似这样的请求
注意:尝试使用不同的浏览器,以避免身份验证交叉(例如,如果使用 Chrome 测试 UI,则使用 Firefox),这不会阻止应用程序运行,但如果跟踪包含来自同一浏览器的混合身份验证,则会使跟踪更难以阅读。
{
"timestamp": 1420558194546,
"info": {
"method": "GET",
"path": "/",
"query": ""
"remote": true,
"proxy": "resource",
"headers": {
"request": {
"accept": "application/json, text/plain, */*",
"x-xsrf-token": "542c7005-309c-4f50-8a1d-d6c74afe8260",
"cookie": "SESSION=c18846b5-f805-4679-9820-cd13bd83be67; XSRF-TOKEN=542c7005-309c-4f50-8a1d-d6c74afe8260",
"x-forwarded-prefix": "/resource",
"x-forwarded-host": "localhost:8080"
},
"response": {
"Content-Type": "application/json;charset=UTF-8",
"status": "200"
}
},
}
},
{
"timestamp": 1420558200232,
"info": {
"method": "GET",
"path": "/resource/",
"headers": {
"request": {
"host": "localhost:8080",
"accept": "application/json, text/plain, */*",
"x-xsrf-token": "542c7005-309c-4f50-8a1d-d6c74afe8260",
"cookie": "SESSION=c18846b5-f805-4679-9820-cd13bd83be67; XSRF-TOKEN=542c7005-309c-4f50-8a1d-d6c74afe8260"
},
"response": {
"Content-Type": "application/json;charset=UTF-8",
"status": "200"
}
}
}
},
第二个条目是客户端向网关发出对“/resource”的请求,你可以看到 Cookie(由浏览器添加)和 CSRF 标头(如 第二部分中所述,由 Angular 添加)。第一个条目有 `remote: true`,这意味着它正在跟踪对资源服务器的调用。你可以看到它发出到 uri 路径“/”,并且你可以看到(至关重要的是)Cookie 和 CSRF 标头也被发送了。如果没有 Spring Session,这些标头对资源服务器毫无意义,但是按照我们的设置方式,它现在可以使用这些标头来重新构建具有身份验证和 CSRF 令牌数据的会话。因此请求被允许,我们成功了!
我们在本文中涵盖了很多内容,但我们最终得到了一个很好的结果:两个服务器中的样板代码最少,它们都非常安全,并且用户体验没有受到影响。仅此一点就是使用 API 网关模式的理由,但实际上,我们只是触及了 API 网关可能用于的用途的表面(Netflix 将其用于 很多事情)。阅读有关 Spring Cloud 的信息,了解如何轻松地向网关添加更多功能。本系列的 下一篇文章 将通过将身份验证职责提取到单独的服务器(单点登录模式)来扩展应用程序架构。