Spring Security和Angular教程

编程知识 更新时间:2023-04-20 13:11:02

Spring Security和Angular教程

(一)安全的单页应用程序

在本教程中,我们展示了Spring Security,Spring Boot和Angular的一些很好的功能,它们协同工作以提供愉快和安全的用户体验。Spring和Angular的初学者应该可以访问它,但是也有很多细节可供专家使用。这实际上是Spring Security和Angular系列部分中的第一部分,其中每个部分都依次公开了新功能。我们将在第二部分和后续部分中改进应用程序,但此后的主要更改是体系结构而非功能性。

Spring和单页应用程序

HTML5,丰富的基于浏览器的功能和“单页面应用程序”是现代开发人员非常有价值的工具,但任何有意义的交互都将涉及后端服务器,以及静态内容(HTML,CSS和JavaScript),我们将采用需要一个后端服务器。后端服务器可以扮演任何或所有角色:提供静态内容,有时(但现在不常见)渲染动态HTML,验证用户,保护对受保护资源的访问,以及(最后但并非最不重要)与JavaScript交互在浏览器中通过HTTP和JSON(有时称为REST API)。

Spring一直是构建后端功能的流行技术(特别是在企业中),随着Spring Boot的出现,事情变得前所未有的简单。让我们看看如何使用Spring Boot,Angular和Twitter Bootstrap从零开始构建新的单页面应用程序。没有特别的理由选择那个特定的堆栈,但它很受欢迎,特别是在企业Java商店的核心Spring选区,所以这是一个有价值的起点。

创建一个新项目

我们将逐步详细地创建这个应用程序,这样任何不完全使用Spring和Angular的人都可以关注正在发生的事情。如果您希望切换到追逐,您可以跳到应用程序正在运行的末尾,并查看它们如何组合在一起。创建新项目有多种选择:

  • 在命令行上使用curl

  • 使用Spring Boot CLI

  • 使用Spring Initializr网站

  • 使用Spring Tool Suite

我们要构建的完整项目的源代码在Github中,所以你可以克隆项目并直接从那里开始工作。然后跳到下一部分。

使用卷曲

创建新项目以开始的最简单方法是通过Spring Boot Initializr。例如在类似UN * X的系统上使用curl:

<span style="color:#34302d"><span style="color:#333333"><code><span style="color:#000000">$ mkdir ui </span><span style="color:#666600">&&</span><span style="color:#000000"> cd ui
$ curl https</span><span style="color:#666600">:</span><span style="color:#880000">//start.spring.io/starter.tgz -d style=web \</span>
<span style="color:#666600">-</span><span style="color:#000000">d style</span><span style="color:#666600">=</span><span style="color:#000000">security </span><span style="color:#666600">-</span><span style="color:#000000">d name</span><span style="color:#666600">=</span><span style="color:#000000">ui </span><span style="color:#666600">|</span><span style="color:#000000"> tar </span><span style="color:#666600">-</span><span style="color:#000000">xzvf </span><span style="color:#666600">-</span></code></span></span>

然后,您可以将该项目(默认情况下是普通的Maven Java项目)导入您喜欢的IDE,或者只使用命令行中的文件和“mvn”。然后跳到下一部分。

使用Spring Boot CLI

您可以使用Spring Boot CLI创建相同的项目,如下所示:

<span style="color:#34302d"><span style="color:#333333"><code><span style="color:#000000">$ spring init </span><span style="color:#666600">--</span><span style="color:#000000">dependencies web</span><span style="color:#666600">,</span><span style="color:#000000">security ui</span><span style="color:#666600">/</span> <span style="color:#666600">&&</span><span style="color:#000000"> cd ui</span></code></span></span>

然后跳到下一部分。

使用Initializr网站

如果您愿意,也可以直接从Spring Boot Initializr获取与.zip文件相同的代码。只需在浏览器中打开它并选择依赖项“Web”和“安全性”,然后单击“生成项目”。.zip文件在根目录中包含标准Maven或Gradle项目,因此您可能需要在解压缩之前创建一个空目录。然后跳到下一部分。

使用Spring Tool Suite

在Spring Tool Suite(一组Eclipse插件)中,您还可以使用向导创建和导入项目File->New->Spring Starter Project。然后跳到下一部分。IntelliJ IDEA和NetBeans具有类似的功能。

添加角度应用程序

如今,Angular(或任何现代前端框架)中的单页面应用程序的核心将是Node.js构建。Angular有一些工具可以快速设置它,所以让我们使用它们,并保留使用Maven构建的选项,就像任何其他Spring Boot应用程序一样。有关如何设置Angular应用程序的详细信息将在其他地方介绍,或者您可以从github查看本教程的代码。

运行应用程序

一旦Angular应用程序启动,您的应用程序将可以在浏览器中加载(即使它还没有做太多)。在命令行上,您可以执行此操作

<span style="color:#34302d"><span style="color:#333333"><code><span style="color:#000000">$ mvn spring</span><span style="color:#666600">-</span><span style="color:#000000">boot</span><span style="color:#666600">:</span><span style="color:#000000">run</span></code></span></span>

并转到http:// localhost:8080的浏览器。当您加载主页时,您应该获得一个浏览器对话框,询问用户名和密码(用户名是“user”,密码在启动时在控制台日志中打印)。实际上还没有任何内容(或者可能是来自ngCLI 的默认“英雄”教程内容),所以你应该基本上得到一个空白页面。

 如果您不喜欢在控制台日志中输入密码,只需将其添加到“application.properties”(在“src / main / resources”中):( security.user.password=password并选择您自己的密码)。我们使用“application.yml”在示例代码中完成了此操作。

在IDE中,只需main()在应用程序类中运行该方法(只有一个类,UiApplication如果使用上面的“curl”命令则调用它)。

要打包并作为独立的JAR运行,您可以这样做:

<span style="color:#34302d"><span style="color:#333333"><code><span style="color:#000000">$ mvn </span><span style="color:#000088">package</span><span style="color:#000000">
$ java </span><span style="color:#666600">-</span><span style="color:#000000">jar target</span><span style="color:#880000">/*.jar</span></code></span></span>

自定义角度应用程序

让我们自定义“app-root”组件(在“src / app / appponent.ts”中)。

最小的Angular应用程序如下所示:

appponent.ts

<span style="color:#34302d"><span style="color:#333333"><code class="language-javascript"><span style="color:#000088">import</span> <span style="color:#666600">{</span> <span style="color:#660066">Component</span> <span style="color:#666600">}</span> <span style="color:#000088">from</span> <span style="color:#008800">'@angular/core'</span><span style="color:#666600">;</span>

<span style="color:#006666">@Component</span><span style="color:#666600">({</span><span style="color:#000000">
  selector</span><span style="color:#666600">:</span> <span style="color:#008800">'app-root'</span><span style="color:#666600">,</span><span style="color:#000000">
  templateUrl</span><span style="color:#666600">:</span> <span style="color:#008800">'./appponent.html'</span><span style="color:#666600">,</span><span style="color:#000000">
  styleUrls</span><span style="color:#666600">:</span> <span style="color:#666600">[</span><span style="color:#008800">'./appponent.css'</span><span style="color:#666600">]</span>
<span style="color:#666600">})</span>
<span style="color:#000088">export</span> <span style="color:#000088">class</span> <span style="color:#660066">AppComponent</span> <span style="color:#666600">{</span><span style="color:#000000">
  title </span><span style="color:#666600">=</span> <span style="color:#008800">'Demo'</span><span style="color:#666600">;</span><span style="color:#000000">
  greeting </span><span style="color:#666600">=</span> <span style="color:#666600">{</span><span style="color:#008800">'id'</span><span style="color:#666600">:</span> <span style="color:#008800">'XXX'</span><span style="color:#666600">,</span> <span style="color:#008800">'content'</span><span style="color:#666600">:</span> <span style="color:#008800">'Hello World'</span><span style="color:#666600">};</span>
<span style="color:#666600">}</span></code></span></span>

此TypeScript中的大多数代码都是锅炉板。有趣的东西都将出现在AppComponent我们定义“selector”(HTML元素的名称)的地方,以及通过@Component注释呈现的HTML片段。我们还需要编辑HTML模板(“appponent.html”):

appponent.html

<span style="color:#34302d"><span style="color:#333333"><code class="language-html"><span style="color:#000088"><div</span> <span style="color:#660066">style</span><span style="color:#666600">=</span><span style="color:#008800">"</span><span style="color:#000000">text</span><span style="color:#666600">-</span><span style="color:#000000">align</span><span style="color:#666600">:</span><span style="color:#000000">center</span><span style="color:#008800">"</span><span style="color:#660066">class</span><span style="color:#666600">=</span><span style="color:#008800">"container"</span><span style="color:#000088">></span>
  <span style="color:#000088"><h1></span><span style="color:#000000">
    Welcome {{title}}!
  </span><span style="color:#000088"></h1></span>
  <span style="color:#000088"><div</span> <span style="color:#660066">class</span><span style="color:#666600">=</span><span style="color:#008800">"container"</span><span style="color:#000088">></span>
    <span style="color:#000088"><p></span><span style="color:#000000">Id: </span><span style="color:#000088"><span></span><span style="color:#000000">{{greeting.id}}</span><span style="color:#000088"></span></p></span>
    <span style="color:#000088"><p></span><span style="color:#000000">Message: </span><span style="color:#000088"><span></span><span style="color:#000000">{{greeting.content}}!</span><span style="color:#000088"></span></p></span>
  <span style="color:#000088"></div></span>
<span style="color:#000088"></div></span></code></span></span>

如果您在“src / app”下添加了这些文件并重建了应用程序,它现在应该是安全且实用的,它会说“Hello World!”。在greeting由角在HTML中使用车把呈现占位符,{{greeting.id}}{{greeting.content}}

添加动态内容

到目前为止,我们有一个带有硬编码问候语的应用程序。这对于了解事物是如何组合在一起很有用,但实际上我们希望内容来自后端服务器,所以让我们创建一个HTTP端点,我们可以使用它来获取问候语。在您的应用程序类中(在“src / main / java / demo”中),添加@RestController注释并定义一个新的@RequestMapping

UiApplication.java

<span style="color:#34302d"><span style="color:#333333"><code class="language-java"><span style="color:#006666">@SpringBootApplication</span>
<span style="color:#006666">@RestController</span>
<span style="color:#000088">public</span> <span style="color:#000088">class</span> <span style="color:#660066">UiApplication</span> <span style="color:#666600">{</span>

  <span style="color:#006666">@RequestMapping</span><span style="color:#666600">(</span><span style="color:#008800">"/resource"</span><span style="color:#666600">)</span>
  <span style="color:#000088">public</span> <span style="color:#660066">Map</span><span style="color:#666600"><</span><span style="color:#660066">String</span><span style="color:#666600">,</span><span style="color:#660066">Object</span><span style="color:#666600">></span><span style="color:#000000"> home</span><span style="color:#666600">()</span> <span style="color:#666600">{</span>
    <span style="color:#660066">Map</span><span style="color:#666600"><</span><span style="color:#660066">String</span><span style="color:#666600">,</span><span style="color:#660066">Object</span><span style="color:#666600">></span><span style="color:#000000"> model </span><span style="color:#666600">=</span> <span style="color:#000088">new</span> <span style="color:#660066">HashMap</span><span style="color:#666600"><</span><span style="color:#660066">String</span><span style="color:#666600">,</span><span style="color:#660066">Object</span><span style="color:#666600">>();</span><span style="color:#000000">
    model</span><span style="color:#666600">.</span><span style="color:#000000">put</span><span style="color:#666600">(</span><span style="color:#008800">"id"</span><span style="color:#666600">,</span><span style="color:#000000"> UUID</span><span style="color:#666600">.</span><span style="color:#000000">randomUUID</span><span style="color:#666600">().</span><span style="color:#000000">toString</span><span style="color:#666600">());</span><span style="color:#000000">
    model</span><span style="color:#666600">.</span><span style="color:#000000">put</span><span style="color:#666600">(</span><span style="color:#008800">"content"</span><span style="color:#666600">,</span> <span style="color:#008800">"Hello World"</span><span style="color:#666600">);</span>
    <span style="color:#000088">return</span><span style="color:#000000"> model</span><span style="color:#666600">;</span>
  <span style="color:#666600">}</span>

  <span style="color:#000088">public</span> <span style="color:#000088">static</span> <span style="color:#000088">void</span><span style="color:#000000"> main</span><span style="color:#666600">(</span><span style="color:#660066">String</span><span style="color:#666600">[]</span><span style="color:#000000"> args</span><span style="color:#666600">)</span> <span style="color:#666600">{</span>
    <span style="color:#660066">SpringApplication</span><span style="color:#666600">.</span><span style="color:#000000">run</span><span style="color:#666600">(</span><span style="color:#660066">UiApplication</span><span style="color:#666600">.</span><span style="color:#000088">class</span><span style="color:#666600">,</span><span style="color:#000000"> args</span><span style="color:#666600">);</span>
  <span style="color:#666600">}</span>

<span style="color:#666600">}</span></code></span></span>
 根据您创建新项目的方式,可能不会调用它UiApplication

运行该应用程序并尝试卷曲“/ resource”端点,您会发现默认情况下它是安全的:

<span style="color:#34302d"><span style="color:#333333"><code><span style="color:#000000">$ curl localhost</span><span style="color:#666600">:</span><span style="color:#006666">8080</span><span style="color:#666600">/</span><span style="color:#000000">resource
</span><span style="color:#666600">{</span><span style="color:#008800">"timestamp"</span><span style="color:#666600">:</span><span style="color:#006666">1420442772928</span><span style="color:#666600">,</span><span style="color:#008800">"status"</span><span style="color:#666600">:</span><span style="color:#006666">401</span><span style="color:#666600">,</span><span style="color:#008800">"error"</span><span style="color:#666600">:</span><span style="color:#008800">"Unauthorized"</span><span style="color:#666600">,</span><span style="color:#008800">"message"</span><span style="color:#666600">:</span><span style="color:#008800">"Full authentication is required to access this resource"</span><span style="color:#666600">,</span><span style="color:#008800">"path"</span><span style="color:#666600">:</span><span style="color:#008800">"/resource"</span><span style="color:#666600">}</span></code></span></span>

从Angular加载动态资源

所以让我们在浏览器中抓取该消息。修改AppComponent以使用XHR加载受保护资源:

appponent.ts

<span style="color:#34302d"><span style="color:#333333"><code class="language-javascript"><span style="color:#000088">import</span> <span style="color:#666600">{</span> <span style="color:#660066">Component</span> <span style="color:#666600">}</span> <span style="color:#000088">from</span> <span style="color:#008800">'@angular/core'</span><span style="color:#666600">;</span>
<span style="color:#000088">import</span> <span style="color:#666600">{</span> <span style="color:#660066">HttpClient</span> <span style="color:#666600">}</span> <span style="color:#000088">from</span> <span style="color:#008800">'@angular/common/http'</span><span style="color:#666600">;</span>

<span style="color:#006666">@Component</span><span style="color:#666600">({</span><span style="color:#000000">
  selector</span><span style="color:#666600">:</span> <span style="color:#008800">'app-root'</span><span style="color:#666600">,</span><span style="color:#000000">
  templateUrl</span><span style="color:#666600">:</span> <span style="color:#008800">'./appponent.html'</span><span style="color:#666600">,</span><span style="color:#000000">
  styleUrls</span><span style="color:#666600">:</span> <span style="color:#666600">[</span><span style="color:#008800">'./appponent.css'</span><span style="color:#666600">]</span>
<span style="color:#666600">})</span>
<span style="color:#000088">export</span> <span style="color:#000088">class</span> <span style="color:#660066">AppComponent</span> <span style="color:#666600">{</span><span style="color:#000000">
  title </span><span style="color:#666600">=</span> <span style="color:#008800">'Demo'</span><span style="color:#666600">;</span><span style="color:#000000">
  greeting </span><span style="color:#666600">=</span> <span style="color:#666600">{};</span><span style="color:#000000">
  constructor</span><span style="color:#666600">(</span><span style="color:#000088">private</span><span style="color:#000000"> http</span><span style="color:#666600">:</span> <span style="color:#660066">HttpClient</span><span style="color:#666600">)</span> <span style="color:#666600">{</span><span style="color:#000000">
    http</span><span style="color:#666600">.</span><span style="color:#000088">get</span><span style="color:#666600">(</span><span style="color:#008800">'resource'</span><span style="color:#666600">).</span><span style="color:#000000">subscribe</span><span style="color:#666600">(</span><span style="color:#000000">data </span><span style="color:#666600">=></span> <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">greeting </span><span style="color:#666600">=</span><span style="color:#000000"> data</span><span style="color:#666600">);</span>
  <span style="color:#666600">}</span>
<span style="color:#666600">}</span></code></span></span>

我们注入了一个由Angular通过模块提供的http服务,http并用它来获取我们的资源。Angular将响应传递给我们,我们提取JSON并将其分配给问候语。

为了将http服务依赖注入到我们的自定义组件中,我们需要在AppModule包含组件的组件中声明它(imports与初始草稿相比,它只是一行):

app.module.ts

<span style="color:#34302d"><span style="color:#333333"><code class="language-javascript"><span style="color:#000088">import</span> <span style="color:#666600">{</span> <span style="color:#660066">BrowserModule</span> <span style="color:#666600">}</span> <span style="color:#000088">from</span> <span style="color:#008800">'@angular/platform-browser'</span><span style="color:#666600">;</span>
<span style="color:#000088">import</span> <span style="color:#666600">{</span> <span style="color:#660066">NgModule</span> <span style="color:#666600">}</span> <span style="color:#000088">from</span> <span style="color:#008800">'@angular/core'</span><span style="color:#666600">;</span>

<span style="color:#000088">import</span> <span style="color:#666600">{</span> <span style="color:#660066">AppComponent</span> <span style="color:#666600">}</span> <span style="color:#000088">from</span> <span style="color:#008800">'./appponent'</span><span style="color:#666600">;</span>
<span style="color:#000088">import</span> <span style="color:#666600">{</span> <span style="color:#660066">HttpClientModule</span> <span style="color:#666600">}</span> <span style="color:#000088">from</span> <span style="color:#008800">'@angular/common/http'</span><span style="color:#666600">;</span>

<span style="color:#006666">@NgModule</span><span style="color:#666600">({</span><span style="color:#000000">
  declarations</span><span style="color:#666600">:</span> <span style="color:#666600">[</span>
    <span style="color:#660066">AppComponent</span>
  <span style="color:#666600">],</span><span style="color:#000000">
  imports</span><span style="color:#666600">:</span> <span style="color:#666600">[</span>
    <span style="color:#660066">BrowserModule</span><span style="color:#666600">,</span>
    <span style="color:#660066">HttpClientModule</span>
  <span style="color:#666600">],</span><span style="color:#000000">
  providers</span><span style="color:#666600">:</span> <span style="color:#666600">[],</span><span style="color:#000000">
  bootstrap</span><span style="color:#666600">:</span> <span style="color:#666600">[</span><span style="color:#660066">AppComponent</span><span style="color:#666600">]</span>
<span style="color:#666600">})</span>
<span style="color:#000088">export</span> <span style="color:#000088">class</span> <span style="color:#660066">AppModule</span> <span style="color:#666600">{</span> <span style="color:#666600">}</span></code></span></span>

再次运行应用程序(或只是在浏览器中重新加载主页),您将看到带有唯一ID的动态消息。因此,即使资源受到保护而您无法直接卷曲,浏览器也能够访问内容。我们有一个安全的单页应用程序,不到一百行代码!

 您可能需要强制浏览器在更改后重新加载静态资源。在Chrome(以及带有插件的Firefox)中,您可以使用“开发人员工具”(F12),这可能就足够了。或者您可能必须使用CTRL + F5。

它是如何工作的?

如果您使用某些开发人员工具,则可以在浏览器中看到浏览器和后端之间的交互(通常F12打开它,默认情况下在Chrome中运行,可能需要Firefox中的插件)。这是一个总结:

动词路径状态响应

得到

/

401

浏览器提示进行身份验证

得到

/

200

的index.html

得到

/*.js

200

角度来自第三资产的负荷

得到

/main.bundle.js

200

应用逻辑

得到

/资源

200

JSON问候语

您可能看不到401,因为浏览器将主页加载视为单个交互,您可能会看到2个“/ resource”请求,因为存在CORS协商。

仔细查看请求,您将看到所有这些请求都有一个“授权”标题,如下所示:

<span style="color:#34302d"><span style="color:#333333"><code><span style="color:#660066">Authorization</span><span style="color:#666600">:</span> <span style="color:#660066">Basic</span><span style="color:#000000"> dXNlcjpwYXNzd29yZA</span><span style="color:#666600">==</span></code></span></span>

浏览器正在为每个请求发送用户名和密码(因此请记住在生产中使用HTTPS)。没有什么“Angular”,所以它适用于您的JavaScript框架或非框架的选择。

这有什么不对?

从表面上看,似乎我们做得非常好,它简洁,易于实现,我们所有的数据都通过密码保护,如果我们改变了前端或后端技术,它仍然可以工作。但是有一些问题。

  • 基本身份验证仅限于用户名和密码身份验证。

  • 身份验证UI无处不在但很难看(浏览器对话框)。

  • Cross Site Request Forgery(CSRF)不提供任何保护。

CSRF并不是我们的应用程序的问题,因为它只需要获取后端资源(即服务器中没有状态更改)。一旦你在你的应用程序中有POST,PUT或DELETE,任何合理的现代措施都不再安全。

在本系列的下一部分中,我们将扩展应用程序以使用基于表单的身份验证,这比HTTP Basic更灵活。一旦我们有了表单,我们将需要CSRF保护,Spring Security和Angular都有一些很好的开箱即用功能来帮助解决这个问题。剧透:我们将需要使用HttpSession

谢谢:我要感谢帮助我开发这个系列的所有人,特别是Rob Winch和Thorsten Spaeth对文本和源代码的仔细审查,以及教我一些技巧,我甚至不知道这些部分我以为我最熟悉。

(二)登录页面

在本节中,我们将继续讨论如何在“单页面应用程序”中使用带有Angular的Spring Security。在这里,我们将展示如何使用Angular通过表单对用户进行身份验证,并获取要在UI中呈现的安全资源。这是一系列部分中的第二部分,您可以通过阅读第一部分来了解应用程序的基本构建块或从头开始构建它,或者您可以直接访问Github中的源代码。在第一部分中,我们构建了一个使用HTTP Basic身份验证来保护后端资源的简单应用程序。在这一个中,我们添加一个登录表单,让用户可以控制是否进行身份验证,并在第一次迭代时解决问题(主要是缺乏CSRF保护)。

提醒:如果您正在使用示例应用程序完成此部分,请务必清除Cookie和HTTP Basic凭据的浏览器缓存。在Chrome中,为单个服务器执行此操作的最佳方法是打开新的隐身窗口。

添加导航到主页

Angular应用程序的核心是基本页面布局的HTML模板。我们已经有一个非常基本的,但对于这个应用程序,我们需要提供一些导航功能(登录,注销,主页),所以让我们修改它(in src/app):

appponent.html

<span style="color:#34302d"><span style="color:#333333"><code class="language-html"><span style="color:#000088"><div</span> <span style="color:#660066">class</span><span style="color:#666600">=</span><span style="color:#008800">"container"</span><span style="color:#000088">></span>
  <span style="color:#000088"><ul</span> <span style="color:#660066">class</span><span style="color:#666600">=</span><span style="color:#008800">"nav nav-pills"</span><span style="color:#000088">></span>
    <span style="color:#000088"><li><a</span> <span style="color:#660066">routerLinkActive</span><span style="color:#666600">=</span><span style="color:#008800">"active"</span> <span style="color:#660066">routerLink</span><span style="color:#666600">=</span><span style="color:#008800">"/home"</span><span style="color:#000088">></span><span style="color:#000000">Home</span><span style="color:#000088"></a></li></span>
    <span style="color:#000088"><li><a</span> <span style="color:#660066">routerLinkActive</span><span style="color:#666600">=</span><span style="color:#008800">"active"</span> <span style="color:#660066">routerLink</span><span style="color:#666600">=</span><span style="color:#008800">"/login"</span><span style="color:#000088">></span><span style="color:#000000">Login</span><span style="color:#000088"></a></li></span>
    <span style="color:#000088"><li><a</span><span style="color:#000000"> (</span><span style="color:#660066">click</span><span style="color:#000000">)</span><span style="color:#666600">=</span><span style="color:#008800">"logout()"</span><span style="color:#000088">></span><span style="color:#000000">Logout</span><span style="color:#000088"></a></li></span>
  <span style="color:#000088"></ul></span>
<span style="color:#000088"></div></span>
<span style="color:#000088"><div</span> <span style="color:#660066">class</span><span style="color:#666600">=</span><span style="color:#008800">"container"</span><span style="color:#000088">></span>
  <span style="color:#000088"><router-outlet></router-outlet></span>
<span style="color:#000088"></div></span></code></span></span>

主要内容是a,<router-outlet/>并且有一个带登录和注销链接的导航栏。

所述<router-outlet/>选择器是由角提供,它需要在主模块中进行接线同一个组件。每个路由将有一个组件(每个菜单链接),以及将它们粘合在一起的帮助程序服务,并共享一些state(AppService)。这是将所有部分组合在一起的模块的实现:

app.module.ts

<span style="color:#34302d"><span style="color:#333333"><code class="language-javascript"><span style="color:#000088">import</span> <span style="color:#666600">{</span> <span style="color:#660066">BrowserModule</span> <span style="color:#666600">}</span> <span style="color:#000088">from</span> <span style="color:#008800">'@angular/platform-browser'</span><span style="color:#666600">;</span>
<span style="color:#000088">import</span> <span style="color:#666600">{</span> <span style="color:#660066">NgModule</span> <span style="color:#666600">}</span> <span style="color:#000088">from</span> <span style="color:#008800">'@angular/core'</span><span style="color:#666600">;</span>
<span style="color:#000088">import</span> <span style="color:#666600">{</span> <span style="color:#660066">FormsModule</span> <span style="color:#666600">}</span> <span style="color:#000088">from</span> <span style="color:#008800">'@angular/forms'</span><span style="color:#666600">;</span>
<span style="color:#000088">import</span> <span style="color:#666600">{</span> <span style="color:#660066">HttpClientModule</span> <span style="color:#666600">}</span> <span style="color:#000088">from</span> <span style="color:#008800">'@angular/common/http'</span><span style="color:#666600">;</span>
<span style="color:#000088">import</span> <span style="color:#666600">{</span> <span style="color:#660066">RouterModule</span><span style="color:#666600">,</span> <span style="color:#660066">Routes</span> <span style="color:#666600">}</span> <span style="color:#000088">from</span> <span style="color:#008800">'@angular/router'</span><span style="color:#666600">;</span>
<span style="color:#000088">import</span> <span style="color:#666600">{</span> <span style="color:#660066">AppService</span> <span style="color:#666600">}</span> <span style="color:#000088">from</span> <span style="color:#008800">'./app.service'</span><span style="color:#666600">;</span>
<span style="color:#000088">import</span> <span style="color:#666600">{</span> <span style="color:#660066">HomeComponent</span> <span style="color:#666600">}</span> <span style="color:#000088">from</span> <span style="color:#008800">'./homeponent'</span><span style="color:#666600">;</span>
<span style="color:#000088">import</span> <span style="color:#666600">{</span> <span style="color:#660066">LoginComponent</span> <span style="color:#666600">}</span> <span style="color:#000088">from</span> <span style="color:#008800">'./loginponent'</span><span style="color:#666600">;</span>
<span style="color:#000088">import</span> <span style="color:#666600">{</span> <span style="color:#660066">AppComponent</span> <span style="color:#666600">}</span> <span style="color:#000088">from</span> <span style="color:#008800">'./appponent'</span><span style="color:#666600">;</span>

<span style="color:#000088">const</span><span style="color:#000000"> routes</span><span style="color:#666600">:</span> <span style="color:#660066">Routes</span> <span style="color:#666600">=</span> <span style="color:#666600">[</span>
  <span style="color:#666600">{</span><span style="color:#000000"> path</span><span style="color:#666600">:</span> <span style="color:#008800">''</span><span style="color:#666600">,</span><span style="color:#000000"> pathMatch</span><span style="color:#666600">:</span> <span style="color:#008800">'full'</span><span style="color:#666600">,</span><span style="color:#000000"> redirectTo</span><span style="color:#666600">:</span> <span style="color:#008800">'home'</span><span style="color:#666600">},</span>
  <span style="color:#666600">{</span><span style="color:#000000"> path</span><span style="color:#666600">:</span> <span style="color:#008800">'home'</span><span style="color:#666600">,</span><span style="color:#000000"> component</span><span style="color:#666600">:</span> <span style="color:#660066">HomeComponent</span><span style="color:#666600">},</span>
  <span style="color:#666600">{</span><span style="color:#000000"> path</span><span style="color:#666600">:</span> <span style="color:#008800">'login'</span><span style="color:#666600">,</span><span style="color:#000000"> component</span><span style="color:#666600">:</span> <span style="color:#660066">LoginComponent</span><span style="color:#666600">}</span>
<span style="color:#666600">];</span>

<span style="color:#006666">@NgModule</span><span style="color:#666600">({</span><span style="color:#000000">
  declarations</span><span style="color:#666600">:</span> <span style="color:#666600">[</span>
    <span style="color:#660066">AppComponent</span><span style="color:#666600">,</span>
    <span style="color:#660066">HomeComponent</span><span style="color:#666600">,</span>
    <span style="color:#660066">LoginComponent</span>
  <span style="color:#666600">],</span><span style="color:#000000">
  imports</span><span style="color:#666600">:</span> <span style="color:#666600">[</span>
    <span style="color:#660066">RouterModule</span><span style="color:#666600">.</span><span style="color:#000000">forRoot</span><span style="color:#666600">(</span><span style="color:#000000">routes</span><span style="color:#666600">),</span>
    <span style="color:#660066">BrowserModule</span><span style="color:#666600">,</span>
    <span style="color:#660066">HttpClientModule</span><span style="color:#666600">,</span>
    <span style="color:#660066">FormsModule</span>
  <span style="color:#666600">],</span><span style="color:#000000">
  providers</span><span style="color:#666600">:</span> <span style="color:#666600">[</span><span style="color:#660066">AppService</span><span style="color:#666600">]</span><span style="color:#000000">
  bootstrap</span><span style="color:#666600">:</span> <span style="color:#666600">[</span><span style="color:#660066">AppComponent</span><span style="color:#666600">]</span>
<span style="color:#666600">})</span>
<span style="color:#000088">export</span> <span style="color:#000088">class</span> <span style="color:#660066">AppModule</span> <span style="color:#666600">{</span> <span style="color:#666600">}</span></code></span></span>

我们在一个名为“RouterModule”的Angular模块上添加了一个依赖项,这使我们能够在一个router构造函数中注入一个魔法AppComponent。在routes使用的进口内AppModule设置链接到“/”(“家”控制器)和“/登录”(以下简称“登陆”控制器)。

我们还潜入了FormsModule那里,因为稍后需要将数据绑定到我们想要在用户登录时提交的表单。

UI组件都是“声明”,服务粘合剂是“提供者”。在AppComponent实际上并没有做很多。与app root一起使用的TypeScript组件位于:

appponent.ts

<span style="color:#34302d"><span style="color:#333333"><code class="language-javascript"><span style="color:#000088">import</span> <span style="color:#666600">{</span> <span style="color:#660066">Component</span> <span style="color:#666600">}</span> <span style="color:#000088">from</span> <span style="color:#008800">'@angular/core'</span><span style="color:#666600">;</span>
<span style="color:#000088">import</span> <span style="color:#666600">{</span> <span style="color:#660066">AppService</span> <span style="color:#666600">}</span> <span style="color:#000088">from</span> <span style="color:#008800">'./app.service'</span><span style="color:#666600">;</span>
<span style="color:#000088">import</span> <span style="color:#666600">{</span> <span style="color:#660066">HttpClient</span> <span style="color:#666600">}</span> <span style="color:#000088">from</span> <span style="color:#008800">'@angular/common/http'</span><span style="color:#666600">;</span>
<span style="color:#000088">import</span> <span style="color:#666600">{</span> <span style="color:#660066">Router</span> <span style="color:#666600">}</span> <span style="color:#000088">from</span> <span style="color:#008800">'@angular/router'</span><span style="color:#666600">;</span>
<span style="color:#000088">import</span> <span style="color:#008800">'rxjs/add/operator/finally'</span><span style="color:#666600">;</span>

<span style="color:#006666">@Component</span><span style="color:#666600">({</span><span style="color:#000000">
  selector</span><span style="color:#666600">:</span> <span style="color:#008800">'app-root'</span><span style="color:#666600">,</span><span style="color:#000000">
  templateUrl</span><span style="color:#666600">:</span> <span style="color:#008800">'./appponent.html'</span><span style="color:#666600">,</span><span style="color:#000000">
  styleUrls</span><span style="color:#666600">:</span> <span style="color:#666600">[</span><span style="color:#008800">'./appponent.css'</span><span style="color:#666600">]</span>
<span style="color:#666600">})</span>
<span style="color:#000088">export</span> <span style="color:#000088">class</span> <span style="color:#660066">AppComponent</span> <span style="color:#666600">{</span><span style="color:#000000">
  constructor</span><span style="color:#666600">(</span><span style="color:#000088">private</span><span style="color:#000000"> app</span><span style="color:#666600">:</span> <span style="color:#660066">AppService</span><span style="color:#666600">,</span> <span style="color:#000088">private</span><span style="color:#000000"> http</span><span style="color:#666600">:</span> <span style="color:#660066">HttpClient</span><span style="color:#666600">,</span> <span style="color:#000088">private</span><span style="color:#000000"> router</span><span style="color:#666600">:</span> <span style="color:#660066">Router</span><span style="color:#666600">)</span> <span style="color:#666600">{</span>
      <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">app</span><span style="color:#666600">.</span><span style="color:#000000">authenticate</span><span style="color:#666600">(</span><span style="color:#000088">undefined</span><span style="color:#666600">,</span> <span style="color:#000088">undefined</span><span style="color:#666600">);</span>
    <span style="color:#666600">}</span><span style="color:#000000">
    logout</span><span style="color:#666600">()</span> <span style="color:#666600">{</span>
      <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">http</span><span style="color:#666600">.</span><span style="color:#000000">post</span><span style="color:#666600">(</span><span style="color:#008800">'logout'</span><span style="color:#666600">,</span> <span style="color:#666600">{}).</span><span style="color:#000088">finally</span><span style="color:#666600">(()</span> <span style="color:#666600">=></span> <span style="color:#666600">{</span>
          <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">app</span><span style="color:#666600">.</span><span style="color:#000000">authenticated </span><span style="color:#666600">=</span> <span style="color:#000088">false</span><span style="color:#666600">;</span>
          <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">router</span><span style="color:#666600">.</span><span style="color:#000000">navigateByUrl</span><span style="color:#666600">(</span><span style="color:#008800">'/login'</span><span style="color:#666600">);</span>
      <span style="color:#666600">}).</span><span style="color:#000000">subscribe</span><span style="color:#666600">();</span>
    <span style="color:#666600">}</span>

<span style="color:#666600">}</span></code></span></span>

突出特点:

  • 还有一些依赖注入,这次是 AppService

  • 有一个注销函数作为组件的属性公开,我们稍后可以使用它向后端发送注销请求。它在app服务中设置一个标志,并将用户发送回登录屏幕(并通过finally()回调无条件地执行此操作)。

  • 我们正在使用templateUrl将模板HTML外部化为单独的文件。

  • authenticate()加载控制器时调用该函数以查看用户是否实际上已经过身份验证(例如,如果他在会话中间刷新了浏览器)。我们需要authenticate()函数来进行远程调用,因为实际的身份验证是由服务器完成的,我们不希望信任浏览器来跟踪它。

app我们上面注入的服务需要一个布尔标志,以便我们可以判断用户当前是否已经过身份验证,以及authenticate()可用于对后端服务器进行身份验证的函数,或者仅查询用户详细信息:

app.service.ts

<span style="color:#34302d"><span style="color:#333333"><code class="language-javascript"><span style="color:#000088">import</span> <span style="color:#666600">{</span> <span style="color:#660066">Injectable</span> <span style="color:#666600">}</span> <span style="color:#000088">from</span> <span style="color:#008800">'@angular/core'</span><span style="color:#666600">;</span>
<span style="color:#000088">import</span> <span style="color:#666600">{</span> <span style="color:#660066">HttpClient</span><span style="color:#666600">,</span> <span style="color:#660066">HttpHeaders</span> <span style="color:#666600">}</span> <span style="color:#000088">from</span> <span style="color:#008800">'@angular/common/http'</span><span style="color:#666600">;</span>

<span style="color:#006666">@Injectable</span><span style="color:#666600">()</span>
<span style="color:#000088">export</span> <span style="color:#000088">class</span> <span style="color:#660066">AppService</span> <span style="color:#666600">{</span><span style="color:#000000">

  authenticated </span><span style="color:#666600">=</span> <span style="color:#000088">false</span><span style="color:#666600">;</span><span style="color:#000000">

  constructor</span><span style="color:#666600">(</span><span style="color:#000088">private</span><span style="color:#000000"> http</span><span style="color:#666600">:</span> <span style="color:#660066">HttpClient</span><span style="color:#666600">)</span> <span style="color:#666600">{</span>
  <span style="color:#666600">}</span><span style="color:#000000">

  authenticate</span><span style="color:#666600">(</span><span style="color:#000000">credentials</span><span style="color:#666600">,</span><span style="color:#000000"> callback</span><span style="color:#666600">)</span> <span style="color:#666600">{</span>

        <span style="color:#000088">const</span><span style="color:#000000"> headers </span><span style="color:#666600">=</span> <span style="color:#000088">new</span> <span style="color:#660066">HttpHeaders</span><span style="color:#666600">(</span><span style="color:#000000">credentials </span><span style="color:#666600">?</span> <span style="color:#666600">{</span><span style="color:#000000">
            authorization </span><span style="color:#666600">:</span> <span style="color:#008800">'Basic '</span> <span style="color:#666600">+</span><span style="color:#000000"> btoa</span><span style="color:#666600">(</span><span style="color:#000000">credentials</span><span style="color:#666600">.</span><span style="color:#000000">username </span><span style="color:#666600">+</span> <span style="color:#008800">':'</span> <span style="color:#666600">+</span><span style="color:#000000"> credentials</span><span style="color:#666600">.</span><span style="color:#000000">password</span><span style="color:#666600">)</span>
        <span style="color:#666600">}</span> <span style="color:#666600">:</span> <span style="color:#666600">{});</span>

        <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">http</span><span style="color:#666600">.</span><span style="color:#000088">get</span><span style="color:#666600">(</span><span style="color:#008800">'user'</span><span style="color:#666600">,</span> <span style="color:#666600">{</span><span style="color:#000000">headers</span><span style="color:#666600">:</span><span style="color:#000000"> headers</span><span style="color:#666600">}).</span><span style="color:#000000">subscribe</span><span style="color:#666600">(</span><span style="color:#000000">response </span><span style="color:#666600">=></span> <span style="color:#666600">{</span>
            <span style="color:#000088">if</span> <span style="color:#666600">(</span><span style="color:#000000">response</span><span style="color:#666600">[</span><span style="color:#008800">'name'</span><span style="color:#666600">])</span> <span style="color:#666600">{</span>
                <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">authenticated </span><span style="color:#666600">=</span> <span style="color:#000088">true</span><span style="color:#666600">;</span>
            <span style="color:#666600">}</span> <span style="color:#000088">else</span> <span style="color:#666600">{</span>
                <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">authenticated </span><span style="color:#666600">=</span> <span style="color:#000088">false</span><span style="color:#666600">;</span>
            <span style="color:#666600">}</span>
            <span style="color:#000088">return</span><span style="color:#000000"> callback </span><span style="color:#666600">&&</span><span style="color:#000000"> callback</span><span style="color:#666600">();</span>
        <span style="color:#666600">});</span>

    <span style="color:#666600">}</span>

<span style="color:#666600">}</span></code></span></span>

authenticated标志是简单的。authenticate()如果提供了HTTP基本身份验证凭据,则该函数将发送,否则不会。它还有一个可选callback参数,如果认证成功,我们可以用它来执行一些代码。

打招呼

来自旧主页的问候语内容可以直接显示在“src / app”中的“appponent.html”旁边:

homeponent.html

<span style="color:#34302d"><span style="color:#333333"><code class="language-html"><span style="color:#000088"><h1></span><span style="color:#000000">Greeting</span><span style="color:#000088"></h1></span>
<span style="color:#000088"><div</span><span style="color:#000000"> [</span><span style="color:#660066">hidden</span><span style="color:#000000">]</span><span style="color:#666600">=</span><span style="color:#008800">"!authenticated()"</span><span style="color:#000088">></span>
	<span style="color:#000088"><p></span><span style="color:#000000">The ID is {{greeting.id}}</span><span style="color:#000088"></p></span>
	<span style="color:#000088"><p></span><span style="color:#000000">The content is {{greeting.content}}</span><span style="color:#000088"></p></span>
<span style="color:#000088"></div></span>
<span style="color:#000088"><div</span><span style="color:#000000"> [</span><span style="color:#660066">hidden</span><span style="color:#000000">]</span><span style="color:#666600">=</span><span style="color:#008800">"authenticated()"</span><span style="color:#000088">></span>
	<span style="color:#000088"><p></span><span style="color:#000000">Login to see your greeting</span><span style="color:#000088"></p></span>
<span style="color:#000088"></div></span></code></span></span>

由于用户现在可以选择是否登录(在浏览器完全控制之前),我们需要在UI中区分安全内容和非安全内容。我们通过添加对(当前不存在的)authenticated()函数的引用来预测这一点。

HomeComponent再去捡问候,同时还提供了authenticated()拉动旗出的效用函数AppService

homeponent.ts

<span style="color:#34302d"><span style="color:#333333"><code class="language-javascript"><span style="color:#000088">import</span> <span style="color:#666600">{</span> <span style="color:#660066">Component</span><span style="color:#666600">,</span> <span style="color:#660066">OnInit</span> <span style="color:#666600">}</span> <span style="color:#000088">from</span> <span style="color:#008800">'@angular/core'</span><span style="color:#666600">;</span>
<span style="color:#000088">import</span> <span style="color:#666600">{</span> <span style="color:#660066">AppService</span> <span style="color:#666600">}</span> <span style="color:#000088">from</span> <span style="color:#008800">'./app.service'</span><span style="color:#666600">;</span>
<span style="color:#000088">import</span> <span style="color:#666600">{</span> <span style="color:#660066">HttpClient</span> <span style="color:#666600">}</span> <span style="color:#000088">from</span> <span style="color:#008800">'@angular/common/http'</span><span style="color:#666600">;</span>

<span style="color:#006666">@Component</span><span style="color:#666600">({</span><span style="color:#000000">
  templateUrl</span><span style="color:#666600">:</span> <span style="color:#008800">'./homeponent.html'</span>
<span style="color:#666600">})</span>
<span style="color:#000088">export</span> <span style="color:#000088">class</span> <span style="color:#660066">HomeComponent</span> <span style="color:#666600">{</span><span style="color:#000000">

  title </span><span style="color:#666600">=</span> <span style="color:#008800">'Demo'</span><span style="color:#666600">;</span><span style="color:#000000">
  greeting </span><span style="color:#666600">=</span> <span style="color:#666600">{};</span><span style="color:#000000">

  constructor</span><span style="color:#666600">(</span><span style="color:#000088">private</span><span style="color:#000000"> app</span><span style="color:#666600">:</span> <span style="color:#660066">AppService</span><span style="color:#666600">,</span> <span style="color:#000088">private</span><span style="color:#000000"> http</span><span style="color:#666600">:</span> <span style="color:#660066">HttpClient</span><span style="color:#666600">)</span> <span style="color:#666600">{</span><span style="color:#000000">
    http</span><span style="color:#666600">.</span><span style="color:#000088">get</span><span style="color:#666600">(</span><span style="color:#008800">'resource'</span><span style="color:#666600">).</span><span style="color:#000000">subscribe</span><span style="color:#666600">(</span><span style="color:#000000">data </span><span style="color:#666600">=></span> <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">greeting </span><span style="color:#666600">=</span><span style="color:#000000"> data</span><span style="color:#666600">);</span>
  <span style="color:#666600">}</span><span style="color:#000000">

  authenticated</span><span style="color:#666600">()</span> <span style="color:#666600">{</span> <span style="color:#000088">return</span> <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">app</span><span style="color:#666600">.</span><span style="color:#000000">authenticated</span><span style="color:#666600">;</span> <span style="color:#666600">}</span>

<span style="color:#666600">}</span></code></span></span>

登录表格

登录表单也有自己的组件:

loginponent.html

<span style="color:#34302d"><span style="color:#333333"><code class="language-html"><span style="color:#000088"><div</span> <span style="color:#660066">class</span><span style="color:#666600">=</span><span style="color:#008800">"alert alert-danger"</span><span style="color:#000000"> [</span><span style="color:#660066">hidden</span><span style="color:#000000">]</span><span style="color:#666600">=</span><span style="color:#008800">"!error"</span><span style="color:#000088">></span><span style="color:#000000">
	There was a problem logging in. Please try again.
</span><span style="color:#000088"></div></span>
<span style="color:#000088"><form</span> <span style="color:#660066">role</span><span style="color:#666600">=</span><span style="color:#008800">"form"</span><span style="color:#000000"> (</span><span style="color:#660066">submit</span><span style="color:#000000">)</span><span style="color:#666600">=</span><span style="color:#008800">"login()"</span><span style="color:#000088">></span>
	<span style="color:#000088"><div</span> <span style="color:#660066">class</span><span style="color:#666600">=</span><span style="color:#008800">"form-group"</span><span style="color:#000088">></span>
		<span style="color:#000088"><label</span> <span style="color:#660066">for</span><span style="color:#666600">=</span><span style="color:#008800">"username"</span><span style="color:#000088">></span><span style="color:#000000">Username:</span><span style="color:#000088"></label></span> <span style="color:#000088"><input</span> <span style="color:#660066">type</span><span style="color:#666600">=</span><span style="color:#008800">"text"</span>
			<span style="color:#660066">class</span><span style="color:#666600">=</span><span style="color:#008800">"form-control"</span> <span style="color:#660066">id</span><span style="color:#666600">=</span><span style="color:#008800">"username"</span> <span style="color:#660066">name</span><span style="color:#666600">=</span><span style="color:#008800">"username"</span><span style="color:#000000"> [(</span><span style="color:#660066">ngModel</span><span style="color:#000000">)]</span><span style="color:#666600">=</span><span style="color:#008800">"credentials.username"</span><span style="color:#000088">/></span>
	<span style="color:#000088"></div></span>
	<span style="color:#000088"><div</span> <span style="color:#660066">class</span><span style="color:#666600">=</span><span style="color:#008800">"form-group"</span><span style="color:#000088">></span>
		<span style="color:#000088"><label</span> <span style="color:#660066">for</span><span style="color:#666600">=</span><span style="color:#008800">"password"</span><span style="color:#000088">></span><span style="color:#000000">Password:</span><span style="color:#000088"></label></span> <span style="color:#000088"><input</span> <span style="color:#660066">type</span><span style="color:#666600">=</span><span style="color:#008800">"password"</span>
			<span style="color:#660066">class</span><span style="color:#666600">=</span><span style="color:#008800">"form-control"</span> <span style="color:#660066">id</span><span style="color:#666600">=</span><span style="color:#008800">"password"</span> <span style="color:#660066">name</span><span style="color:#666600">=</span><span style="color:#008800">"password"</span><span style="color:#000000"> [(</span><span style="color:#660066">ngModel</span><span style="color:#000000">)]</span><span style="color:#666600">=</span><span style="color:#008800">"credentials.password"</span><span style="color:#000088">/></span>
	<span style="color:#000088"></div></span>
	<span style="color:#000088"><button</span> <span style="color:#660066">type</span><span style="color:#666600">=</span><span style="color:#008800">"submit"</span> <span style="color:#660066">class</span><span style="color:#666600">=</span><span style="color:#008800">"btn btn-primary"</span><span style="color:#000088">></span><span style="color:#000000">Submit</span><span style="color:#000088"></button></span>
<span style="color:#000088"></form></span></code></span></span>

这是一个非常标准的登录表单,有2个用户名和密码输入,以及一个通过Angular事件处理程序提交表单的按钮(submit)。您不需要对表单标记执行操作,因此最好不要将其放入表单标记中。还有一条错误消息,仅在角度模型包含时显示error。表单控件使用ngModel从角形式的HTML和角控制器之间传递数据,并且在这种情况下,我们使用的是credentials对象来保存用户名和pasword。

身份验证过程

为了支持我们刚刚添加的登录表单,我们需要添加更多功能。在客户端,这些将LoginComponent在服务器上实现,它将是Spring Security配置。

提交登录表格

要提交表单,我们需要定义login()我们在表单via中引用的函数ng-submit,以及credentials我们引用的对象ng-model。让我们充实“登录”组件:

loginponent.ts

<span style="color:#34302d"><span style="color:#333333"><code class="language-javascript"><span style="color:#000088">import</span> <span style="color:#666600">{</span> <span style="color:#660066">Component</span><span style="color:#666600">,</span> <span style="color:#660066">OnInit</span> <span style="color:#666600">}</span> <span style="color:#000088">from</span> <span style="color:#008800">'@angular/core'</span><span style="color:#666600">;</span>
<span style="color:#000088">import</span> <span style="color:#666600">{</span> <span style="color:#660066">AppService</span> <span style="color:#666600">}</span> <span style="color:#000088">from</span> <span style="color:#008800">'./app.service'</span><span style="color:#666600">;</span>
<span style="color:#000088">import</span> <span style="color:#666600">{</span> <span style="color:#660066">HttpClient</span> <span style="color:#666600">}</span> <span style="color:#000088">from</span> <span style="color:#008800">'@angular/common/http'</span><span style="color:#666600">;</span>
<span style="color:#000088">import</span> <span style="color:#666600">{</span> <span style="color:#660066">Router</span> <span style="color:#666600">}</span> <span style="color:#000088">from</span> <span style="color:#008800">'@angular/router'</span><span style="color:#666600">;</span>

<span style="color:#006666">@Component</span><span style="color:#666600">({</span><span style="color:#000000">
  templateUrl</span><span style="color:#666600">:</span> <span style="color:#008800">'./loginponent.html'</span>
<span style="color:#666600">})</span>
<span style="color:#000088">export</span> <span style="color:#000088">class</span> <span style="color:#660066">LoginComponent</span> <span style="color:#666600">{</span><span style="color:#000000">

  credentials </span><span style="color:#666600">=</span> <span style="color:#666600">{</span><span style="color:#000000">username</span><span style="color:#666600">:</span> <span style="color:#008800">''</span><span style="color:#666600">,</span><span style="color:#000000"> password</span><span style="color:#666600">:</span> <span style="color:#008800">''</span><span style="color:#666600">};</span><span style="color:#000000">

  constructor</span><span style="color:#666600">(</span><span style="color:#000088">private</span><span style="color:#000000"> app</span><span style="color:#666600">:</span> <span style="color:#660066">AppService</span><span style="color:#666600">,</span> <span style="color:#000088">private</span><span style="color:#000000"> http</span><span style="color:#666600">:</span> <span style="color:#660066">HttpClient</span><span style="color:#666600">,</span> <span style="color:#000088">private</span><span style="color:#000000"> router</span><span style="color:#666600">:</span> <span style="color:#660066">Router</span><span style="color:#666600">)</span> <span style="color:#666600">{</span>
  <span style="color:#666600">}</span><span style="color:#000000">

  login</span><span style="color:#666600">()</span> <span style="color:#666600">{</span>
    <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">app</span><span style="color:#666600">.</span><span style="color:#000000">authenticate</span><span style="color:#666600">(</span><span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">credentials</span><span style="color:#666600">,</span> <span style="color:#666600">()</span> <span style="color:#666600">=></span> <span style="color:#666600">{</span>
        <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">router</span><span style="color:#666600">.</span><span style="color:#000000">navigateByUrl</span><span style="color:#666600">(</span><span style="color:#008800">'/'</span><span style="color:#666600">);</span>
    <span style="color:#666600">});</span>
    <span style="color:#000088">return</span> <span style="color:#000088">false</span><span style="color:#666600">;</span>
  <span style="color:#666600">}</span>

<span style="color:#666600">}</span></code></span></span>

除了初始化credentials对象外,它还定义login()了表单中我们需要的对象。

authenticate()发出GET一个相对资源(相对于你的应用程序的部署根)“/用户”。从login()函数调用时,它会在标头中添加Base64编码的凭据,因此在服务器上它会进行身份验证并接受cookie作为回报。当我们获得认证结果时,该login()函数还相应地设置本地$scope.error标志,该结果用于控制登录表单上方的错误消息的显示。

当前经过身份验证的用户

要为该authenticate()功能提供服务,我们需要在后端添加一个新端点:

UiApplication.java

<span style="color:#34302d"><span style="color:#333333"><code class="language-java"><span style="color:#006666">@SpringBootApplication</span>
<span style="color:#006666">@RestController</span>
<span style="color:#000088">public</span> <span style="color:#000088">class</span> <span style="color:#660066">UiApplication</span> <span style="color:#666600">{</span>

  <span style="color:#006666">@RequestMapping</span><span style="color:#666600">(</span><span style="color:#008800">"/user"</span><span style="color:#666600">)</span>
  <span style="color:#000088">public</span> <span style="color:#660066">Principal</span><span style="color:#000000"> user</span><span style="color:#666600">(</span><span style="color:#660066">Principal</span><span style="color:#000000"> user</span><span style="color:#666600">)</span> <span style="color:#666600">{</span>
    <span style="color:#000088">return</span><span style="color:#000000"> user</span><span style="color:#666600">;</span>
  <span style="color:#666600">}</span>

  <span style="color:#666600">...</span>

<span style="color:#666600">}</span></code></span></span>

这是Spring Security应用程序中的一个有用技巧。如果“/ user”资源可以访问,那么它将返回当前经过身份验证的用户(an Authentication),否则Spring Security将拦截该请求并通过a发送401响应AuthenticationEntryPoint

处理服务器上的登录请求

Spring Security可以轻松处理登录请求。我们只需要在主应用程序类中添加一些配置(例如作为内部类):

UiApplication.java

<span style="color:#34302d"><span style="color:#333333"><code class="language-java"><span style="color:#006666">@SpringBootApplication</span>
<span style="color:#006666">@RestController</span>
<span style="color:#000088">public</span> <span style="color:#000088">class</span> <span style="color:#660066">UiApplication</span> <span style="color:#666600">{</span>

  <span style="color:#666600">...</span>

  <span style="color:#006666">@Configuration</span>
  <span style="color:#006666">@Order</span><span style="color:#666600">(</span><span style="color:#660066">SecurityProperties</span><span style="color:#666600">.</span><span style="color:#000000">ACCESS_OVERRIDE_ORDER</span><span style="color:#666600">)</span>
  <span style="color:#000088">protected</span> <span style="color:#000088">static</span> <span style="color:#000088">class</span> <span style="color:#660066">SecurityConfiguration</span> <span style="color:#000088">extends</span> <span style="color:#660066">WebSecurityConfigurerAdapter</span> <span style="color:#666600">{</span>
    <span style="color:#006666">@Override</span>
    <span style="color:#000088">protected</span> <span style="color:#000088">void</span><span style="color:#000000"> configure</span><span style="color:#666600">(</span><span style="color:#660066">HttpSecurity</span><span style="color:#000000"> http</span><span style="color:#666600">)</span> <span style="color:#000088">throws</span> <span style="color:#660066">Exception</span> <span style="color:#666600">{</span><span style="color:#000000">
      http
        </span><span style="color:#666600">.</span><span style="color:#000000">httpBasic</span><span style="color:#666600">()</span>
      <span style="color:#666600">.</span><span style="color:#000088">and</span><span style="color:#666600">()</span>
        <span style="color:#666600">.</span><span style="color:#000000">authorizeRequests</span><span style="color:#666600">()</span>
          <span style="color:#666600">.</span><span style="color:#000000">antMatchers</span><span style="color:#666600">(</span><span style="color:#008800">"/index.html"</span><span style="color:#666600">,</span> <span style="color:#008800">"/"</span><span style="color:#666600">,</span> <span style="color:#008800">"/home"</span><span style="color:#666600">,</span> <span style="color:#008800">"/login"</span><span style="color:#666600">).</span><span style="color:#000000">permitAll</span><span style="color:#666600">()</span>
          <span style="color:#666600">.</span><span style="color:#000000">anyRequest</span><span style="color:#666600">().</span><span style="color:#000000">authenticated</span><span style="color:#666600">();</span>
    <span style="color:#666600">}</span>
  <span style="color:#666600">}</span>

<span style="color:#666600">}</span></code></span></span>

这是一个带有Spring Security自定义的标准Spring Boot应用程序,只允许匿名访问静态(HTML)资源。HTML资源需要供匿名用户使用,而不仅仅是被Spring Security忽略,原因很明显。

我们需要记住的最后一件事是使Angular提供的JavaScript组件匿名提供给应用程序。我们可以在HttpSecurity上面的配置中做到这一点,但由于它是静态内容,最好简单地忽略它:

application.yml

<span style="color:#34302d"><span style="color:#333333"><code class="language-yaml"><span style="color:#000000">security</span><span style="color:#666600">:</span><span style="color:#000000">
  ignored</span><span style="color:#666600">:</span>
  <span style="color:#666600">-</span> <span style="color:#008800">"*.bundle.*"</span></code></span></span>

添加默认HTTP请求标头

如果此时运行应用程序,您会发现浏览器会弹出基本身份验证对话框(用户名和密码)。它这样做是因为它看到一个401效应初探从XHR请求/user,并/resource以“WWW身份验证”标头。抑制此弹出窗口的方法是禁止来自Spring Security的标头。抑制响应标头的方法是发送一个特殊的传统请求标头“X-Requested-With = XMLHttpRequest”。它曾经是Angular中的默认值,但是它们在1.3.0中取出了它。所以这里是如何在Angular XHR请求中设置默认标头。

首先扩展RequestOptionsAngular HTTP模块提供的默认值:

app.module.ts

<span style="color:#34302d"><span style="color:#333333"><code class="language-javascript"><span style="color:#006666">@Injectable</span><span style="color:#666600">()</span>
<span style="color:#000088">export</span> <span style="color:#000088">class</span> <span style="color:#660066">XhrInterceptor</span> <span style="color:#000088">implements</span> <span style="color:#660066">HttpInterceptor</span> <span style="color:#666600">{</span><span style="color:#000000">

  intercept</span><span style="color:#666600">(</span><span style="color:#000000">req</span><span style="color:#666600">:</span> <span style="color:#660066">HttpRequest</span><span style="color:#008800"><any></span><span style="color:#666600">,</span> <span style="color:#000088">next</span><span style="color:#666600">:</span> <span style="color:#660066">HttpHandler</span><span style="color:#666600">)</span> <span style="color:#666600">{</span>
    <span style="color:#000088">const</span><span style="color:#000000"> xhr </span><span style="color:#666600">=</span><span style="color:#000000"> req</span><span style="color:#666600">.</span><span style="color:#000000">clone</span><span style="color:#666600">({</span><span style="color:#000000">
      headers</span><span style="color:#666600">:</span><span style="color:#000000"> req</span><span style="color:#666600">.</span><span style="color:#000000">headers</span><span style="color:#666600">.</span><span style="color:#000088">set</span><span style="color:#666600">(</span><span style="color:#008800">'X-Requested-With'</span><span style="color:#666600">,</span> <span style="color:#008800">'XMLHttpRequest'</span><span style="color:#666600">)</span>
    <span style="color:#666600">});</span>
    <span style="color:#000088">return</span> <span style="color:#000088">next</span><span style="color:#666600">.</span><span style="color:#000000">handle</span><span style="color:#666600">(</span><span style="color:#000000">xhr</span><span style="color:#666600">);</span>
  <span style="color:#666600">}</span>
<span style="color:#666600">}</span></code></span></span>

这里的语法是样板文件。该implements财产Class是它的基类,而除了构造函数中,我们真正需要做的是重写intercept()它总是被称为角,可用于添加额外的头功能。

要安装这个新的RequestOptions工厂,我们需要声明它在providersAppModule

app.module.ts

<span style="color:#34302d"><span style="color:#333333"><code class="language-javascript"><span style="color:#006666">@NgModule</span><span style="color:#666600">({</span>
  <span style="color:#666600">...</span><span style="color:#000000">
  providers</span><span style="color:#666600">:</span> <span style="color:#666600">[</span><span style="color:#660066">AppService</span><span style="color:#666600">,</span> <span style="color:#666600">{</span><span style="color:#000000"> provide</span><span style="color:#666600">:</span><span style="color:#000000"> HTTP_INTERCEPTORS</span><span style="color:#666600">,</span><span style="color:#000000"> useClass</span><span style="color:#666600">:</span> <span style="color:#660066">XhrInterceptor</span><span style="color:#666600">,</span><span style="color:#000000"> multi</span><span style="color:#666600">:</span> <span style="color:#000088">true</span> <span style="color:#666600">}],</span>
  <span style="color:#666600">...</span>
<span style="color:#666600">})</span>
<span style="color:#000088">export</span> <span style="color:#000088">class</span> <span style="color:#660066">AppModule</span> <span style="color:#666600">{</span> <span style="color:#666600">}</span></code></span></span>

登出

该应用程序几乎在功能上完成。我们需要做的最后一件事是实现我们在主页中勾画的注销功能。如果用户已通过身份验证,那么我们会显示“注销”链接并将其挂钩到该logout()功能中AppComponent。请记住,它将HTTP POST发送到“/ logout”,我们现在需要在服务器上实现。这很简单,因为Spring Security已经为我们添加了它(即我们不需要为这个简单的用例做任何事情)。为了更好地控制注销行为,您可以使用to中的HttpSecurity回调WebSecurityAdapter,例如在注销后执行一些业务逻辑。

CSRF保护

该应用程序几乎可以使用,事实上,如果你运行它,你会发现到目前为止我们构建的所有内容实际上都有效,除了注销链接。尝试使用它并查看浏览器中的响应,您将看到原因:

<span style="color:#34302d"><span style="color:#333333"><code><span style="color:#000000">POST </span><span style="color:#666600">/</span><span style="color:#000000">logout HTTP</span><span style="color:#666600">/</span><span style="color:#006666">1.1</span>
<span style="color:#666600">...</span>
<span style="color:#660066">Content</span><span style="color:#666600">-</span><span style="color:#660066">Type</span><span style="color:#666600">:</span><span style="color:#000000"> application</span><span style="color:#666600">/</span><span style="color:#000000">x</span><span style="color:#666600">-</span><span style="color:#000000">www</span><span style="color:#666600">-</span><span style="color:#000000">form</span><span style="color:#666600">-</span><span style="color:#000000">urlencoded

username</span><span style="color:#666600">=</span><span style="color:#000000">user</span><span style="color:#666600">&</span><span style="color:#000000">password</span><span style="color:#666600">=</span><span style="color:#000000">password

HTTP</span><span style="color:#666600">/</span><span style="color:#006666">1.1</span> <span style="color:#006666">403</span> <span style="color:#660066">Forbidden</span>
<span style="color:#660066">Set</span><span style="color:#666600">-</span><span style="color:#660066">Cookie</span><span style="color:#666600">:</span><span style="color:#000000"> JSESSIONID</span><span style="color:#666600">=</span><span style="color:#006666">3941352C51ABB941781E1DF312DA474E</span><span style="color:#666600">;</span> <span style="color:#660066">Path</span><span style="color:#666600">=/;</span> <span style="color:#660066">HttpOnly</span>
<span style="color:#660066">Content</span><span style="color:#666600">-</span><span style="color:#660066">Type</span><span style="color:#666600">:</span><span style="color:#000000"> application</span><span style="color:#666600">/</span><span style="color:#000000">json</span><span style="color:#666600">;</span><span style="color:#000000">charset</span><span style="color:#666600">=</span><span style="color:#000000">UTF</span><span style="color:#666600">-</span><span style="color:#006666">8</span>
<span style="color:#660066">Transfer</span><span style="color:#666600">-</span><span style="color:#660066">Encoding</span><span style="color:#666600">:</span><span style="color:#000000"> chunked
</span><span style="color:#666600">...</span>

<span style="color:#666600">{</span><span style="color:#008800">"timestamp"</span><span style="color:#666600">:</span><span style="color:#006666">1420467113764</span><span style="color:#666600">,</span><span style="color:#008800">"status"</span><span style="color:#666600">:</span><span style="color:#006666">403</span><span style="color:#666600">,</span><span style="color:#008800">"error"</span><span style="color:#666600">:</span><span style="color:#008800">"Forbidden"</span><span style="color:#666600">,</span><span style="color:#008800">"message"</span><span style="color:#666600">:</span><span style="color:#008800">"Expected CSRF token not found. Has your session expired?"</span><span style="color:#666600">,</span><span style="color:#008800">"path"</span><span style="color:#666600">:</span><span style="color:#008800">"/login"</span><span style="color:#666600">}</span></code></span></span>

这很好,因为这意味着Spring Security的内置CSRF保护措施已经开始,以防止我们在脚下射击。它只需要一个名为“X-CSRF”的标头发送给它的标记。在HttpRequest加载主页的初始请求的属性中,CSRF令牌的值在服务器端可用。要将它传送到客户端,我们可以使用服务器上的动态HTML页面呈现它,或通过自定义端点公开它,否则我们可以将其作为cookie发送。最后一个选择是最好的,因为Angular已经基于cookie 内置了对CSRF(它称之为“XSRF”)的支持。

所以在服务器上我们需要一个自定义过滤器来发送cookie。Angular希望cookie名称为“XSRF-TOKEN”,默认情况下Spring Security将其作为请求属性提供,因此我们只需要将值从请求属性传递到cookie。幸运的是,Spring Security(自4.1.0开始)提供了一个特殊的功能CsrfTokenRepository,它可以完成以下任务:

UiApplication.java

<span style="color:#34302d"><span style="color:#333333"><code class="language-java"><span style="color:#006666">@Configuration</span>
<span style="color:#006666">@Order</span><span style="color:#666600">(</span><span style="color:#660066">SecurityProperties</span><span style="color:#666600">.</span><span style="color:#000000">ACCESS_OVERRIDE_ORDER</span><span style="color:#666600">)</span>
<span style="color:#000088">protected</span> <span style="color:#000088">static</span> <span style="color:#000088">class</span> <span style="color:#660066">SecurityConfiguration</span> <span style="color:#000088">extends</span> <span style="color:#660066">WebSecurityConfigurerAdapter</span> <span style="color:#666600">{</span>
  <span style="color:#006666">@Override</span>
  <span style="color:#000088">protected</span> <span style="color:#000088">void</span><span style="color:#000000"> configure</span><span style="color:#666600">(</span><span style="color:#660066">HttpSecurity</span><span style="color:#000000"> http</span><span style="color:#666600">)</span> <span style="color:#000088">throws</span> <span style="color:#660066">Exception</span> <span style="color:#666600">{</span><span style="color:#000000">
    http
      </span><span style="color:#666600">...</span>
      <span style="color:#666600">.</span><span style="color:#000088">and</span><span style="color:#666600">().</span><span style="color:#000000">csrf</span><span style="color:#666600">()</span>
        <span style="color:#666600">.</span><span style="color:#000000">csrfTokenRepository</span><span style="color:#666600">(</span><span style="color:#660066">CookieCsrfTokenRepository</span><span style="color:#666600">.</span><span style="color:#000000">withHttpOnlyFalse</span><span style="color:#666600">());</span>
  <span style="color:#666600">}</span>
<span style="color:#666600">}</span></code></span></span>

有了这些更改,我们不需要在客户端执行任何操作,登录表单现在正在运行。

它是如何工作的?

如果您使用某些开发人员工具,则可以在浏览器中看到浏览器和后端之间的交互(通常F12打开它,默认情况下在Chrome中运行,可能需要Firefox中的插件)。这是一个总结:

动词路径状态响应

得到

/

200

的index.html

得到

/*.js

200

角度资产

得到

/用户

401

未经授权(被忽略)

得到

/家

200

主页

得到

/用户

401

未经授权(被忽略)

得到

/资源

401

未经授权(被忽略)

得到

/用户

200

发送凭据并获取JSON

得到

/资源

200

JSON问候语

上面标记为“忽略”的响应是Angular在XHR调用中收到的HTML响应,并且由于我们不处理该数据,因此HTML被丢弃在地板上。我们确实在“/ user”资源的情况下寻找经过身份验证的用户,但由于它在第一次调用中不存在,因此该响应被删除。

仔细查看请求,您会看到他们都有cookie。如果你从一个干净的浏览器开始(例如在Chrome中隐身),第一个请求没有cookie去服务器,但是服务器发回“Set-Cookie”为“JSESSIONID”(常规HttpSession)和“X-XSRF” -TOKEN“(我们在上面设置的CRSF cookie)。后续请求都有这些cookie,它们很重要:没有它们,应用程序就无法工作,它们提供了一些非常基本的安全功能(身份验证和CSRF保护)。当用户进行身份验证(POST后)时,cookie的值会发生变化,这是另一个重要的安全功能(防止会话固定攻击)。

 CSRF保护依赖于将cookie发送回服务器是不够的,因为即使您不在从应用程序加载的页面中,浏览器也会自动发送它(跨站点脚本攻击,也称为XSS)。标题不会自动发送,因此原点受到控制。您可能会在我们的应用程序中看到CSRF令牌作为cookie发送到客户端,因此我们将看到它由浏览器自动发回,但它是提供保护的头。

帮助,我的应用程序如何扩展?

“但是等等......”你说,“在单页应用程序中使用会话状态真的不好吗?” 这个问题的答案必须“大部分”,因为使用会话进行身份验证和CSRF保护肯定是一件好事。该状态必须存储在某个地方,如果您将其从会话中取出,则必须将其放在其他位置,并在服务器和客户端上自行手动管理。这只是更多的代码,可能更多的维护,并通常重新发明一个非常好的车轮。

“但是,但是......”你会回答,“我现在如何横向扩展我的应用程序?” 这是你上面提到的“真实”问题,但它往往会缩短为“会话状态不好,我必须是无国籍”。不要惊慌。这里要考虑的重点是安全有状态的。您无法拥有安全的无状态应用程序。那么你要在哪里存储州?这里的所有都是它的。Rob Winch在Spring Exchange 2014上发表了非常有用且富有洞察力的演讲,解释了对状态的需求(以及它的无处不在 - TCP和SSL是有状态的,因此无论你是否知道你的系统都是有状态的),这可能值得一看如果你想更深入地研究这个话题。

好消息是你有一个选择。最简单的选择是将会话数据存储在内存中,并依赖负载均衡器中的粘性会话将来自同一会话的请求路由回相同的JVM(它们都以某种方式支持)。这是够好让你掉在地上,并会为工作真正大量的使用案例。另一种选择是在应用程序的实例之间共享会话数据。只要您是严格的并且只存储安全数据,它就很小并且不经常更改(仅当用户登录和退出,或者他们的会话超时时),因此不应该存在任何重大的基础结构问题。使用Spring Session也很容易。我们将在本系列的下一部分中使用Spring Session,因此没有必要详细介绍如何在此处进行设置,但它实际上是几行代码和一个Redis服务器,它超级快速。

 设置共享会话状态的另一种简单方法是将应用程序作为WAR文件部署到Cloud Foundry Pivotal Web服务,并将其绑定到Redis服务。

但是,我的自定义令牌实现怎么样(它是无状态的,看看)?

如果那是你对上一部分的回应,那么再读一遍,因为也许你第一次没有得到它。如果你将令牌存储在某处,它可能不是无状态的,但即使你没有(例如你使用JWT编码的令牌),你将如何提供CSRF保护?这一点很重要。这是一条经验法则(归功于Rob Winch):如果您的应用程序或API将由浏览器访问,则需要CSRF保护。这并不是说你没有会话就无法做到这一点,只是你必须自己编写所有代码,这是什么意思,因为它已经实现并且在上面完美运行HttpSession(从一开始,它又是你正在使用的容器的一部分,并且已经成为规格的一部分)?即使您决定不需要CSRF,并且拥有完美的“无状态”(非基于会话)令牌实现,您仍然必须在客户端编写额外的代码来使用和使用它,您可以将其委托给浏览器和服务器自带的内置功能:浏览器总是发送cookie,服务器总是有一个会话(除非你关闭它)。这段代码不是商业逻辑,它不会让你赚钱,只是一个开销,所以更糟糕的是,它花费你的钱。

结论

我们现在拥有的应用程序接近于用户在实时环境中的“真实”应用程序中可能期望的内容,并且它可能被用作模板,用于构建具有该体系结构的功能更丰富的应用程序(具有静态的单个服务器)内容和JSON资源)。我们使用HttpSession存储安全数据,依靠我们的客户尊重和使用我们发送的cookie,我们对此感到满意,因为它让我们专注于我们自己的业务领域。在下一节中我们将架构扩展到单独的身份验证和UI服务器,以及JSON的独立资源服务器。这显然很容易推广到多个资源服务器。我们还将把Spring Session引入堆栈,并展示如何使用它来共享身份验证数据。

(三)资源服务器

在本节中,我们将继续讨论如何在“单页面应用程序”中使用带有Angular的Spring Security。在这里,我们首先将我们用作应用程序中的动态内容的“greeting”资源分解为一个单独的服务器,首先作为不受保护的资源,然后由不透明的令牌保护。这是一系列部分中的第三部分,您可以通过阅读第一部分来了解应用程序的基本构建块或从头开始构建它,或者您可以直接访问Github中的源代码,它位于两部分:一部分资源不受保护,另一部分受资产保护。

 如果您正在使用示例应用程序完成此部分,请务必清除Cookie和HTTP Basic凭据的浏览器缓存。在Chrome中,为单个服务器执行此操作的最佳方法是打开新的隐身窗口。

一个单独的资源服务器

客户端更改

在客户端,将资源移动到不同的后端并没有太多的事情要做。这是上一节中的“home”组件:

homeponent.ts

<span style="color:#34302d"><span style="color:#333333"><code class="language-javascript"><span style="color:#006666">@Component</span><span style="color:#666600">({</span><span style="color:#000000">
  templateUrl</span><span style="color:#666600">:</span> <span style="color:#008800">'./homeponent.html'</span>
<span style="color:#666600">})</span>
<span style="color:#000088">export</span> <span style="color:#000088">class</span> <span style="color:#660066">HomeComponent</span> <span style="color:#666600">{</span><span style="color:#000000">

  title </span><span style="color:#666600">=</span> <span style="color:#008800">'Demo'</span><span style="color:#666600">;</span><span style="color:#000000">
  greeting </span><span style="color:#666600">=</span> <span style="color:#666600">{};</span><span style="color:#000000">

  constructor</span><span style="color:#666600">(</span><span style="color:#000088">private</span><span style="color:#000000"> app</span><span style="color:#666600">:</span> <span style="color:#660066">AppService</span><span style="color:#666600">,</span> <span style="color:#000088">private</span><span style="color:#000000"> http</span><span style="color:#666600">:</span> <span style="color:#660066">HttpClient</span><span style="color:#666600">)</span> <span style="color:#666600">{</span><span style="color:#000000">
    http</span><span style="color:#666600">.</span><span style="color:#000088">get</span><span style="color:#666600">(</span><span style="color:#008800">'resource'</span><span style="color:#666600">).</span><span style="color:#000000">subscribe</span><span style="color:#666600">(</span><span style="color:#000000">data </span><span style="color:#666600">=></span> <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">greeting </span><span style="color:#666600">=</span><span style="color:#000000"> data</span><span style="color:#666600">);</span>
  <span style="color:#666600">}</span><span style="color:#000000">

  authenticated</span><span style="color:#666600">()</span> <span style="color:#666600">{</span> <span style="color:#000088">return</span> <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">app</span><span style="color:#666600">.</span><span style="color:#000000">authenticated</span><span style="color:#666600">;</span> <span style="color:#666600">}</span>

<span style="color:#666600">}</span></code></span></span>

我们需要做的就是更改URL。例如,如果我们要在localhost上运行新资源,它可能如下所示:

homeponent.ts

<span style="color:#34302d"><span style="color:#333333"><code class="language-javascript"><span style="color:#000000">        http</span><span style="color:#666600">.</span><span style="color:#000088">get</span><span style="color:#666600">(</span><span style="color:#008800">'http://localhost:9000'</span><span style="color:#666600">).</span><span style="color:#000000">subscribe</span><span style="color:#666600">(</span><span style="color:#000000">data </span><span style="color:#666600">=></span> <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">greeting </span><span style="color:#666600">=</span><span style="color:#000000"> data</span><span style="color:#666600">);</span></code></span></span>

服务器端更改

该UI服务器是微不足道的改变:我们只需要删除@RequestMapping的问候资源(这是“/资源”)。然后我们需要创建一个新的资源服务器,我们可以像使用Spring Boot Initializr在第一部分中那样做。例如在类似UN * X的系统上使用curl:

<span style="color:#34302d"><span style="color:#333333"><code><span style="color:#000000">$ mkdir resource </span><span style="color:#666600">&&</span><span style="color:#000000"> cd resource
$ curl https</span><span style="color:#666600">:</span><span style="color:#880000">//start.spring.io/starter.tgz -d style=web \</span>
<span style="color:#666600">-</span><span style="color:#000000">d name</span><span style="color:#666600">=</span><span style="color:#000000">resource </span><span style="color:#666600">|</span><span style="color:#000000"> tar </span><span style="color:#666600">-</span><span style="color:#000000">xzvf </span><span style="color:#666600">-</span></code></span></span>

然后,您可以将该项目(默认情况下是普通的Maven Java项目)导入您喜欢的IDE,或者只使用命令行中的文件和“mvn”。

只需@RequestMapping在主应用程序类中添加一个,从旧UI复制实现:

ResourceApplication.java

<span style="color:#34302d"><span style="color:#333333"><code class="language-java"><span style="color:#006666">@SpringBootApplication</span>
<span style="color:#006666">@RestController</span>
<span style="color:#000088">class</span> <span style="color:#660066">ResourceApplication</span> <span style="color:#666600">{</span>

  <span style="color:#006666">@RequestMapping</span><span style="color:#666600">(</span><span style="color:#008800">"/"</span><span style="color:#666600">)</span>
  <span style="color:#000088">public</span> <span style="color:#660066">Message</span><span style="color:#000000"> home</span><span style="color:#666600">()</span> <span style="color:#666600">{</span>
    <span style="color:#000088">return</span> <span style="color:#000088">new</span> <span style="color:#660066">Message</span><span style="color:#666600">(</span><span style="color:#008800">"Hello World"</span><span style="color:#666600">);</span>
  <span style="color:#666600">}</span>

  <span style="color:#000088">public</span> <span style="color:#000088">static</span> <span style="color:#000088">void</span><span style="color:#000000"> main</span><span style="color:#666600">(</span><span style="color:#660066">String</span><span style="color:#666600">[]</span><span style="color:#000000"> args</span><span style="color:#666600">)</span> <span style="color:#666600">{</span>
    <span style="color:#660066">SpringApplication</span><span style="color:#666600">.</span><span style="color:#000000">run</span><span style="color:#666600">(</span><span style="color:#660066">ResourceApplication</span><span style="color:#666600">.</span><span style="color:#000088">class</span><span style="color:#666600">,</span><span style="color:#000000"> args</span><span style="color:#666600">);</span>
  <span style="color:#666600">}</span>

<span style="color:#666600">}</span>

<span style="color:#000088">class</span> <span style="color:#660066">Message</span> <span style="color:#666600">{</span>
  <span style="color:#000088">private</span> <span style="color:#660066">String</span><span style="color:#000000"> id </span><span style="color:#666600">=</span><span style="color:#000000"> UUID</span><span style="color:#666600">.</span><span style="color:#000000">randomUUID</span><span style="color:#666600">().</span><span style="color:#000000">toString</span><span style="color:#666600">();</span>
  <span style="color:#000088">private</span> <span style="color:#660066">String</span><span style="color:#000000"> content</span><span style="color:#666600">;</span>
  <span style="color:#000088">public</span> <span style="color:#660066">Message</span><span style="color:#666600">(</span><span style="color:#660066">String</span><span style="color:#000000"> content</span><span style="color:#666600">)</span> <span style="color:#666600">{</span>
    <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">content </span><span style="color:#666600">=</span><span style="color:#000000"> content</span><span style="color:#666600">;</span>
  <span style="color:#666600">}</span>
  <span style="color:#880000">// ... getters and setters and default constructor</span>
<span style="color:#666600">}</span></code></span></span>

完成后,您的应用程序将可以在浏览器中加载。在命令行上,您可以执行此操作

<span style="color:#34302d"><span style="color:#333333"><code><span style="color:#000000">$ mvn spring</span><span style="color:#666600">-</span><span style="color:#000000">boot</span><span style="color:#666600">:</span><span style="color:#000000">run </span><span style="color:#666600">-</span><span style="color:#660066">Dserver</span><span style="color:#666600">.</span><span style="color:#000000">port</span><span style="color:#666600">=</span><span style="color:#006666">9000</span></code></span></span>

并转到http:// localhost:9000的浏览器,您应该看到带有问候语的JSON。您可以在端口更改中进行烘焙application.properties(在“src / main / resources”中):

application.properties

<span style="color:#34302d"><span style="color:#333333"><code class="language-properties"><span style="color:#000000">server</span><span style="color:#666600">.</span><span style="color:#000000">port</span><span style="color:#666600">:</span> <span style="color:#006666">9000</span></code></span></span>

如果您尝试从浏览器中的UI(在端口8080上)加载该资源,您会发现它不起作用,因为浏览器不允许XHR请求。

CORS谈判

浏览器尝试与我们的资源服务器协商,以确定是否允许根据跨源资源共享协议访问它。这不是Angular的责任,所以就像cookie合同一样,它将与浏览器中的所有JavaScript一样工作。这两个服务器没有声明它们具有共同的来源,因此浏览器拒绝发送请求并且UI被破坏。

为了解决这个问题,我们需要支持CORS协议,该协议涉及“飞行前”OPTIONS请求和一些标题,列出调用者允许的行为。Spring 4.2有一些很好的细粒度CORS支持,所以我们可以在控制器映射中添加一个注释,例如:

ResourceApplication.java

<span style="color:#34302d"><span style="color:#333333"><code class="language-java"><span style="color:#006666">@RequestMapping</span><span style="color:#666600">(</span><span style="color:#008800">"/"</span><span style="color:#666600">)</span>
<span style="color:#006666">@CrossOrigin</span><span style="color:#666600">(</span><span style="color:#000000">origins</span><span style="color:#666600">=</span><span style="color:#008800">"*"</span><span style="color:#666600">,</span><span style="color:#000000"> maxAge</span><span style="color:#666600">=</span><span style="color:#006666">3600</span><span style="color:#666600">)</span>
<span style="color:#000088">public</span> <span style="color:#660066">Message</span><span style="color:#000000"> home</span><span style="color:#666600">()</span> <span style="color:#666600">{</span>
  <span style="color:#000088">return</span> <span style="color:#000088">new</span> <span style="color:#660066">Message</span><span style="color:#666600">(</span><span style="color:#008800">"Hello World"</span><span style="color:#666600">);</span>
<span style="color:#666600">}</span></code></span></span>
 巧妙地使用origins=*是快速和肮脏的,并且它可以工作,但它并不安全,并且不以任何方式推荐。

保护资源服务器

大!我们有一个新架构的工作应用程序。唯一的问题是资源服务器没有安全性。

添加Spring Security

我们还可以查看如何将安全性作为过滤器层添加到资源服务器,就像在UI服务器中一样。第一步非常简单:只需将Spring Security添加到Maven POM的类路径中:

的pom.xml

<span style="color:#34302d"><span style="color:#333333"><code class="language-xml"><span style="color:#000088"><dependencies></span>
  <span style="color:#000088"><dependency></span>
    <span style="color:#000088"><groupId></span><span style="color:#000000">org.springframework.boot</span><span style="color:#000088"></groupId></span>
    <span style="color:#000088"><artifactId></span><span style="color:#000000">spring-boot-starter-security</span><span style="color:#000088"></artifactId></span>
  <span style="color:#000088"></dependency></span><span style="color:#000000">
  ...
</span><span style="color:#000088"></dependencies></span></code></span></span>

重新启动资源服务器,嘿presto!它很安全:

<span style="color:#34302d"><span style="color:#333333"><code><span style="color:#000000">$ curl </span><span style="color:#666600">-</span><span style="color:#000000">v localhost</span><span style="color:#666600">:</span><span style="color:#006666">9000</span>
<span style="color:#666600"><</span><span style="color:#000000"> HTTP</span><span style="color:#666600">/</span><span style="color:#006666">1.1</span> <span style="color:#006666">302</span> <span style="color:#660066">Found</span>
<span style="color:#666600"><</span> <span style="color:#660066">Location</span><span style="color:#666600">:</span><span style="color:#000000"> http</span><span style="color:#666600">:</span><span style="color:#880000">//localhost:9000/login</span>
<span style="color:#666600">...</span></code></span></span>

我们正在重定向到(whitelabel)登录页面,因为curl没有发送与Angular客户端相同的头文件。修改命令以发送更多类似的标头:

<span style="color:#34302d"><span style="color:#333333"><code><span style="color:#000000">$ curl </span><span style="color:#666600">-</span><span style="color:#000000">v </span><span style="color:#666600">-</span><span style="color:#000000">H </span><span style="color:#008800">"Accept: application/json"</span><span style="color:#000000"> \
    </span><span style="color:#666600">-</span><span style="color:#000000">H </span><span style="color:#008800">"X-Requested-With: XMLHttpRequest"</span><span style="color:#000000"> localhost</span><span style="color:#666600">:</span><span style="color:#006666">9000</span>
<span style="color:#666600"><</span><span style="color:#000000"> HTTP</span><span style="color:#666600">/</span><span style="color:#006666">1.1</span> <span style="color:#006666">401</span> <span style="color:#660066">Unauthorized</span>
<span style="color:#666600">...</span></code></span></span>

所以我们需要做的就是教会客户端发送每个请求的凭据。

令牌认证

互联网和人们的Spring后端项目充斥着基于自定义令牌的身份验证解决方案。Spring Security提供了一个准确的Filter实现,让您自己开始(参见例如AbstractPreAuthenticatedProcessingFilterTokenService)。虽然Spring Security中没有规范的实现,但其中一个原因可能是更简单的方法。

请记住,本系列的第二部分中,Spring Security HttpSession默认使用它来存储身份验证数据。它不会直接与会话交互:中间有一个抽象层(SecurityContextRepository),您可以使用它来更改存储后端。如果我们可以在我们的资源服务器中将该存储库指向具有由我们的UI验证的身份验证的商店,那么我们可以在两个服务器之间共享身份验证。UI服务器已经有了这样的商店(HttpSession),所以如果我们可以分发该商店并将其打开到资源服务器,我们就拥有了大部分解决方案。

春季会议

使用Spring Session,解决方案的这一部分非常简单。我们所需要的只是一个共享数据存储(Redis和JDBC支持开箱即用),以及服务器中的几行配置来设置Filter

在UI应用程序中,我们需要向POM添加一些依赖项:

的pom.xml

<span style="color:#34302d"><span style="color:#333333"><code class="language-xml"><span style="color:#000088"><dependency></span>
  <span style="color:#000088"><groupId></span><span style="color:#000000">org.springframework.session</span><span style="color:#000088"></groupId></span>
  <span style="color:#000088"><artifactId></span><span style="color:#000000">spring-session</span><span style="color:#000088"></artifactId></span>
<span style="color:#000088"></dependency></span>
<span style="color:#000088"><dependency></span>
  <span style="color:#000088"><groupId></span><span style="color:#000000">org.springframework.boot</span><span style="color:#000088"></groupId></span>
  <span style="color:#000088"><artifactId></span><span style="color:#000000">spring-boot-starter-data-redis</span><span style="color:#000088"></artifactId></span>
<span style="color:#000088"></dependency></span></code></span></span>

Spring Boot和Spring Session一起工作以连接到Redis并集中存储会话数据。

使用这一行代码并在localhost上运行Redis服务器,您可以运行UI应用程序,使用一些有效的用户凭据登录,并且会话数据(身份验证)将存储在redis中。

 如果您没有在本地运行的redis服务器,您可以使用Docker轻松地启动它(在Windows或MacOS上这需要一个VM)。在Githubdocker-compose.yml的源代码中有一个文件,您可以在命令行上轻松运行docker-compose up。如果在VM中执行此操作,Redis服务器将在与localhost不同的主机上运行,​​因此您需要将其隧道传输到localhost,或者将应用程序配置为指向正确spring.redis.hostapplication.properties

从UI发送自定义标记

唯一缺失的部分是商店中数据密钥的传输机制。关键是HttpSessionID,因此如果我们可以在UI客户端中获取该密钥,我们可以将其作为自定义标头发送到资源服务器。因此,“home”控制器需要进行更改,以便它将标头作为问候资源的HTTP请求的一部分发送。例如:

homeponent.ts

<span style="color:#34302d"><span style="color:#333333"><code class="language-javascript"><span style="color:#000000">  constructor</span><span style="color:#666600">(</span><span style="color:#000088">private</span><span style="color:#000000"> app</span><span style="color:#666600">:</span> <span style="color:#660066">AppService</span><span style="color:#666600">,</span> <span style="color:#000088">private</span><span style="color:#000000"> http</span><span style="color:#666600">:</span> <span style="color:#660066">HttpClient</span><span style="color:#666600">)</span> <span style="color:#666600">{</span><span style="color:#000000">
    http</span><span style="color:#666600">.</span><span style="color:#000088">get</span><span style="color:#666600">(</span><span style="color:#008800">'token'</span><span style="color:#666600">).</span><span style="color:#000000">subscribe</span><span style="color:#666600">(</span><span style="color:#000000">data </span><span style="color:#666600">=></span> <span style="color:#666600">{</span>
      <span style="color:#000088">const</span><span style="color:#000000"> token </span><span style="color:#666600">=</span><span style="color:#000000"> data</span><span style="color:#666600">[</span><span style="color:#008800">'token'</span><span style="color:#666600">];</span><span style="color:#000000">
      http</span><span style="color:#666600">.</span><span style="color:#000088">get</span><span style="color:#666600">(</span><span style="color:#008800">'http://localhost:9000'</span><span style="color:#666600">,</span> <span style="color:#666600">{</span><span style="color:#000000">headers </span><span style="color:#666600">:</span> <span style="color:#000088">new</span> <span style="color:#660066">HttpHeaders</span><span style="color:#666600">().</span><span style="color:#000088">set</span><span style="color:#666600">(</span><span style="color:#008800">'X-Auth-Token'</span><span style="color:#666600">,</span><span style="color:#000000"> token</span><span style="color:#666600">)})</span>
        <span style="color:#666600">.</span><span style="color:#000000">subscribe</span><span style="color:#666600">(</span><span style="color:#000000">response </span><span style="color:#666600">=></span> <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">greeting </span><span style="color:#666600">=</span><span style="color:#000000"> response</span><span style="color:#666600">);</span>
    <span style="color:#666600">},</span> <span style="color:#666600">()</span> <span style="color:#666600">=></span> <span style="color:#666600">{});</span>
  <span style="color:#666600">}</span></code></span></span>

(更优雅的解决方案可能是根据需要获取令牌,并使用我们RequestOptionsService将标头添加到资源服务器的每个请求。)

我们没有直接转到“http:// localhost:9000 [ http:// localhost:9000 ]”,而是在“/ token”处对UI服务器上的新自定义端点的调用成功回调中包含该调用。实现这一点很简单:

UiApplication.java

<span style="color:#34302d"><span style="color:#333333"><code class="language-java"><span style="color:#006666">@SpringBootApplication</span>
<span style="color:#006666">@RestController</span>
<span style="color:#000088">public</span> <span style="color:#000088">class</span> <span style="color:#660066">UiApplication</span> <span style="color:#666600">{</span>

  <span style="color:#000088">public</span> <span style="color:#000088">static</span> <span style="color:#000088">void</span><span style="color:#000000"> main</span><span style="color:#666600">(</span><span style="color:#660066">String</span><span style="color:#666600">[]</span><span style="color:#000000"> args</span><span style="color:#666600">)</span> <span style="color:#666600">{</span>
    <span style="color:#660066">SpringApplication</span><span style="color:#666600">.</span><span style="color:#000000">run</span><span style="color:#666600">(</span><span style="color:#660066">UiApplication</span><span style="color:#666600">.</span><span style="color:#000088">class</span><span style="color:#666600">,</span><span style="color:#000000"> args</span><span style="color:#666600">);</span>
  <span style="color:#666600">}</span>

  <span style="color:#666600">...</span>

  <span style="color:#006666">@RequestMapping</span><span style="color:#666600">(</span><span style="color:#008800">"/token"</span><span style="color:#666600">)</span>
  <span style="color:#000088">public</span> <span style="color:#660066">Map</span><span style="color:#666600"><</span><span style="color:#660066">String</span><span style="color:#666600">,</span><span style="color:#660066">String</span><span style="color:#666600">></span><span style="color:#000000"> token</span><span style="color:#666600">(</span><span style="color:#660066">HttpSession</span><span style="color:#000000"> session</span><span style="color:#666600">)</span> <span style="color:#666600">{</span>
    <span style="color:#000088">return</span> <span style="color:#660066">Collections</span><span style="color:#666600">.</span><span style="color:#000000">singletonMap</span><span style="color:#666600">(</span><span style="color:#008800">"token"</span><span style="color:#666600">,</span><span style="color:#000000"> session</span><span style="color:#666600">.</span><span style="color:#000000">getId</span><span style="color:#666600">());</span>
  <span style="color:#666600">}</span>

<span style="color:#666600">}</span></code></span></span>

因此,UI应用程序已准备就绪,并将会话ID包含在名为“X-Auth-Token”的标头中,用于对后端的所有调用。

资源服务器中的身份验证

资源服务器有一个微小的变化,它可以接受自定义标头。CORS配置必须将该标头指定为来自远程客户端的允许标头,例如

ResourceApplication.java

<span style="color:#34302d"><span style="color:#333333"><code class="language-java"><span style="color:#006666">@RequestMapping</span><span style="color:#666600">(</span><span style="color:#008800">"/"</span><span style="color:#666600">)</span>
<span style="color:#006666">@CrossOrigin</span><span style="color:#666600">(</span><span style="color:#000000">origins </span><span style="color:#666600">=</span> <span style="color:#008800">"*"</span><span style="color:#666600">,</span><span style="color:#000000"> maxAge </span><span style="color:#666600">=</span> <span style="color:#006666">3600</span><span style="color:#666600">,</span><span style="color:#000000">
    allowedHeaders</span><span style="color:#666600">={</span><span style="color:#008800">"x-auth-token"</span><span style="color:#666600">,</span> <span style="color:#008800">"x-requested-with"</span><span style="color:#666600">,</span> <span style="color:#008800">"x-xsrf-token"</span><span style="color:#666600">})</span>
<span style="color:#000088">public</span> <span style="color:#660066">Message</span><span style="color:#000000"> home</span><span style="color:#666600">()</span> <span style="color:#666600">{</span>
  <span style="color:#000088">return</span> <span style="color:#000088">new</span> <span style="color:#660066">Message</span><span style="color:#666600">(</span><span style="color:#008800">"Hello World"</span><span style="color:#666600">);</span>
<span style="color:#666600">}</span></code></span></span>

从浏览器开始的飞行前检查现在将由Spring MVC处理,但是我们需要告诉Spring Security允​​许它通过:

ResourceApplication.java

<span style="color:#34302d"><span style="color:#333333"><code class="language-java"><span style="color:#000088">public</span> <span style="color:#000088">class</span> <span style="color:#660066">ResourceApplication</span> <span style="color:#000088">extends</span> <span style="color:#660066">WebSecurityConfigurerAdapter</span> <span style="color:#666600">{</span>

  <span style="color:#006666">@Override</span>
  <span style="color:#000088">protected</span> <span style="color:#000088">void</span><span style="color:#000000"> configure</span><span style="color:#666600">(</span><span style="color:#660066">HttpSecurity</span><span style="color:#000000"> http</span><span style="color:#666600">)</span> <span style="color:#000088">throws</span> <span style="color:#660066">Exception</span> <span style="color:#666600">{</span><span style="color:#000000">
    http</span><span style="color:#666600">.</span><span style="color:#000000">cors</span><span style="color:#666600">().</span><span style="color:#000088">and</span><span style="color:#666600">().</span><span style="color:#000000">authorizeRequests</span><span style="color:#666600">()</span>
      <span style="color:#666600">.</span><span style="color:#000000">anyRequest</span><span style="color:#666600">().</span><span style="color:#000000">authenticated</span><span style="color:#666600">();</span>
  <span style="color:#666600">}</span>

  <span style="color:#666600">...</span></code></span></span>
 无需permitAll()访问所有资源,并且可能存在无意中发送敏感数据的处理程序,因为它不知道请求是在飞行前。该cors()配置实用程序通过处理的过滤层的所有飞行前请求减轻这一点。

剩下的就是在资源服务器中获取自定义令牌并使用它来验证我们的用户。事实证明这非常简单,因为我们需要做的就是告诉Spring Security会话存储库在哪里,以及在哪里查找传入请求中的令牌(会话ID)。首先我们需要添加Spring Session和Redis依赖项,然后我们可以设置Filter

ResourceApplication.java

<span style="color:#34302d"><span style="color:#333333"><code class="language-java"><span style="color:#006666">@SpringBootApplication</span>
<span style="color:#006666">@RestController</span>
<span style="color:#000088">class</span> <span style="color:#660066">ResourceApplication</span> <span style="color:#666600">{</span>

  <span style="color:#666600">...</span>

  <span style="color:#006666">@Bean</span>
  <span style="color:#660066">HeaderHttpSessionStrategy</span><span style="color:#000000"> sessionStrategy</span><span style="color:#666600">()</span> <span style="color:#666600">{</span>
    <span style="color:#000088">return</span> <span style="color:#000088">new</span> <span style="color:#660066">HeaderHttpSessionStrategy</span><span style="color:#666600">();</span>
  <span style="color:#666600">}</span>

<span style="color:#666600">}</span></code></span></span>

Filter创建是一个在UI服务器的镜像,所以它建立的Redis作为会话存储。唯一的区别是它使用HttpSessionStrategy在标题中查找的自定义(默认情况下为“X-Auth-Token”)而不是默认值(名为“JSESSIONID”的cookie)。我们还需要阻止浏览器在未经身份验证的客户端中弹出一个对话框 - 该应用程序是安全的,但WWW-Authenticate: Basic默认情况下会发送一个401 ,因此浏览器会响应用户名和密码的对话框。实现这一目标的方法不止一种,但我们已经让Angular发送了一个“X-Requested-With”标头,因此Spring Security会默认为它处理它。

资源服务器有一个最终更改,以使其与我们的新身份验证方案一起使用。Spring Boot默认安全性是无状态的,我们希望它在会话中存储身份验证,因此我们需要在application.yml(或application.properties)中显式:

application.yml

<span style="color:#34302d"><span style="color:#333333"><code class="language-yaml"><span style="color:#000000">security</span><span style="color:#666600">:</span><span style="color:#000000">
  sessions</span><span style="color:#666600">:</span><span style="color:#000000"> NEVER</span></code></span></span>

这告诉Spring Security“永远不会创建会话,但如果它在那里则使用一个”(由于UI中的身份验证,它已经存在)。

重新启动资源服务器并在新的浏览器窗口中打开UI。

为什么不能全部使用Cookie?

我们必须使用自定义标头并在客户端中编写代码来填充标题,这并不是非常复杂,但它似乎与第二部分中的建议相矛盾,即尽可能使用cookie和会话。那里的论点是不这样做会带来额外的不必要的复杂性,而且我们现在实现的确实是迄今为止我们看到的最复杂的:解决方案的技术部分远远超过业务逻辑(这无疑是微不足道的)。这绝对是一个公平的批评(我们打算在本系列的下一部分中讨论),但我们只是简单地看一下为什么它不像只使用cookie和会话一样简单。

至少我们仍在使用会话,这是有道理的,因为Spring Security和Servlet容器知道如何做到这一点,我们不费吹灰之力。但我们不能继续使用cookie来传输身份验证令牌吗?它会很好,但有一个原因它不起作用,那就是浏览器不会让我们。您可以从JavaScript客户端浏览浏览器的cookie存储区,但是有一些限制,并且有充分的理由。特别是,您无权访问服务器以“HttpOnly”发送的cookie(默认情况下,您将看到会话cookie的情况)。您也无法在传出请求中设置cookie,因此我们无法设置“SESSION”cookie(这是Spring Session的默认cookie名称),我们必须使用自定义的“X-Session” 头。这些限制都是为了您自己的保护,因此如果没有适当的授权,恶意脚本无法访问您的资源。

TL; DR UI和资源服务器没有共同的来源,所以他们不能共享cookie(即使我们可以使用Spring Session来强制他们共享会话)。

结论

我们在本系列的第II部分中复制了应用程序的功能:从远程后端获取问候语的主页,导航栏中有登录和注销链接。不同之处在于问候语来自独立的资源服务器,而不是嵌入在UI服务器中。这增加了实现的复杂性,但好消息是我们有一个主要基于配置(实际上是100%声明)的解决方案。我们甚至可以通过将所有新代码提取到库中来使解决方案100%声明(Spring配置和Angular自定义指令)。在接下来的几期分期之后,我们将推迟这项有趣的任务。在下一节中 我们将看一个不同的非常好的方法来减少当前实现中的所有复杂性:API网关模式(客户端将其所有请求发送到一个地方并在那里处理身份验证)。

 我们在这里使用Spring Session来共享两个逻辑上不同的应用程序服务器之间的会话。这是一个巧妙的技巧,并且“常规”JEE分布式会话无法实现。

(四)API网关

在本节中,我们将继续讨论如何在“单页面应用程序”中使用带有Angular的Spring Security。在这里,我们将展示如何使用Spring Cloud构建API网关来控制身份验证和访问后端资源。这是一系列部分中的第四部分,您可以通过阅读第一部分来了解应用程序的基本构建块或从头开始构建它,或者您可以直接访问Github中的源代码。在上一节中,我们构建了一个使用Spring Session的简单分布式应用程序验证后端资源。在这个中,我们将UI服务器变为后端资源服务器的反向代理,修复上一个实现的问题(自定义令牌认证引入的技术复杂性),并为我们提供了许多用于控制来自浏览器客户端的访问的新选项。

提醒:如果您正在使用示例应用程序完成此部分,请务必清除Cookie和HTTP Basic凭据的浏览器缓存。在Chrome中,为单个服务器执行此操作的最佳方法是打开新的隐身窗口。

创建API网关

API网关是前端客户端的单一入口点(和控制点),可以是基于浏览器的(如本节中的示例)或移动设备。客户端只需要知道一个服务器的URL,后端可以随意重构而不会发生变化,这是一个重要的优势。在集中和控制方面还有其他优势:速率限制,身份验证,审计和日志记录。使用Spring Cloud实现简单的反向代理非常简单。

如果你在代码中跟随,你就会知道最后一节末尾的应用程序实现有点复杂,所以它不是一个很好的迭代之处。然而,有一个中间点,我们可以更容易地开始,其中后端资源尚未通过Spring Security获得。这个源代码是Github中的一个单独的项目,所以我们将从那里开始。它有一个UI服务器和一个资源服务器,他们正在互相交谈。资源服务器还没有Spring Security,所以我们可以让系统首先运行,然后添加该层。

一行中的声明性反向代理

要将其转换为API网关,UI服务器需要一个小的调整。在Spring配置的某个地方,我们需要添加一个@EnableZuulProxy注释,例如在主(仅)应用程序类中:

UiApplication.java

<span style="color:#34302d"><span style="color:#333333"><code class="language-java"><span style="color:#006666">@SpringBootApplication</span>
<span style="color:#006666">@RestController</span>
<span style="color:#006666">@EnableZuulProxy</span>
<span style="color:#000088">public</span> <span style="color:#000088">class</span> <span style="color:#660066">UiApplication</span> <span style="color:#666600">{</span>
  <span style="color:#666600">...</span>
<span style="color:#666600">}</span></code></span></span>

在外部配置文件中,我们需要将UI服务器中的本地资源映射到外部配置中的远程资源(“application.yml”):

application.yml

<span style="color:#34302d"><span style="color:#333333"><code class="language-yaml"><span style="color:#000000">security</span><span style="color:#666600">:</span>
  <span style="color:#666600">...</span><span style="color:#000000">
zuul</span><span style="color:#666600">:</span><span style="color:#000000">
  routes</span><span style="color:#666600">:</span><span style="color:#000000">
    resource</span><span style="color:#666600">:</span><span style="color:#000000">
      path</span><span style="color:#666600">:</span> <span style="color:#008800">/resource/</span><span style="color:#666600">**</span><span style="color:#000000">
      url</span><span style="color:#666600">:</span><span style="color:#000000"> http</span><span style="color:#666600">:</span><span style="color:#880000">//localhost:9000</span></code></span></span>

这表示“将此服务器中带有pattern / resource / **的路径映射到localhost:9000上远程服务器中的相同路径”。简单但有效(好吧,所以它包括YAML的6行,但你并不总是需要它)!

我们需要做的就是在classpath上做正确的事情。为此,我们在Maven POM中有一些新行:

的pom.xml

<span style="color:#34302d"><span style="color:#333333"><code class="language-xml"><span style="color:#000088"><dependencyManagement></span>
  <span style="color:#000088"><dependencies></span>
    <span style="color:#000088"><dependency></span>
      <span style="color:#000088"><groupId></span><span style="color:#000000">org.springframework.cloud</span><span style="color:#000088"></groupId></span>
      <span style="color:#000088"><artifactId></span><span style="color:#000000">spring-cloud-dependencies</span><span style="color:#000088"></artifactId></span>
      <span style="color:#000088"><version></span><span style="color:#000000">Dalston.SR4</span><span style="color:#000088"></version></span>
      <span style="color:#000088"><type></span><span style="color:#000000">pom</span><span style="color:#000088"></type></span>
      <span style="color:#000088"><scope></span><span style="color:#000000">import</span><span style="color:#000088"></scope></span>
    <span style="color:#000088"></dependency></span>
  <span style="color:#000088"></dependencies></span>
<span style="color:#000088"></dependencyManagement></span>

<span style="color:#000088"><dependencies></span>
  <span style="color:#000088"><dependency></span>
    <span style="color:#000088"><groupId></span><span style="color:#000000">org.springframework.cloud</span><span style="color:#000088"></groupId></span>
    <span style="color:#000088"><artifactId></span><span style="color:#000000">spring-cloud-starter-zuul</span><span style="color:#000088"></artifactId></span>
  <span style="color:#000088"></dependency></span><span style="color:#000000">
  ...
</span><span style="color:#000088"></dependencies></span></code></span></span>

注意使用“spring-cloud-starter-zuul” - 它就像Spring Boot那样是一个初学者POM,但它控制着我们对这个Zuul代理所需的依赖。我们也在使用,<dependencyManagement>因为我们希望能够依赖所有传递依赖的版本是正确的。

在客户端中使用代理

有了这些更改,我们的应用程序仍然有效,但在修改客户端之前,我们还没有实际使用过新的代理。幸运的是,这是微不足道的。我们只需要将上一节中从“单一”到“香草”样本所做的更改还原为:

homeponent.ts

<span style="color:#34302d"><span style="color:#333333"><code class="language-javascript"><span style="color:#000000">constructor</span><span style="color:#666600">(</span><span style="color:#000088">private</span><span style="color:#000000"> app</span><span style="color:#666600">:</span> <span style="color:#660066">AppService</span><span style="color:#666600">,</span> <span style="color:#000088">private</span><span style="color:#000000"> http</span><span style="color:#666600">:</span> <span style="color:#660066">HttpClient</span><span style="color:#666600">)</span> <span style="color:#666600">{</span><span style="color:#000000">
  http</span><span style="color:#666600">.</span><span style="color:#000088">get</span><span style="color:#666600">(</span><span style="color:#008800">'resource'</span><span style="color:#666600">).</span><span style="color:#000000">subscribe</span><span style="color:#666600">(</span><span style="color:#000000">data </span><span style="color:#666600">=></span> <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">greeting </span><span style="color:#666600">=</span><span style="color:#000000"> data</span><span style="color:#666600">);</span>
<span style="color:#666600">}</span></code></span></span>

现在,当我们启动服务器时,一切正常,请求通过UI(API网关)代理到资源服务器。

进一步简化

更好的是:我们在资源服务器中不再需要CORS过滤器。无论如何,我们很快将这一个扔到了一起,它应该是一个红灯,我们不得不做任何技术上专注的事情(尤其是涉及安全的地方)。幸运的是它现在已经多余了,所以我们可以扔掉它,然后晚上回去睡觉吧!

保护资源服务器

您可能还记得我们从中间状态开始,资源服务器没有安全性。

除此之外:如果您的网络体系结构镜像应用程序体系结构,那么缺乏软件安全性甚至可能不成问题(除了UI服务器之外,您可以使资源服务器在物理上无法访问)。作为一个简单的演示,我们可以使资源服务器只能在localhost上访问。只需将其添加到application.properties资源服务器中:

application.properties

<span style="color:#34302d"><span style="color:#333333"><code><span style="color:#000000">server</span><span style="color:#666600">.</span><span style="color:#000000">address</span><span style="color:#666600">:</span> <span style="color:#006666">127.0</span><span style="color:#666600">.</span><span style="color:#006666">0.1</span></code></span></span>

哇,这很容易!使用仅在数据中心中可见的网络地址并且您拥有适用于所有资源服务器和所有用户桌面的安全解决方案。

假设我们确定我们确实需要软件级别的安全性(很可能由于多种原因)。这不会成为问题,因为我们需要做的就是将Spring Security添加为依赖项(在资源服务器POM中):

的pom.xml

<span style="color:#34302d"><span style="color:#333333"><code class="language-xml"><span style="color:#000088"><dependency></span>
  <span style="color:#000088"><groupId></span><span style="color:#000000">org.springframework.boot</span><span style="color:#000088"></groupId></span>
  <span style="color:#000088"><artifactId></span><span style="color:#000000">spring-boot-starter-security</span><span style="color:#000088"></artifactId></span>
<span style="color:#000088"></dependency></span></code></span></span>

这足以让我们成为一个安全的资源服务器,但它不会让我们成为一个有用的应用程序,原因与它在第三部分中没有相同:两个服务器之间没有共享的身份验证状态。

共享验证状态

我们可以使用相同的机制来共享身份验证(和CSRF)状态,就像我们在上一次会话中所做的那样,即Spring Session。我们像以前一样将依赖项添加到两个服务器:

的pom.xml

<span style="color:#34302d"><span style="color:#333333"><code class="language-xml"><span style="color:#000088"><dependency></span>
  <span style="color:#000088"><groupId></span><span style="color:#000000">org.springframework.session</span><span style="color:#000088"></groupId></span>
  <span style="color:#000088"><artifactId></span><span style="color:#000000">spring-session</span><span style="color:#000088"></artifactId></span>
<span style="color:#000088"></dependency></span>
<span style="color:#000088"><dependency></span>
  <span style="color:#000088"><groupId></span><span style="color:#000000">org.springframework.boot</span><span style="color:#000088"></groupId></span>
  <span style="color:#000088"><artifactId></span><span style="color:#000000">spring-boot-starter-redis</span><span style="color:#000088"></artifactId></span>
<span style="color:#000088"></dependency></span></code></span></span>

但这次配置要简单得多,因为我们可以Filter向两者添加相同的声明。首先是UI服务器,明确声明我们希望转发所有标头(即没有“敏感”):

application.yml

<span style="color:#34302d"><span style="color:#333333"><code class="language-properties"><span style="color:#000000">zuul</span><span style="color:#666600">:</span><span style="color:#000000">
  routes</span><span style="color:#666600">:</span><span style="color:#000000">
    resource</span><span style="color:#666600">:</span><span style="color:#000000">
      sensitive</span><span style="color:#666600">-</span><span style="color:#000000">headers</span><span style="color:#666600">:</span></code></span></span>

然后我们可以继续前进到资源服务器。要进行两项小的更改:一种是在资源服务器中显式禁用HTTP Basic(以防止浏览器弹出身份验证对话框):

ResourceApplication.java

<span style="color:#34302d"><span style="color:#333333"><code class="language-java"><span style="color:#006666">@SpringBootApplication</span>
<span style="color:#006666">@RestController</span>
<span style="color:#000088">class</span> <span style="color:#660066">ResourceApplication</span> <span style="color:#000088">extends</span> <span style="color:#660066">WebSecurityConfigurerAdapter</span> <span style="color:#666600">{</span>

  <span style="color:#666600">...</span>

  <span style="color:#006666">@Override</span>
  <span style="color:#000088">protected</span> <span style="color:#000088">void</span><span style="color:#000000"> configure</span><span style="color:#666600">(</span><span style="color:#660066">HttpSecurity</span><span style="color:#000000"> http</span><span style="color:#666600">)</span> <span style="color:#000088">throws</span> <span style="color:#660066">Exception</span> <span style="color:#666600">{</span><span style="color:#000000">
    http</span><span style="color:#666600">.</span><span style="color:#000000">httpBasic</span><span style="color:#666600">().</span><span style="color:#000000">disable</span><span style="color:#666600">();</span><span style="color:#000000">
    http</span><span style="color:#666600">.</span><span style="color:#000000">authorizeRequests</span><span style="color:#666600">().</span><span style="color:#000000">anyRequest</span><span style="color:#666600">().</span><span style="color:#000000">authenticated</span><span style="color:#666600">();</span>
  <span style="color:#666600">}</span>

<span style="color:#666600">}</span></code></span></span>

除此之外:另一个也会阻止身份验证对话框的替代方案是保持HTTP Basic,但将401挑战更改为“Basic”之外的其他内容。您可以使用配置回调AuthenticationEntryPoint中的单行实现来完成此操作HttpSecurity

另一种是在以下方面明确要求非无状态会话创建策略application.properties

application.properties

<span style="color:#34302d"><span style="color:#333333"><code class="language-properties"><span style="color:#000000">security</span><span style="color:#666600">.</span><span style="color:#000000">sessions</span><span style="color:#666600">:</span><span style="color:#000000"> NEVER</span></code></span></span>

只要redis仍然在后台运行(docker-compose.yml如果你想启动它就使用它),那么系统就可以工作了。在http:// localhost:8080加载UI的主页并登录,您将在主页上看到来自后端的消息。

它是如何工作的?

现在幕后发生了什么?首先,我们可以查看UI服务器(和API网关)中的HTTP请求:

动词路径状态响应

得到

/

200

的index.html

得到

/*.js

200

资产形成角度

得到

/用户

401

未经授权(被忽略)

得到

/资源

401

未经身份验证的资源访问权限

得到

/用户

200

JSON认证用户

得到

/资源

200

(Proxied)JSON问候语

这与第二部分末尾的序列完全相同,只是因为我们使用的是Spring会话,所以cookie名称略有不同(“SESSION”而不是“JSESSIONID”)。但是体系结构是不同的,并且对“/ resource”的最后请求是特殊的,因为它被代理到资源服务器。

我们可以通过查看UI服务器中的“/ trace”端点(来自Spring Boot Actuator,我们添加了Spring Cloud依赖项)来查看反向代理。在新的浏览器中转到http:// localhost:8080 / trace(如果你没有一个已经为你的浏览器获得一个JSON插件,使它变得美观和可读)。您需要使用HTTP Basic(浏览器弹出窗口)进行身份验证,但相同的凭据对您的登录表单有效。在开始或接近开始时,你应该看到一对这样的请求:

 尝试使用不同的浏览器,以便无法进行身份验证交叉(例如,如果您使用Chrome浏览器测试用户界面,则使用Firefox) - 它不会阻止应用程序运行,但如果它们包含,则会使跟踪难以阅读来自同一浏览器的身份验证混合。

/跟踪

<span style="color:#34302d"><span style="color:#333333"><code class="language-javascript"><span style="color:#666600">{</span>
  <span style="color:#008800">"timestamp"</span><span style="color:#666600">:</span> <span style="color:#006666">1420558194546</span><span style="color:#666600">,</span>
  <span style="color:#008800">"info"</span><span style="color:#666600">:</span> <span style="color:#666600">{</span>
    <span style="color:#008800">"method"</span><span style="color:#666600">:</span> <span style="color:#008800">"GET"</span><span style="color:#666600">,</span>
    <span style="color:#008800">"path"</span><span style="color:#666600">:</span> <span style="color:#008800">"/"</span><span style="color:#666600">,</span>
    <span style="color:#008800">"query"</span><span style="color:#666600">:</span> <span style="color:#008800">""</span>
    <span style="color:#008800">"remote"</span><span style="color:#666600">:</span> <span style="color:#000088">true</span><span style="color:#666600">,</span>
    <span style="color:#008800">"proxy"</span><span style="color:#666600">:</span> <span style="color:#008800">"resource"</span><span style="color:#666600">,</span>
    <span style="color:#008800">"headers"</span><span style="color:#666600">:</span> <span style="color:#666600">{</span>
      <span style="color:#008800">"request"</span><span style="color:#666600">:</span> <span style="color:#666600">{</span>
        <span style="color:#008800">"accept"</span><span style="color:#666600">:</span> <span style="color:#008800">"application/json, text/plain, */*"</span><span style="color:#666600">,</span>
        <span style="color:#008800">"x-xsrf-token"</span><span style="color:#666600">:</span> <span style="color:#008800">"542c7005-309c-4f50-8a1d-d6c74afe8260"</span><span style="color:#666600">,</span>
        <span style="color:#008800">"cookie"</span><span style="color:#666600">:</span> <span style="color:#008800">"SESSION=c18846b5-f805-4679-9820-cd13bd83be67; XSRF-TOKEN=542c7005-309c-4f50-8a1d-d6c74afe8260"</span><span style="color:#666600">,</span>
        <span style="color:#008800">"x-forwarded-prefix"</span><span style="color:#666600">:</span> <span style="color:#008800">"/resource"</span><span style="color:#666600">,</span>
        <span style="color:#008800">"x-forwarded-host"</span><span style="color:#666600">:</span> <span style="color:#008800">"localhost:8080"</span>
      <span style="color:#666600">},</span>
      <span style="color:#008800">"response"</span><span style="color:#666600">:</span> <span style="color:#666600">{</span>
        <span style="color:#008800">"Content-Type"</span><span style="color:#666600">:</span> <span style="color:#008800">"application/json;charset=UTF-8"</span><span style="color:#666600">,</span>
        <span style="color:#008800">"status"</span><span style="color:#666600">:</span> <span style="color:#008800">"200"</span>
      <span style="color:#666600">}</span>
    <span style="color:#666600">},</span>
  <span style="color:#666600">}</span>
<span style="color:#666600">},</span>
<span style="color:#666600">{</span>
  <span style="color:#008800">"timestamp"</span><span style="color:#666600">:</span> <span style="color:#006666">1420558200232</span><span style="color:#666600">,</span>
  <span style="color:#008800">"info"</span><span style="color:#666600">:</span> <span style="color:#666600">{</span>
    <span style="color:#008800">"method"</span><span style="color:#666600">:</span> <span style="color:#008800">"GET"</span><span style="color:#666600">,</span>
    <span style="color:#008800">"path"</span><span style="color:#666600">:</span> <span style="color:#008800">"/resource/"</span><span style="color:#666600">,</span>
    <span style="color:#008800">"headers"</span><span style="color:#666600">:</span> <span style="color:#666600">{</span>
      <span style="color:#008800">"request"</span><span style="color:#666600">:</span> <span style="color:#666600">{</span>
        <span style="color:#008800">"host"</span><span style="color:#666600">:</span> <span style="color:#008800">"localhost:8080"</span><span style="color:#666600">,</span>
        <span style="color:#008800">"accept"</span><span style="color:#666600">:</span> <span style="color:#008800">"application/json, text/plain, */*"</span><span style="color:#666600">,</span>
        <span style="color:#008800">"x-xsrf-token"</span><span style="color:#666600">:</span> <span style="color:#008800">"542c7005-309c-4f50-8a1d-d6c74afe8260"</span><span style="color:#666600">,</span>
        <span style="color:#008800">"cookie"</span><span style="color:#666600">:</span> <span style="color:#008800">"SESSION=c18846b5-f805-4679-9820-cd13bd83be67; XSRF-TOKEN=542c7005-309c-4f50-8a1d-d6c74afe8260"</span>
      <span style="color:#666600">},</span>
      <span style="color:#008800">"response"</span><span style="color:#666600">:</span> <span style="color:#666600">{</span>
        <span style="color:#008800">"Content-Type"</span><span style="color:#666600">:</span> <span style="color:#008800">"application/json;charset=UTF-8"</span><span style="color:#666600">,</span>
        <span style="color:#008800">"status"</span><span style="color:#666600">:</span> <span style="color:#008800">"200"</span>
      <span style="color:#666600">}</span>
    <span style="color:#666600">}</span>
  <span style="color:#666600">}</span>
<span style="color:#666600">},</span></code></span></span>

第二个条目是客户端对“/ resource”上的网关的请求,您可以看到cookie(由浏览器添加)和CSRF头(由Angular添加,如第二部分所述)。第一个条目有remote: true,这意味着它正在跟踪对资源服务器的调用。你可以看到它走到了一条uri路径“/”,你可以看到(至关重要的)cookie和CSRF头也被发送了。如果没有Spring Session,这些头文件对资源服务器来说没有意义,但是我们设置它的方式现在可以使用这些头部重新构成具有身份验证和CSRF令牌数据的会话。所以请求是允许的,我们在业务!

结论

我们在本节中介绍了很多,但是我们到了一个非常好的地方,在我们的两台服务器中只有极少量的样板代码,它们都非常安全且用户体验不会受到影响。仅这一点就是使用API​​网关模式的一个原因,但实际上我们只是触及了可能用于的内容(Netflix将它用于很多事情)。阅读Spring Cloud,了解有关如何轻松向网关添加更多功能的更多信息。本系列的下一部分将通过将身份验证职责提取到单独的服务器(单点登录模式)来扩展应用程序体系结构。

(五)单点登录OAuth2

在本节中,我们将继续讨论如何在“单页面应用程序”中使用带有Angular的Spring Security。在这里,我们将展示如何将Spring Security OAuth与Spring Cloud一起使用,以扩展我们的API网关,以便对后端资源进行单点登录和OAuth2令牌身份验证。这是一系列部分中的第五部分,您可以通过阅读第一部分来了解应用程序的基本构建块或从头开始构建它,或者您可以直接访问Github中的源代码。在上一节中,我们构建了一个使用Spring Session的小型分布式应用程序验证后端资源和Spring Cloud以在UI服务器中实现嵌入式API网关。在本节中,我们将身份验证职责提取到单独的服务器,以使我们的UI服务器成为授权服务器中可能有许多单点登录应用程序中的第一个。如今,无论是在企业还是在社交创业公司中,这都是许多应用程序中的常见模式。我们将使用OAuth2服务器作为身份验证器,以便我们也可以使用它来为后端资源服务器授予令牌。Spring Cloud会自动将访问令牌中继到我们的后端,并使我们能够进一步简化UI和资源服务器的实现。

提醒:如果您正在使用示例应用程序完成此部分,请务必清除Cookie和HTTP Basic凭据的浏览器缓存。在Chrome中,为单个服务器执行此操作的最佳方法是打开新的隐身窗口。

创建OAuth2授权服务器

我们的第一步是创建一个新服务器来处理身份验证和令牌管理。按照第一部分中的步骤,我们可以从Spring Boot Initializr开始。例如在类似UN * X的系统上使用curl:

<span style="color:#34302d"><span style="color:#333333"><code><span style="color:#000000">$ curl https</span><span style="color:#666600">:</span><span style="color:#880000">//start.spring.io/starter.tgz -d style=web \</span>
<span style="color:#666600">-</span><span style="color:#000000">d style</span><span style="color:#666600">=</span><span style="color:#000000">security </span><span style="color:#666600">-</span><span style="color:#000000">d name</span><span style="color:#666600">=</span><span style="color:#000000">authserver </span><span style="color:#666600">|</span><span style="color:#000000"> tar </span><span style="color:#666600">-</span><span style="color:#000000">xzvf </span><span style="color:#666600">-</span></code></span></span>

然后,您可以将该项目(默认情况下是普通的Maven Java项目)导入您喜欢的IDE,或者只使用命令行中的文件和“mvn”。

添加OAuth2依赖项

我们需要添加Spring OAuth依赖项,因此在我们的POM中我们添加:

的pom.xml

<span style="color:#34302d"><span style="color:#333333"><code class="language-xml"><span style="color:#000088"><dependency></span>
  <span style="color:#000088"><groupId></span><span style="color:#000000">org.springframework.security.oauth</span><span style="color:#000088"></groupId></span>
  <span style="color:#000088"><artifactId></span><span style="color:#000000">spring-security-oauth2</span><span style="color:#000088"></artifactId></span>
<span style="color:#000088"></dependency></span></code></span></span>

授权服务器非常容易实现。最小版本看起来像这样:

AuthserverApplication.java

<span style="color:#34302d"><span style="color:#333333"><code class="language-java"><span style="color:#006666">@SpringBootApplication</span>
<span style="color:#006666">@EnableAuthorizationServer</span>
<span style="color:#000088">public</span> <span style="color:#000088">class</span> <span style="color:#660066">AuthserverApplication</span> <span style="color:#000088">extends</span> <span style="color:#660066">WebMvcConfigurerAdapter</span> <span style="color:#666600">{</span>

  <span style="color:#000088">public</span> <span style="color:#000088">static</span> <span style="color:#000088">void</span><span style="color:#000000"> main</span><span style="color:#666600">(</span><span style="color:#660066">String</span><span style="color:#666600">[]</span><span style="color:#000000"> args</span><span style="color:#666600">)</span> <span style="color:#666600">{</span>
    <span style="color:#660066">SpringApplication</span><span style="color:#666600">.</span><span style="color:#000000">run</span><span style="color:#666600">(</span><span style="color:#660066">AuthserverApplication</span><span style="color:#666600">.</span><span style="color:#000088">class</span><span style="color:#666600">,</span><span style="color:#000000"> args</span><span style="color:#666600">);</span>
  <span style="color:#666600">}</span>

<span style="color:#666600">}</span></code></span></span>

我们只需再做一件事(添加后@EnableAuthorizationServer):

application.properties

<span style="color:#34302d"><span style="color:#333333"><code class="language-properties"><span style="color:#666600">---</span>
<span style="color:#666600">...</span><span style="color:#000000">
security</span><span style="color:#666600">.</span><span style="color:#000000">oauth2</span><span style="color:#666600">.</span><span style="color:#000000">client</span><span style="color:#666600">.</span><span style="color:#000000">clientId</span><span style="color:#666600">:</span><span style="color:#000000"> acme
security</span><span style="color:#666600">.</span><span style="color:#000000">oauth2</span><span style="color:#666600">.</span><span style="color:#000000">client</span><span style="color:#666600">.</span><span style="color:#000000">clientSecret</span><span style="color:#666600">:</span><span style="color:#000000"> acmesecret
security</span><span style="color:#666600">.</span><span style="color:#000000">oauth2</span><span style="color:#666600">.</span><span style="color:#000000">client</span><span style="color:#666600">.</span><span style="color:#000000">authorized</span><span style="color:#666600">-</span><span style="color:#000000">grant</span><span style="color:#666600">-</span><span style="color:#000000">types</span><span style="color:#666600">:</span><span style="color:#000000"> authorization_code</span><span style="color:#666600">,</span><span style="color:#000000">refresh_token</span><span style="color:#666600">,</span><span style="color:#000000">password
security</span><span style="color:#666600">.</span><span style="color:#000000">oauth2</span><span style="color:#666600">.</span><span style="color:#000000">client</span><span style="color:#666600">.</span><span style="color:#000000">scope</span><span style="color:#666600">:</span><span style="color:#000000"> openid
</span><span style="color:#666600">---</span></code></span></span>

这向客户端“acme”注册了一个秘密和一些授权的授权类型,包括“authorization_code”。

现在让我们在端口9999上运行它,并使用可预测的密码进行测试:

application.properties

<span style="color:#34302d"><span style="color:#333333"><code class="language-properties"><span style="color:#000000">server</span><span style="color:#666600">.</span><span style="color:#000000">port</span><span style="color:#666600">=</span><span style="color:#006666">9999</span><span style="color:#000000">
security</span><span style="color:#666600">.</span><span style="color:#000000">user</span><span style="color:#666600">.</span><span style="color:#000000">password</span><span style="color:#666600">=</span><span style="color:#000000">password
server</span><span style="color:#666600">.</span><span style="color:#000000">contextPath</span><span style="color:#666600">=/</span><span style="color:#000000">uaa
</span><span style="color:#666600">...</span></code></span></span>

我们还设置了上下文路径,使其不使用默认值(“/”),因为否则您可以将localhost上的其他服务器的cookie发送到错误的服务器。因此,让服务器运行,我们可以确保它正常工作:

<span style="color:#34302d"><span style="color:#333333"><code><span style="color:#000000">$ mvn spring</span><span style="color:#666600">-</span><span style="color:#000000">boot</span><span style="color:#666600">:</span><span style="color:#000000">run</span></code></span></span>

或者main()在IDE中启动该方法。

测试授权服务器

我们的服务器使用Spring Boot默认安全设置,因此与第一部分中的服务器一样,它将受到HTTP基本身份验证的保护。要启动授权代码令牌,您可以访问授权端点,例如http:// localhost:9999 / uaa / oauth / authorize?response_type = code&client_id = acme&redirect_uri = http://example一旦您通过身份验证,您将获得重定向到附带授权码的example,例如http://example/?code=jYWioI。

 出于此示例应用程序的目的,我们创建了一个没有注册重定向的客户端“acme”,这使我们能够重定向example。在生产应用程序中,您应始终注册重定向(并使用HTTPS)。

可以使用令牌端点上的“acme”客户端凭据将代码交换为访问令牌:

<span style="color:#34302d"><span style="color:#333333"><code><span style="color:#000000">$ curl acme</span><span style="color:#666600">:</span><span style="color:#000000">acmesecret@localhost</span><span style="color:#666600">:</span><span style="color:#006666">9999</span><span style="color:#666600">/</span><span style="color:#000000">uaa</span><span style="color:#666600">/</span><span style="color:#000000">oauth</span><span style="color:#666600">/</span><span style="color:#000000">token  \
</span><span style="color:#666600">-</span><span style="color:#000000">d grant_type</span><span style="color:#666600">=</span><span style="color:#000000">authorization_code </span><span style="color:#666600">-</span><span style="color:#000000">d client_id</span><span style="color:#666600">=</span><span style="color:#000000">acme     \
</span><span style="color:#666600">-</span><span style="color:#000000">d redirect_uri</span><span style="color:#666600">=</span><span style="color:#000000">http</span><span style="color:#666600">:</span><span style="color:#880000">//example -d code=jYWioI</span>
<span style="color:#666600">{</span><span style="color:#008800">"access_token"</span><span style="color:#666600">:</span><span style="color:#008800">"2219199c-966e-4466-8b7e-12bb9038c9bb"</span><span style="color:#666600">,</span><span style="color:#008800">"token_type"</span><span style="color:#666600">:</span><span style="color:#008800">"bearer"</span><span style="color:#666600">,</span><span style="color:#008800">"refresh_token"</span><span style="color:#666600">:</span><span style="color:#008800">"d193caf4-5643-4988-9a4a-1c03c9d657aa"</span><span style="color:#666600">,</span><span style="color:#008800">"expires_in"</span><span style="color:#666600">:</span><span style="color:#006666">43199</span><span style="color:#666600">,</span><span style="color:#008800">"scope"</span><span style="color:#666600">:</span><span style="color:#008800">"openid"</span><span style="color:#666600">}</span></code></span></span>

访问令牌是UUID(“2219199c ...”),由服务器中的内存中令牌存储支持。我们还获得了一个刷新令牌,当前一个令牌到期时,我们可以使用它来获取新的访问令牌。

 因为我们允许“acme”客户端的“密码”授权,我们也可以使用curl和用户凭证而不是授权代码直接从令牌端点获取令牌。这不适合基于浏览器的客户端,但它对测试很有用。

如果您按照上面的链接,您将看到Spring OAuth提供的whitelabel UI。首先,我们将使用它,我们可以稍后回来加强它,就像我们在第二部分中为自包含服务器所做的那样。

更改资源服务器

如果我们从第IV部分继续,我们的资源服务器正在使用Spring Session进行身份验证,因此我们可以将其取出并将其替换为Spring OAuth。我们还需要删除Spring Session和Redis依赖项,所以替换它:

的pom.xml

<span style="color:#34302d"><span style="color:#333333"><code class="language-xml"><span style="color:#000088"><dependency></span>
  <span style="color:#000088"><groupId></span><span style="color:#000000">org.springframework.session</span><span style="color:#000088"></groupId></span>
  <span style="color:#000088"><artifactId></span><span style="color:#000000">spring-session</span><span style="color:#000088"></artifactId></span>
<span style="color:#000088"></dependency></span>
<span style="color:#000088"><dependency></span>
  <span style="color:#000088"><groupId></span><span style="color:#000000">org.springframework.boot</span><span style="color:#000088"></groupId></span>
  <span style="color:#000088"><artifactId></span><span style="color:#000000">spring-boot-starter-redis</span><span style="color:#000088"></artifactId></span>
<span style="color:#000088"></dependency></span></code></span></span>

有了这个:

的pom.xml

<span style="color:#34302d"><span style="color:#333333"><code class="language-xml"><span style="color:#000088"><dependency></span>
  <span style="color:#000088"><groupId></span><span style="color:#000000">org.springframework.security.oauth</span><span style="color:#000088"></groupId></span>
  <span style="color:#000088"><artifactId></span><span style="color:#000000">spring-security-oauth2</span><span style="color:#000088"></artifactId></span>
<span style="color:#000088"></dependency></span></code></span></span>

然后Filter从主应用程序类中删除会话,将其替换为方便的@EnableResourceServer注释(来自Spring Security OAuth2):

ResourceApplication.java

<span style="color:#34302d"><span style="color:#333333"><code class="language-java"><span style="color:#006666">@SpringBootApplication</span>
<span style="color:#006666">@RestController</span>
<span style="color:#006666">@EnableResourceServer</span>
<span style="color:#000088">class</span> <span style="color:#660066">ResourceApplication</span> <span style="color:#666600">{</span>

  <span style="color:#006666">@RequestMapping</span><span style="color:#666600">(</span><span style="color:#008800">"/"</span><span style="color:#666600">)</span>
  <span style="color:#000088">public</span> <span style="color:#660066">Message</span><span style="color:#000000"> home</span><span style="color:#666600">()</span> <span style="color:#666600">{</span>
    <span style="color:#000088">return</span> <span style="color:#000088">new</span> <span style="color:#660066">Message</span><span style="color:#666600">(</span><span style="color:#008800">"Hello World"</span><span style="color:#666600">);</span>
  <span style="color:#666600">}</span>

  <span style="color:#000088">public</span> <span style="color:#000088">static</span> <span style="color:#000088">void</span><span style="color:#000000"> main</span><span style="color:#666600">(</span><span style="color:#660066">String</span><span style="color:#666600">[]</span><span style="color:#000000"> args</span><span style="color:#666600">)</span> <span style="color:#666600">{</span>
    <span style="color:#660066">SpringApplication</span><span style="color:#666600">.</span><span style="color:#000000">run</span><span style="color:#666600">(</span><span style="color:#660066">ResourceApplication</span><span style="color:#666600">.</span><span style="color:#000088">class</span><span style="color:#666600">,</span><span style="color:#000000"> args</span><span style="color:#666600">);</span>
  <span style="color:#666600">}</span>
<span style="color:#666600">}</span></code></span></span>

通过这一次更改,应用程序已准备好挑战访问令牌而不是HTTP Basic,但我们需要更改配置才能实际完成该过程。我们将添加少量外部配置(在“application.properties”中)以允许资源服务器解码它给出的令牌并验证用户:

application.properties

<span style="color:#34302d"><span style="color:#333333"><code class="language-properties"><span style="color:#666600">...</span><span style="color:#000000">
security</span><span style="color:#666600">.</span><span style="color:#000000">oauth2</span><span style="color:#666600">.</span><span style="color:#000000">resource</span><span style="color:#666600">.</span><span style="color:#000000">userInfoUri</span><span style="color:#666600">:</span><span style="color:#000000"> http</span><span style="color:#666600">:</span><span style="color:#880000">//localhost:9999/uaa/user</span></code></span></span>

这告诉服务器它可以使用令牌访问“/ user”端点并使用它来获取身份验证信息(它有点像Facebook API中的“/ me”端点)。实际上,它为资源服务器提供了解码令牌的方法,如ResourceServerTokenServicesSpring OAuth2中的接口所表示的那样。

运行应用程序并使用命令行客户端点击主页:

<span style="color:#34302d"><span style="color:#333333"><code><span style="color:#000000">$ curl </span><span style="color:#666600">-</span><span style="color:#000000">v localhost</span><span style="color:#666600">:</span><span style="color:#006666">9000</span>
<span style="color:#666600">></span><span style="color:#000000"> GET </span><span style="color:#666600">/</span><span style="color:#000000"> HTTP</span><span style="color:#666600">/</span><span style="color:#006666">1.1</span>
<span style="color:#666600">></span> <span style="color:#660066">User</span><span style="color:#666600">-</span><span style="color:#660066">Agent</span><span style="color:#666600">:</span><span style="color:#000000"> curl</span><span style="color:#666600">/</span><span style="color:#006666">7.35</span><span style="color:#666600">.</span><span style="color:#006666">0</span>
<span style="color:#666600">></span> <span style="color:#660066">Host</span><span style="color:#666600">:</span><span style="color:#000000"> localhost</span><span style="color:#666600">:</span><span style="color:#006666">9000</span>
<span style="color:#666600">></span> <span style="color:#660066">Accept</span><span style="color:#666600">:</span> <span style="color:#666600">*</span><span style="color:#880000">/*
>
< HTTP/1.1 401 Unauthorized
...
< WWW-Authenticate: Bearer realm="null", error="unauthorized", error_description="An Authentication object was not found in the SecurityContext"
< Content-Type: application/json;charset=UTF-8
{"error":"unauthorized","error_description":"An Authentication object was not found in the SecurityContext"}</span></code></span></span>

并且您将看到401带有“WWW-Authenticate”标头,表明它需要持有者令牌。

 userInfoUri是迄今为止不挂钩的资源服务器想出了一个办法,以凭证进行解码的唯一途径。事实上,它是最低的共同标准(而不是规范的一部分),但经常可以从OAuth2提供商(如Facebook,Cloud Foundry,Github)获得,还有其他选择。例如,您可以在令牌本身中编码用户身份验证(例如,使用JWT),或使用共享后端存储。/token_infoCloudFoundry中还有一个端点,它提供比用户信息端点更详细的信息,但需要更全面的身份验证。不同的选择(自然地)提供不同的利益和权衡,但对这些的完整讨论超出了本节的范围。

实现用户端点

在授权服务器上,我们可以轻松添加该端点

AuthserverApplication.java

<span style="color:#34302d"><span style="color:#333333"><code class="language-java"><span style="color:#006666">@SpringBootApplication</span>
<span style="color:#006666">@RestController</span>
<span style="color:#006666">@EnableAuthorizationServer</span>
<span style="color:#006666">@EnableResourceServer</span>
<span style="color:#000088">public</span> <span style="color:#000088">class</span> <span style="color:#660066">AuthserverApplication</span> <span style="color:#666600">{</span>

  <span style="color:#006666">@RequestMapping</span><span style="color:#666600">(</span><span style="color:#008800">"/user"</span><span style="color:#666600">)</span>
  <span style="color:#000088">public</span> <span style="color:#660066">Principal</span><span style="color:#000000"> user</span><span style="color:#666600">(</span><span style="color:#660066">Principal</span><span style="color:#000000"> user</span><span style="color:#666600">)</span> <span style="color:#666600">{</span>
    <span style="color:#000088">return</span><span style="color:#000000"> user</span><span style="color:#666600">;</span>
  <span style="color:#666600">}</span>

  <span style="color:#666600">...</span>

<span style="color:#666600">}</span></code></span></span>

我们添加了@RequestMapping与第II部分中的UI服务器相同的内容,以及@EnableResourceServerSpring OAuth中的注释,默认情况下,它保护授权服务器中除“/ oauth / *”端点之外的所有内容。

使用该端点,我们可以测试它和问候资源,因为它们现在都接受由授权服务器创建的承载令牌:

<span style="color:#34302d"><span style="color:#333333"><code><span style="color:#000000">$ TOKEN</span><span style="color:#666600">=</span><span style="color:#006666">2219199c</span><span style="color:#666600">-</span><span style="color:#006666">966e-4466</span><span style="color:#666600">-</span><span style="color:#006666">8b7e-12bb9038c9bb</span><span style="color:#000000">
$ curl </span><span style="color:#666600">-</span><span style="color:#000000">H </span><span style="color:#008800">"Authorization: Bearer $TOKEN"</span><span style="color:#000000"> localhost</span><span style="color:#666600">:</span><span style="color:#006666">9000</span>
<span style="color:#666600">{</span><span style="color:#008800">"id"</span><span style="color:#666600">:</span><span style="color:#008800">"03af8be3-2fc3-4d75-acf7-c484d9cf32b1"</span><span style="color:#666600">,</span><span style="color:#008800">"content"</span><span style="color:#666600">:</span><span style="color:#008800">"Hello World"</span><span style="color:#666600">}</span><span style="color:#000000">
$ curl </span><span style="color:#666600">-</span><span style="color:#000000">H </span><span style="color:#008800">"Authorization: Bearer $TOKEN"</span><span style="color:#000000"> localhost</span><span style="color:#666600">:</span><span style="color:#006666">9999</span><span style="color:#666600">/</span><span style="color:#000000">uaa</span><span style="color:#666600">/</span><span style="color:#000000">user
</span><span style="color:#666600">{</span><span style="color:#008800">"details"</span><span style="color:#666600">:...,</span><span style="color:#008800">"principal"</span><span style="color:#666600">:{</span><span style="color:#008800">"username"</span><span style="color:#666600">:</span><span style="color:#008800">"user"</span><span style="color:#666600">,...},</span><span style="color:#008800">"name"</span><span style="color:#666600">:</span><span style="color:#008800">"user"</span><span style="color:#666600">}</span></code></span></span>

(替换从您自己的授权服务器获取的访问令牌的值,以使其自行运行)。

UI服务器

我们需要完成的这个应用程序的最后一部分是UI服务器,提取身份验证部分并委派给授权服务器。因此,与资源服务器一样,我们首先需要删除Spring Session和Redis依赖项,并将它们替换为Spring OAuth2。因为我们在UI层使用Zuul,所以我们实际使用spring-cloud-starter-oauth2而不是spring-security-oauth2直接(这设置了一些自动配置,用于通过代理中继令牌)。

完成后,我们也可以删除会话过滤器和“/ user”端点,并设置应用程序以重定向到授权服务器(使用@EnableOAuth2Sso注释):

UiApplication.java

<span style="color:#34302d"><span style="color:#333333"><code class="language-java"><span style="color:#006666">@SpringBootApplication</span>
<span style="color:#006666">@EnableZuulProxy</span>
<span style="color:#006666">@EnableOAuth2Sso</span>
<span style="color:#000088">public</span> <span style="color:#000088">class</span> <span style="color:#660066">UiApplication</span> <span style="color:#666600">{</span>

  <span style="color:#000088">public</span> <span style="color:#000088">static</span> <span style="color:#000088">void</span><span style="color:#000000"> main</span><span style="color:#666600">(</span><span style="color:#660066">String</span><span style="color:#666600">[]</span><span style="color:#000000"> args</span><span style="color:#666600">)</span> <span style="color:#666600">{</span>
    <span style="color:#660066">SpringApplication</span><span style="color:#666600">.</span><span style="color:#000000">run</span><span style="color:#666600">(</span><span style="color:#660066">UiApplication</span><span style="color:#666600">.</span><span style="color:#000088">class</span><span style="color:#666600">,</span><span style="color:#000000"> args</span><span style="color:#666600">);</span>
  <span style="color:#666600">}</span>

<span style="color:#666600">...</span>

<span style="color:#666600">}</span></code></span></span>

回想一下第四部分,UI服务器凭借其@EnableZuulProxy作用于API网关,我们可以在YAML中声明路由映射。因此,“/ user”端点可以代理到授权服务器:

application.yml

<span style="color:#34302d"><span style="color:#333333"><code class="language-yaml"><span style="color:#000000">zuul</span><span style="color:#666600">:</span><span style="color:#000000">
  routes</span><span style="color:#666600">:</span><span style="color:#000000">
    resource</span><span style="color:#666600">:</span><span style="color:#000000">
      path</span><span style="color:#666600">:</span> <span style="color:#008800">/resource/</span><span style="color:#666600">**</span><span style="color:#000000">
      url</span><span style="color:#666600">:</span><span style="color:#000000"> http</span><span style="color:#666600">:</span><span style="color:#880000">//localhost:9000</span><span style="color:#000000">
    user</span><span style="color:#666600">:</span><span style="color:#000000">
      path</span><span style="color:#666600">:</span> <span style="color:#008800">/user/</span><span style="color:#666600">**</span><span style="color:#000000">
      url</span><span style="color:#666600">:</span><span style="color:#000000"> http</span><span style="color:#666600">:</span><span style="color:#880000">//localhost:9999/uaa/user</span></code></span></span>

最后,我们需要将应用程序更改为a,WebSecurityConfigurerAdapter因为它现在将用于修改SSO过滤器链中的默认设置@EnableOAuth2Sso

SecurityConfiguration.java

<span style="color:#34302d"><span style="color:#333333"><code class="language-java"><span style="color:#006666">@SpringBootApplication</span>
<span style="color:#006666">@EnableZuulProxy</span>
<span style="color:#006666">@EnableOAuth2Sso</span>
<span style="color:#000088">public</span> <span style="color:#000088">class</span> <span style="color:#660066">UiApplication</span> <span style="color:#000088">extends</span> <span style="color:#660066">WebSecurityConfigurerAdapter</span> <span style="color:#666600">{</span>
    <span style="color:#006666">@Override</span>
    <span style="color:#000088">public</span> <span style="color:#000088">void</span><span style="color:#000000"> configure</span><span style="color:#666600">(</span><span style="color:#660066">HttpSecurity</span><span style="color:#000000"> http</span><span style="color:#666600">)</span> <span style="color:#000088">throws</span> <span style="color:#660066">Exception</span> <span style="color:#666600">{</span><span style="color:#000000">
      http
          </span><span style="color:#666600">.</span><span style="color:#000000">logout</span><span style="color:#666600">().</span><span style="color:#000000">logoutSuccessUrl</span><span style="color:#666600">(</span><span style="color:#008800">"/"</span><span style="color:#666600">).</span><span style="color:#000088">and</span><span style="color:#666600">()</span>
          <span style="color:#666600">.</span><span style="color:#000000">authorizeRequests</span><span style="color:#666600">().</span><span style="color:#000000">antMatchers</span><span style="color:#666600">(</span><span style="color:#008800">"/index.html"</span><span style="color:#666600">,</span> <span style="color:#008800">"/app.html"</span><span style="color:#666600">,</span> <span style="color:#008800">"/"</span><span style="color:#666600">)</span>
          <span style="color:#666600">.</span><span style="color:#000000">permitAll</span><span style="color:#666600">().</span><span style="color:#000000">anyRequest</span><span style="color:#666600">().</span><span style="color:#000000">authenticated</span><span style="color:#666600">().</span><span style="color:#000088">and</span><span style="color:#666600">()</span>
          <span style="color:#666600">.</span><span style="color:#000000">csrf</span><span style="color:#666600">()</span>
            <span style="color:#666600">.</span><span style="color:#000000">csrfTokenRepository</span><span style="color:#666600">(</span><span style="color:#660066">CookieCsrfTokenRepository</span><span style="color:#666600">.</span><span style="color:#000000">withHttpOnlyFalse</span><span style="color:#666600">());</span>
    <span style="color:#666600">}</span>

<span style="color:#666600">}</span></code></span></span>

主要变化(除了基类名称)是匹配器进入他们自己的方法,并且不再需要formLogin()。显式logout()配置显式添加了一个不受保护的成功URL,以便XHR请求/logout将成功返回。

@EnableOAuth2Sso注释还有一些必需的外部配置属性,可以与正确的授权服务器联系和进行身份验证。所以我们需要这个application.yml

application.yml

<span style="color:#34302d"><span style="color:#333333"><code class="language-yaml"><span style="color:#000000">security</span><span style="color:#666600">:</span>
  <span style="color:#666600">...</span><span style="color:#000000">
  oauth2</span><span style="color:#666600">:</span><span style="color:#000000">
    client</span><span style="color:#666600">:</span><span style="color:#000000">
      accessTokenUri</span><span style="color:#666600">:</span><span style="color:#000000"> http</span><span style="color:#666600">:</span><span style="color:#880000">//localhost:9999/uaa/oauth/token</span><span style="color:#000000">
      userAuthorizationUri</span><span style="color:#666600">:</span><span style="color:#000000"> http</span><span style="color:#666600">:</span><span style="color:#880000">//localhost:9999/uaa/oauth/authorize</span><span style="color:#000000">
      clientId</span><span style="color:#666600">:</span><span style="color:#000000"> acme
      clientSecret</span><span style="color:#666600">:</span><span style="color:#000000"> acmesecret
    resource</span><span style="color:#666600">:</span><span style="color:#000000">
      userInfoUri</span><span style="color:#666600">:</span><span style="color:#000000"> http</span><span style="color:#666600">:</span><span style="color:#880000">//localhost:9999/uaa/user</span></code></span></span>

其中大部分是关于OAuth2客户端(“acme”)和授权服务器位置。还有一个userInfoUri(就像在资源服务器中一样),以便用户可以在UI应用程序本身中进行身份验证。

 如果您希望UI应用程序能够自动刷新过期的访问令牌,则必须OAuth2RestOperations注入执行中继的Zuul过滤器。您可以通过创建该类型的bean来执行此操作(请OAuth2TokenRelayFilter查看详细信息):
<span style="color:#34302d"><span style="color:#333333"><code class="language-java"><span style="color:#006666">@Bean</span>
<span style="color:#000088">protected</span> <span style="color:#660066">OAuth2RestTemplate</span> <span style="color:#660066">OAuth2RestTemplate</span><span style="color:#666600">(</span>
    <span style="color:#660066">OAuth2ProtectedResourceDetails</span><span style="color:#000000"> resource</span><span style="color:#666600">,</span> <span style="color:#660066">OAuth2ClientContext</span><span style="color:#000000"> context</span><span style="color:#666600">)</span> <span style="color:#666600">{</span>
  <span style="color:#000088">return</span> <span style="color:#000088">new</span> <span style="color:#660066">OAuth2RestTemplate</span><span style="color:#666600">(</span><span style="color:#000000">resource</span><span style="color:#666600">,</span><span style="color:#000000"> context</span><span style="color:#666600">);</span>
<span style="color:#666600">}</span></code></span></span>

在客户端

前端的UI应用程序有一些调整,我们仍需要触发重定向到授权服务器。在这个简单的演示中,我们可以将Angular应用程序拆分为其基本要素,以便您可以更清楚地了解正在发生的事情。所以我们现在放弃使用表单或路由,然后我们回到单个Angular组件:

appponent.ts

<span style="color:#34302d"><span style="color:#333333"><code class="language-javascript"><span style="color:#000088">import</span> <span style="color:#666600">{</span> <span style="color:#660066">Component</span> <span style="color:#666600">}</span> <span style="color:#000088">from</span> <span style="color:#008800">'@angular/core'</span><span style="color:#666600">;</span>
<span style="color:#000088">import</span> <span style="color:#666600">{</span> <span style="color:#660066">HttpClient</span> <span style="color:#666600">}</span> <span style="color:#000088">from</span> <span style="color:#008800">'@angular/common/http'</span><span style="color:#666600">;</span>
<span style="color:#000088">import</span> <span style="color:#008800">'rxjs/add/operator/finally'</span><span style="color:#666600">;</span>

<span style="color:#006666">@Component</span><span style="color:#666600">({</span><span style="color:#000000">
  selector</span><span style="color:#666600">:</span> <span style="color:#008800">'app-root'</span><span style="color:#666600">,</span><span style="color:#000000">
  templateUrl</span><span style="color:#666600">:</span> <span style="color:#008800">'./appponent.html'</span><span style="color:#666600">,</span><span style="color:#000000">
  styleUrls</span><span style="color:#666600">:</span> <span style="color:#666600">[</span><span style="color:#008800">'./appponent.css'</span><span style="color:#666600">]</span>
<span style="color:#666600">})</span>
<span style="color:#000088">export</span> <span style="color:#000088">class</span> <span style="color:#660066">AppComponent</span> <span style="color:#666600">{</span><span style="color:#000000">

  title </span><span style="color:#666600">=</span> <span style="color:#008800">'Demo'</span><span style="color:#666600">;</span><span style="color:#000000">
  authenticated </span><span style="color:#666600">=</span> <span style="color:#000088">false</span><span style="color:#666600">;</span><span style="color:#000000">
  greeting </span><span style="color:#666600">=</span> <span style="color:#666600">{};</span><span style="color:#000000">

  constructor</span><span style="color:#666600">(</span><span style="color:#000088">private</span><span style="color:#000000"> http</span><span style="color:#666600">:</span> <span style="color:#660066">HttpClient</span><span style="color:#666600">)</span> <span style="color:#666600">{</span>
    <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">authenticate</span><span style="color:#666600">();</span>
  <span style="color:#666600">}</span><span style="color:#000000">

  authenticate</span><span style="color:#666600">()</span> <span style="color:#666600">{</span>

    <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">http</span><span style="color:#666600">.</span><span style="color:#000088">get</span><span style="color:#666600">(</span><span style="color:#008800">'user'</span><span style="color:#666600">).</span><span style="color:#000000">subscribe</span><span style="color:#666600">(</span><span style="color:#000000">response </span><span style="color:#666600">=></span> <span style="color:#666600">{</span>
        <span style="color:#000088">if</span> <span style="color:#666600">(</span><span style="color:#000000">response</span><span style="color:#666600">[</span><span style="color:#008800">'name'</span><span style="color:#666600">])</span> <span style="color:#666600">{</span>
            <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">authenticated </span><span style="color:#666600">=</span> <span style="color:#000088">true</span><span style="color:#666600">;</span>
            <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">http</span><span style="color:#666600">.</span><span style="color:#000088">get</span><span style="color:#666600">(</span><span style="color:#008800">'resource'</span><span style="color:#666600">).</span><span style="color:#000000">subscribe</span><span style="color:#666600">(</span><span style="color:#000000">data </span><span style="color:#666600">=></span> <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">greeting </span><span style="color:#666600">=</span><span style="color:#000000"> data</span><span style="color:#666600">);</span>
        <span style="color:#666600">}</span> <span style="color:#000088">else</span> <span style="color:#666600">{</span>
            <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">authenticated </span><span style="color:#666600">=</span> <span style="color:#000088">false</span><span style="color:#666600">;</span>
        <span style="color:#666600">}</span>
    <span style="color:#666600">},</span> <span style="color:#666600">()</span> <span style="color:#666600">=></span> <span style="color:#666600">{</span> <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">authenticated </span><span style="color:#666600">=</span> <span style="color:#000088">false</span><span style="color:#666600">;</span> <span style="color:#666600">});</span>

  <span style="color:#666600">}</span><span style="color:#000000">
  logout</span><span style="color:#666600">()</span> <span style="color:#666600">{</span>
      <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">http</span><span style="color:#666600">.</span><span style="color:#000000">post</span><span style="color:#666600">(</span><span style="color:#008800">'logout'</span><span style="color:#666600">,</span> <span style="color:#666600">{}).</span><span style="color:#000088">finally</span><span style="color:#666600">(()</span> <span style="color:#666600">=></span> <span style="color:#666600">{</span>
          <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">authenticated </span><span style="color:#666600">=</span> <span style="color:#000088">false</span><span style="color:#666600">;</span>
      <span style="color:#666600">}).</span><span style="color:#000000">subscribe</span><span style="color:#666600">();</span>
  <span style="color:#666600">}</span>

<span style="color:#666600">}</span></code></span></span>

AppComponent处理一切,获取用户的详细信息,如果成功,问候。它还提供logout功能。

现在我们需要为这个新组件创建模板:

appponent.html

<span style="color:#34302d"><span style="color:#333333"><code class="language-html"><span style="color:#000088"><div</span> <span style="color:#660066">class</span><span style="color:#666600">=</span><span style="color:#008800">"container"</span><span style="color:#000088">></span>
  <span style="color:#000088"><ul</span> <span style="color:#660066">class</span><span style="color:#666600">=</span><span style="color:#008800">"nav nav-pills"</span><span style="color:#000088">></span>
    <span style="color:#000088"><li><a></span><span style="color:#000000">Home</span><span style="color:#000088"></a></li></span>
    <span style="color:#000088"><li><a</span> <span style="color:#660066">href</span><span style="color:#666600">=</span><span style="color:#008800">"login"</span><span style="color:#000088">></span><span style="color:#000000">Login</span><span style="color:#000088"></a></li></span>
    <span style="color:#000088"><li><a</span><span style="color:#000000"> (</span><span style="color:#660066">click</span><span style="color:#000000">)</span><span style="color:#666600">=</span><span style="color:#008800">"logout()"</span><span style="color:#000088">></span><span style="color:#000000">Logout</span><span style="color:#000088"></a></li></span>
  <span style="color:#000088"></ul></span>
<span style="color:#000088"></div></span>
<span style="color:#000088"><div</span> <span style="color:#660066">class</span><span style="color:#666600">=</span><span style="color:#008800">"container"</span><span style="color:#000088">></span>
<span style="color:#000088"><h1></span><span style="color:#000000">Greeting</span><span style="color:#000088"></h1></span>
<span style="color:#000088"><div</span><span style="color:#000000"> [</span><span style="color:#660066">hidden</span><span style="color:#000000">]</span><span style="color:#666600">=</span><span style="color:#008800">"!authenticated"</span><span style="color:#000088">></span>
	<span style="color:#000088"><p></span><span style="color:#000000">The ID is {{greeting.id}}</span><span style="color:#000088"></p></span>
	<span style="color:#000088"><p></span><span style="color:#000000">The content is {{greeting.content}}</span><span style="color:#000088"></p></span>
<span style="color:#000088"></div></span>
<span style="color:#000088"><div</span><span style="color:#000000"> [</span><span style="color:#660066">hidden</span><span style="color:#000000">]</span><span style="color:#666600">=</span><span style="color:#008800">"authenticated"</span><span style="color:#000088">></span>
	<span style="color:#000088"><p></span><span style="color:#000000">Login to see your greeting</span><span style="color:#000088"></p></span>
<span style="color:#000088"></div></span></code></span></span>

并将其包含在主页中<app-root/>

请注意,“登录”的导航链接是具有href(不是Angular路线)的常规链接。此过程所使用的“/ login”端点由Spring Security处理,如果用户未经过身份验证,则会导致重定向到授权服务器。

它是如何工作的?

立即运行所有服务器,并在浏览器http:// localhost:8080中访问UI 。单击“登录”链接,您将被重定向到授权服务器进行身份验证(HTTP Basic弹出窗口)并批准令牌授权(whitelabel HTML),然后重定向到UI中的主页,并从OAuth2获取问候语资源服务器使用与我们对UI进行身份验证的相同令牌。

如果您使用某些开发人员工具,则可以在浏览器中看到浏览器和后端之间的交互(通常F12打开它,默认情况下在Chrome中运行,可能需要Firefox中的插件)。这是一个总结:

动词路径状态响应

得到

/

200

的index.html

得到

/*.js

200

角度资产

得到

/用户

302

重定向到登录页面

得到

/登录

302

重定向到auth服务器

得到

(uaa)/ outh / authorize

401

(忽略)

得到

/登录

302

重定向到auth服务器

得到

(uaa)/ outh / authorize

200

HTTP Basic身份验证在这里发生

POST

(uaa)/ outh / authorize

302

用户批准授权,重定向到/ login

得到

/登录

302

重定向到主页

得到

/用户

200

(Proxied)JSON认证用户

得到

/app.html

200

主页的HTML部分

得到

/资源

200

(Proxied)JSON问候语

带有(uaa)前缀的请求是授权服务器。标记为“忽略”的响应是Angular在XHR调用中收到的响应,由于我们不处理这些数据,因此它们被丢弃在地板上。我们确实在“/ user”资源的情况下寻找经过身份验证的用户,但由于它在第一次调用中不存在,因此该响应被删除。

在UI(向下滚动至底部)的“/跟踪”端点,您将看到代理后台请求“/ user”和“/资源”,以remote:true和承载令牌,而不是饼干(因为它会一直在第IV部分中)用于认证。Spring Cloud Security已经为我们解决了这个问题:通过认识到我们已经@EnableOAuth2Sso并且@EnableZuulProxy已经发现(默认情况下)我们想要将令牌转发到代理后端。

 与前面的部分一样,尝试使用不同的浏览器进行“/ trace”,这样就无法进行身份验证交叉(例如,如果您使用Chrome测试用户界面,请使用Firefox)。

退出体验

如果单击“注销”链接,您将看到主页更改(不再显示问候语),因此用户不再使用UI服务器进行身份验证。单击后退的“登录”,虽然和你其实并不需要通过在授权服务器的认证和审批周期要回去(因为你还没有注销的那个)。关于这是否是一个理想的用户体验的意见将分歧,这是一个众所周知的棘手问题(单点注销:Science Direct文章和Shibboleth文档)。理想的用户体验可能在技术上不可行,并且您有时也必须怀疑用户是否真的想要他们想要的内容。“我希望'退出'让我退出”听起来很简单,但明显的反应是,“退出了什么?你想要退出这个SSO服务器控制的所有系统,或者只是那个你点击了“注销”链接?“ 如果您有兴趣,那么本教程的后续部分将对其进行更深入的讨论。

结论

这几乎是我们通过Spring Security和Angular堆栈进行浅层浏览的结束。我们现在有一个很好的架构,在三个独立的组件中有明确的职责,UI / API网关,资源服务器和授权服务器/令牌机。现在,所有层中的非业务代码数量都很少,并且可以通过更多业务逻辑轻松查看扩展位置并改进实现。接下来的步骤是整理授权服务器中的UI,并可能添加一些测试,包括JavaScript客户端上的测试。另一个有趣的任务是提取所有锅炉板代码并将其放入包含Spring Security和Spring Session自动配置的库(例如“spring-security-angular”)和Angular中导航控制器的一些webjars资源。Spring Cloud是新的,这些样本在编写时需要快照,但是有可用的候选版本和即将发布的GA版本,所以请检查它并通过Github或gitter.im发送一些反馈。

本系列的下一部分是关于访问决策(超出身份验证),并在同一代理后面使用多个UI应用程序。

附录:授权服务器的Bootstrap UI和JWT令牌

您将在Github的源代码中找到该应用程序的另一个版本,其中包含一个漂亮的登录页面和用户批准页面,其实现方式类似于我们在第二部分中执行登录页面的方式。它还使用JWT对令牌进行编码,因此资源服务器可以从令牌本身中提取足够的信息来进行简单的身份验证,而不是使用“/ user”端点。浏览器客户端仍然使用它,通过UI服务器代理,以便它可以确定用户是否经过身份验证(与实际应用程序中对资源服务器的可能调用次数相比,它不需要经常执行此操作)。

(六)多个UI应用程序和网关

在本节中,我们将继续讨论如何在“单页面应用程序”中使用带有Angular的Spring Security。在这里,我们将展示如何将Spring Session与Spring Cloud结合使用,以结合我们在第II部分和第IV部分中构建的系统的功能,并最终构建3个具有完全不同职责的单页应用程序。目标是构建一个网关(如第四部分),它不仅用于API资源,还用于从后端服务器加载UI。我们简化了第二部分的令牌争用位通过使用Gateway将身份验证传递给后端。然后,我们扩展系统,以显示我们如何在后端进行本地,细粒度的访问决策,同时仍然控制网关的身份和身份验证。这是一个非常强大的模型,用于构建分布式系统,并且在我们介绍我们构建的代码中的功能时,我们可以探索许多好处。

提醒:如果您正在使用示例应用程序完成此部分,请务必清除Cookie和HTTP Basic凭据的浏览器缓存。在Chrome中,最好的方法是打开一个新的隐身窗口。

目标架构

这是我们要开始构建的基本系统的图片:

与本系列中的其他示例应用程序一样,它具有UI(HTML和JavaScript)和资源服务器。与第IV节中的示例一样,它有一个网关,但这里它是独立的,不是UI的一部分。UI有效地成为后端的一部分,为我们提供了更多选择来重新配置和重新实现功能,并且还带来了我们将看到的其他好处。

浏览器进入网关的所有内容,它不必知道后端的架构(从根本上说,它不知道有后端)。浏览器在此网关中执行的操作之一是身份验证,例如,它会发送第II部分中的用户名和密码,并获得cookie作为回报。在后续请求中,它会自动呈现cookie,并且Gateway会将其传递给后端。不需要在客户端上编写代码来启用cookie传递。后端使用cookie进行身份验证,并且由于所有组件共享会话,因此它们共享有关用户的相同信息。与第五节对比 其中cookie必须转换为网关中的访问令牌,然后访问令牌必须由所有后端组件独立解码。

与第IV部分一样,Gateway简化了客户端和服务器之间的交互,并提供了一个小的,定义明确的表面来处理安全性。例如,我们不需要担心跨源资源共享,这是一个受欢迎的缓解,因为它很容易出错。

我们要构建的完整项目的源代码在Github中,所以你可以克隆项目并直接从那里开始工作。在这个系统的最终状态中有一个额外的组件(“双管理”),所以现在忽略它。

构建后端

在这个架构中,后端非常类似于我们在第III节中构建的“spring-session”示例,除了它实际上不需要登录页面。在这里找到我们想要的最简单方法可能是复制第III部分中的“资源”服务器,并从第I部分的“基本”示例中获取UI 。要从“基本”UI到我们想要的UI,我们只需添加几个依赖项(比如我们在第III部分中第一次使用Spring Session时):

的pom.xml

<span style="color:#34302d"><span style="color:#333333"><code class="language-xml"><span style="color:#000088"><dependency></span>
  <span style="color:#000088"><groupId></span><span style="color:#000000">org.springframework.session</span><span style="color:#000088"></groupId></span>
  <span style="color:#000088"><artifactId></span><span style="color:#000000">spring-session</span><span style="color:#000088"></artifactId></span>
<span style="color:#000088"></dependency></span>
<span style="color:#000088"><dependency></span>
  <span style="color:#000088"><groupId></span><span style="color:#000000">org.springframework.boot</span><span style="color:#000088"></groupId></span>
  <span style="color:#000088"><artifactId></span><span style="color:#000000">spring-boot-starter-redis</span><span style="color:#000088"></artifactId></span>
<span style="color:#000088"></dependency></span></code></span></span>

由于现在是UI,因此不需要“/ resource”端点。完成后,您将拥有一个非常简单的Angular应用程序(与“基本”示例相同),这极大地简化了对其行为的测试和推理。

最后,我们希望此服务器作为后端运行,因此我们将为其提供一个非默认端口来监听(in application.properties):

application.properties

<span style="color:#34302d"><span style="color:#333333"><code class="language-properties"><span style="color:#000000">server</span><span style="color:#666600">.</span><span style="color:#000000">port</span><span style="color:#666600">:</span> <span style="color:#006666">8081</span><span style="color:#000000">
security</span><span style="color:#666600">.</span><span style="color:#000000">sessions</span><span style="color:#666600">:</span><span style="color:#000000"> NEVER</span></code></span></span>

如果这是整个内容,application.properties则应用程序将是安全的,并且对于名为“user”的用户可以访问,其密码是随机的,但在启动时打印在控制台上(在日志级别INFO)。“security.sessions”设置意味着Spring Security将接受cookie作为身份验证令牌,但除非它们已存在,否则不会创建它们。

资源服务器

资源服务器很容易从我们现有的样本中生成。它与第III节中的“spring-session”资源服务器相同:只是一个“/ resource”端点Spring Session来获取分布式会话数据。我们希望这个服务器有一个非默认端口来监听,我们希望能够在会话中查找身份验证,所以我们需要这个(in application.properties):

application.properties

<span style="color:#34302d"><span style="color:#333333"><code class="language-properties"><span style="color:#000000">server</span><span style="color:#666600">.</span><span style="color:#000000">port</span><span style="color:#666600">:</span> <span style="color:#006666">9000</span><span style="color:#000000">
security</span><span style="color:#666600">.</span><span style="color:#000000">sessions</span><span style="color:#666600">:</span><span style="color:#000000"> NEVER</span></code></span></span>

我们将对我们的消息资源进行POST更改,这是本教程中的一项新功能。这意味着我们将在后端需要CSRF保护,我们需要做一些通常的技巧,使Spring Security能够很好地与Angular一起使用:

<span style="color:#34302d"><span style="color:#333333"><code class="language-java"><span style="color:#006666">@Override</span>
<span style="color:#000088">protected</span> <span style="color:#000088">void</span><span style="color:#000000"> configure</span><span style="color:#666600">(</span><span style="color:#660066">HttpSecurity</span><span style="color:#000000"> http</span><span style="color:#666600">)</span> <span style="color:#000088">throws</span> <span style="color:#660066">Exception</span> <span style="color:#666600">{</span><span style="color:#000000">
	http</span><span style="color:#666600">.</span><span style="color:#000000">csrf</span><span style="color:#666600">()</span>
			<span style="color:#666600">.</span><span style="color:#000000">csrfTokenRepository</span><span style="color:#666600">(</span><span style="color:#660066">CookieCsrfTokenRepository</span><span style="color:#666600">.</span><span style="color:#000000">withHttpOnlyFalse</span><span style="color:#666600">());</span>
<span style="color:#666600">}</span></code></span></span>

如果你想偷看,完成的样本在github。

网关

对于Gateway的初始实现(最简单的可能工作),我们可以使用一个空的Spring Boot Web应用程序并添加@EnableZuulProxy注释。正如我们在第一部分中看到的,有几种方法可以做到这一点,一种是使用Spring Initializr生成一个骨架项目。更简单的是,使用Spring Cloud Initializr是一回事,但对于Spring Cloud应用程序。使用与第I部分相同的命令行操作序列:

<span style="color:#34302d"><span style="color:#333333"><code><span style="color:#000000">$ mkdir gateway </span><span style="color:#666600">&&</span><span style="color:#000000"> cd gateway
$ curl https</span><span style="color:#666600">:</span><span style="color:#880000">//cloud-start.spring.io/starter.tgz -d style=web \</span>
  <span style="color:#666600">-</span><span style="color:#000000">d style</span><span style="color:#666600">=</span><span style="color:#000000">security </span><span style="color:#666600">-</span><span style="color:#000000">d style</span><span style="color:#666600">=</span><span style="color:#000000">cloud</span><span style="color:#666600">-</span><span style="color:#000000">zuul </span><span style="color:#666600">-</span><span style="color:#000000">d name</span><span style="color:#666600">=</span><span style="color:#000000">gateway \
  </span><span style="color:#666600">-</span><span style="color:#000000">d style</span><span style="color:#666600">=</span><span style="color:#000000">redis </span><span style="color:#666600">|</span><span style="color:#000000"> tar </span><span style="color:#666600">-</span><span style="color:#000000">xzvf </span><span style="color:#666600">-</span></code></span></span>

然后,您可以将该项目(默认情况下是普通的Maven Java项目)导入您喜欢的IDE,或者只使用命令行中的文件和“mvn”。github中有一个版本,如果你想从那里去,但它有一些我们还不需要的额外功能。

从空白的Initializr应用程序开始,我们添加Spring Session依赖项(如上面的UI中所示)。网关已准备好运行,但它还不知道我们的后端服务,所以让我们在其中设置它application.ymlapplication.properties如果你做了上面的curl事件重命名):

application.yml

<span style="color:#34302d"><span style="color:#333333"><code><span style="color:#000000">zuul</span><span style="color:#666600">:</span><span style="color:#000000">
  sensitive</span><span style="color:#666600">-</span><span style="color:#000000">headers</span><span style="color:#666600">:</span><span style="color:#000000">
  routes</span><span style="color:#666600">:</span><span style="color:#000000">
    ui</span><span style="color:#666600">:</span><span style="color:#000000">
      url</span><span style="color:#666600">:</span><span style="color:#000000"> http</span><span style="color:#666600">:</span><span style="color:#880000">//localhost:8081</span><span style="color:#000000">
   resource</span><span style="color:#666600">:</span><span style="color:#000000">
      url</span><span style="color:#666600">:</span><span style="color:#000000"> http</span><span style="color:#666600">:</span><span style="color:#880000">//localhost:9000</span><span style="color:#000000">
security</span><span style="color:#666600">:</span><span style="color:#000000">
  user</span><span style="color:#666600">:</span><span style="color:#000000">
    password</span><span style="color:#666600">:</span><span style="color:#000000">
      password
  sessions</span><span style="color:#666600">:</span><span style="color:#000000"> ALWAYS</span></code></span></span>

代理中有2条路由,它们都使用sensitive-headers属性向下游传递cookie ,UI和资源服务器各有一条,我们设置了默认密码和会话持久性策略(告诉Spring Security始终创建会话)认证)。最后一点很重要,因为我们需要身份验证,因此会话在网关中进行管理。

启动并运行

我们现在有三个组件,在3个端口上运行。如果您将浏览器指向http:// localhost:8080 / ui /,您应该获得HTTP Basic挑战,并且您可以作为“用户/密码”(您在网关中的凭据)进行身份验证,一旦您这样做,您应该看到UI中的问候语,通过代理到资源服务器的后端调用。

如果您使用某些开发人员工具,则可以在浏览器中看到浏览器和后端之间的交互(通常F12打开它,默认情况下在Chrome中运行,可能需要Firefox中的插件)。这是一个总结:

动词路径状态响应

得到

/ ui /

401

浏览器提示进行身份验证

得到

/ ui /

200

的index.html

得到

/ui/*.js

200

角度资产

得到

/ui/js/hello.js

200

应用逻辑

得到

/ ui / user

200

认证

得到

/资源/

200

JSON问候语

您可能看不到401,因为浏览器将主页加载视为单个交互。所有请求都被代理(网关中没有内容,除了用于管理的Actuator端点之外)。

华友世纪,它有效!您有两个后端服务器,其中一个是UI,每个都具有独立功能并且能够单独进行测试,并且它们通过您控制的安全网关连接在一起,并为您配置了身份验证。如果浏览器无法访问后端,则无关紧要(实际上这可能是一个优势,因为它可以让您更好地控制物理安全性)。

添加登录表单

正如在第一部分的“基本”示例中,我们现在可以向网关添加登录表单,例如通过复制第II部分中的代码。当我们这样做时,我们还可以在网关中添加一些基本的导航元素,因此用户不必知道代理中UI后端的路径。因此,让我们首先将静态资产从“单个”UI复制到网关中,删除消息呈现并将登录表单插入到我们的主页(在<app/>某处):

app.html

<span style="color:#34302d"><span style="color:#333333"><code class="language-html"><span style="color:#000088"><div</span> <span style="color:#660066">class</span><span style="color:#666600">=</span><span style="color:#008800">"container"</span><span style="color:#000000"> [</span><span style="color:#660066">hidden</span><span style="color:#000000">]</span><span style="color:#666600">=</span><span style="color:#008800">"authenticated"</span><span style="color:#000088">></span>
	<span style="color:#000088"><form</span> <span style="color:#660066">role</span><span style="color:#666600">=</span><span style="color:#008800">"form"</span><span style="color:#000000"> (</span><span style="color:#660066">submit</span><span style="color:#000000">)</span><span style="color:#666600">=</span><span style="color:#008800">"login()"</span><span style="color:#000088">></span>
		<span style="color:#000088"><div</span> <span style="color:#660066">class</span><span style="color:#666600">=</span><span style="color:#008800">"form-group"</span><span style="color:#000088">></span>
			<span style="color:#000088"><label</span> <span style="color:#660066">for</span><span style="color:#666600">=</span><span style="color:#008800">"username"</span><span style="color:#000088">></span><span style="color:#000000">Username:</span><span style="color:#000088"></label></span> <span style="color:#000088"><input</span> <span style="color:#660066">type</span><span style="color:#666600">=</span><span style="color:#008800">"text"</span>
				<span style="color:#660066">class</span><span style="color:#666600">=</span><span style="color:#008800">"form-control"</span> <span style="color:#660066">id</span><span style="color:#666600">=</span><span style="color:#008800">"username"</span> <span style="color:#660066">name</span><span style="color:#666600">=</span><span style="color:#008800">"username"</span><span style="color:#000000">
				[(</span><span style="color:#660066">ngModel</span><span style="color:#000000">)]</span><span style="color:#666600">=</span><span style="color:#008800">"credentials.username"</span> <span style="color:#000088">/></span>
		<span style="color:#000088"></div></span>
		<span style="color:#000088"><div</span> <span style="color:#660066">class</span><span style="color:#666600">=</span><span style="color:#008800">"form-group"</span><span style="color:#000088">></span>
			<span style="color:#000088"><label</span> <span style="color:#660066">for</span><span style="color:#666600">=</span><span style="color:#008800">"password"</span><span style="color:#000088">></span><span style="color:#000000">Password:</span><span style="color:#000088"></label></span> <span style="color:#000088"><input</span> <span style="color:#660066">type</span><span style="color:#666600">=</span><span style="color:#008800">"password"</span>
				<span style="color:#660066">class</span><span style="color:#666600">=</span><span style="color:#008800">"form-control"</span> <span style="color:#660066">id</span><span style="color:#666600">=</span><span style="color:#008800">"password"</span> <span style="color:#660066">name</span><span style="color:#666600">=</span><span style="color:#008800">"password"</span><span style="color:#000000">
				[(</span><span style="color:#660066">ngModel</span><span style="color:#000000">)]</span><span style="color:#666600">=</span><span style="color:#008800">"credentials.password"</span> <span style="color:#000088">/></span>
		<span style="color:#000088"></div></span>
		<span style="color:#000088"><button</span> <span style="color:#660066">type</span><span style="color:#666600">=</span><span style="color:#008800">"submit"</span> <span style="color:#660066">class</span><span style="color:#666600">=</span><span style="color:#008800">"btn btn-primary"</span><span style="color:#000088">></span><span style="color:#000000">Submit</span><span style="color:#000088"></button></span>
	<span style="color:#000088"></form></span>
<span style="color:#000088"></div></span></code></span></span>

而不是消息呈现,我们将有一个很好的大导航按钮:

的index.html

<span style="color:#34302d"><span style="color:#333333"><code class="language-html"><span style="color:#000088"><div</span> <span style="color:#660066">class</span><span style="color:#666600">=</span><span style="color:#008800">"container"</span><span style="color:#000000"> [</span><span style="color:#660066">hidden</span><span style="color:#000000">]</span><span style="color:#666600">=</span><span style="color:#008800">"!authenticated"</span><span style="color:#000088">></span>
	<span style="color:#000088"><a</span> <span style="color:#660066">class</span><span style="color:#666600">=</span><span style="color:#008800">"btn btn-primary"</span> <span style="color:#660066">href</span><span style="color:#666600">=</span><span style="color:#008800">"/ui/"</span><span style="color:#000088">></span><span style="color:#000000">Go To User Interface</span><span style="color:#000088"></a></span>
<span style="color:#000088"></div></span></code></span></span>

如果您正在查看github中的示例,它还有一个带有“Logout”按钮的最小导航栏。这是截图中的登录表单:

为了支持登录表单,我们需要一些带有实现login()我们在其中声明的函数的组件的TypeScript <form/>,并且我们需要设置authenticated标志,以便主页的呈现方式不同,具体取决于用户是否经过身份验证。例如:

appponent.ts

<span style="color:#34302d"><span style="color:#333333"><code class="language-javascript"><span style="color:#000000">include</span><span style="color:#666600">::</span><span style="color:#000000">src</span><span style="color:#666600">/</span><span style="color:#000000">app</span><span style="color:#666600">/</span><span style="color:#000000">app</span><span style="color:#666600">.</span><span style="color:#000000">component</span><span style="color:#666600">.</span><span style="color:#000000">ts</span></code></span></span>

其中login()函数的实现类似于第二节中的实现。

我们可以使用它self来存储authenticated标志,因为这个简单的应用程序中只有一个组件。

如果我们运行此增强型网关,而不必记住UI的URL,我们只需加载主页并关注链接即可。这是经过身份验证的用户的主页:

后端的粒度访问决策

到目前为止,我们的应用程序在功能上与第III部分或第IV 部分中的应用非常相似,但具有额外的专用网关。额外层的优势可能尚不明显,但我们可以通过扩展系统来强调它。假设我们希望使用该网关公开另一个后端UI,以便用户“管理”主UI中的内容,并且我们希望限制对具有特殊角色的用户访问此功能。所以我们将在代理后面添加一个“Admin”应用程序,系统将如下所示:

Gateway中有一个新组件(Admin)和一个新路由application.yml

application.yml

<span style="color:#34302d"><span style="color:#333333"><code class="language-yaml"><span style="color:#000000">zuul</span><span style="color:#666600">:</span><span style="color:#000000">
  sensitive</span><span style="color:#666600">-</span><span style="color:#000000">headers</span><span style="color:#666600">:</span><span style="color:#000000">
  routes</span><span style="color:#666600">:</span><span style="color:#000000">
    ui</span><span style="color:#666600">:</span><span style="color:#000000">
      url</span><span style="color:#666600">:</span><span style="color:#000000"> http</span><span style="color:#666600">:</span><span style="color:#880000">//localhost:8081</span><span style="color:#000000">
    admin</span><span style="color:#666600">:</span><span style="color:#000000">
      url</span><span style="color:#666600">:</span><span style="color:#000000"> http</span><span style="color:#666600">:</span><span style="color:#880000">//localhost:8082</span><span style="color:#000000">
    resource</span><span style="color:#666600">:</span><span style="color:#000000">
      url</span><span style="color:#666600">:</span><span style="color:#000000"> http</span><span style="color:#666600">:</span><span style="color:#880000">//localhost:9000</span></code></span></span>

“USER”角色中的用户可以使用现有UI的事实在“网关”框(绿色字母)上方的框图中显示,因为需要“ADMIN”角色才能转到Admin应用程序。“ADMIN”角色的访问决策可以在Gateway中应用,在这种情况下它将出现在a中WebSecurityConfigurerAdapter,或者可以在Admin应用程序本身中应用(我们将在下面看到如何执行此操作)。

首先,创建一个新的Spring Boot应用程序,或者复制UI并对其进行编辑。除了名称之外,您不需要在UI应用程序中进行太多更改。完成的应用程序在Github这里。

假设在Admin应用程序中我们要区分“READER”和“WRITER”角色,以便我们允许(比方说)作为审计员的用户查看主管理员用户所做的更改。这是一种细粒度访问决策,其中规则仅在后端应用程序中是已知的,并且应该是已知的。在Gateway中,我们只需要确保我们的用户帐户具有所需的角色,并且此信息可用,但Gateway不需要知道如何解释它。在Gateway中,我们创建用户帐户以保持示例应用程序自包含:

SecurityConfiguration.class

<span style="color:#34302d"><span style="color:#333333"><code class="language-javascript"><span style="color:#006666">@Configuration</span>
<span style="color:#000088">public</span> <span style="color:#000088">class</span> <span style="color:#660066">SecurityConfiguration</span> <span style="color:#000088">extends</span> <span style="color:#660066">WebSecurityConfigurerAdapter</span> <span style="color:#666600">{</span>

  <span style="color:#006666">@Autowired</span>
  <span style="color:#000088">public</span> <span style="color:#000088">void</span><span style="color:#000000"> globalUserDetails</span><span style="color:#666600">(</span><span style="color:#660066">AuthenticationManagerBuilder</span><span style="color:#000000"> auth</span><span style="color:#666600">)</span> <span style="color:#000088">throws</span> <span style="color:#660066">Exception</span> <span style="color:#666600">{</span><span style="color:#000000">
    auth</span><span style="color:#666600">.</span><span style="color:#000000">inMemoryAuthentication</span><span style="color:#666600">()</span>
      <span style="color:#666600">.</span><span style="color:#000000">withUser</span><span style="color:#666600">(</span><span style="color:#008800">"user"</span><span style="color:#666600">).</span><span style="color:#000000">password</span><span style="color:#666600">(</span><span style="color:#008800">"password"</span><span style="color:#666600">).</span><span style="color:#000000">roles</span><span style="color:#666600">(</span><span style="color:#008800">"USER"</span><span style="color:#666600">)</span>
    <span style="color:#666600">.</span><span style="color:#000088">and</span><span style="color:#666600">()</span>
      <span style="color:#666600">.</span><span style="color:#000000">withUser</span><span style="color:#666600">(</span><span style="color:#008800">"admin"</span><span style="color:#666600">).</span><span style="color:#000000">password</span><span style="color:#666600">(</span><span style="color:#008800">"admin"</span><span style="color:#666600">).</span><span style="color:#000000">roles</span><span style="color:#666600">(</span><span style="color:#008800">"USER"</span><span style="color:#666600">,</span> <span style="color:#008800">"ADMIN"</span><span style="color:#666600">,</span> <span style="color:#008800">"READER"</span><span style="color:#666600">,</span> <span style="color:#008800">"WRITER"</span><span style="color:#666600">)</span>
    <span style="color:#666600">.</span><span style="color:#000088">and</span><span style="color:#666600">()</span>
      <span style="color:#666600">.</span><span style="color:#000000">withUser</span><span style="color:#666600">(</span><span style="color:#008800">"audit"</span><span style="color:#666600">).</span><span style="color:#000000">password</span><span style="color:#666600">(</span><span style="color:#008800">"audit"</span><span style="color:#666600">).</span><span style="color:#000000">roles</span><span style="color:#666600">(</span><span style="color:#008800">"USER"</span><span style="color:#666600">,</span> <span style="color:#008800">"ADMIN"</span><span style="color:#666600">,</span> <span style="color:#008800">"READER"</span><span style="color:#666600">);</span>
  <span style="color:#666600">}</span>

<span style="color:#666600">}</span></code></span></span>

其中“admin”用户已使用3个新角色(“ADMIN”,“READER”和“WRITER”)进行了增强,我们还添加了一个“ADMIN”访问权限的“审核”用户,但不是“WRITER”。

 在生产系统中,用户帐户数据将在后端数据库(很可能是目录服务)中进行管理,而不是在Spring配置中进行硬编码。连接到此类数据库的示例应用程序很容易在Internet上找到,例如在Spring Security Samples中。

访问决策在Admin应用程序中进行。对于“ADMIN”角色(此后端全局需要),我们在Spring Security中执行此操作:

SecurityConfiguration.java

<span style="color:#34302d"><span style="color:#333333"><code class="language-java"><span style="color:#006666">@Configuration</span>
<span style="color:#000088">public</span> <span style="color:#000088">class</span> <span style="color:#660066">SecurityConfiguration</span> <span style="color:#000088">extends</span> <span style="color:#660066">WebSecurityConfigurerAdapter</span> <span style="color:#666600">{</span>

<span style="color:#006666">@Override</span>
  <span style="color:#000088">protected</span> <span style="color:#000088">void</span><span style="color:#000000"> configure</span><span style="color:#666600">(</span><span style="color:#660066">HttpSecurity</span><span style="color:#000000"> http</span><span style="color:#666600">)</span> <span style="color:#000088">throws</span> <span style="color:#660066">Exception</span> <span style="color:#666600">{</span><span style="color:#000000">
    http
    </span><span style="color:#666600">...</span>
      <span style="color:#666600">.</span><span style="color:#000000">authorizeRequests</span><span style="color:#666600">()</span>
        <span style="color:#666600">.</span><span style="color:#000000">antMatchers</span><span style="color:#666600">(</span><span style="color:#008800">"/index.html"</span><span style="color:#666600">,</span> <span style="color:#008800">"/"</span><span style="color:#666600">).</span><span style="color:#000000">permitAll</span><span style="color:#666600">()</span>
        <span style="color:#666600">.</span><span style="color:#000000">antMatchers</span><span style="color:#666600">(</span><span style="color:#008800">"/admin/**"</span><span style="color:#666600">).</span><span style="color:#000000">hasRole</span><span style="color:#666600">(</span><span style="color:#008800">"ADMIN"</span><span style="color:#666600">)</span>
        <span style="color:#666600">.</span><span style="color:#000000">anyRequest</span><span style="color:#666600">().</span><span style="color:#000000">authenticated</span><span style="color:#666600">()</span>
    <span style="color:#666600">...</span>
  <span style="color:#666600">}</span>

<span style="color:#666600">}</span></code></span></span>

对于“READER”和“WRITER”角色,应用程序本身是分开的,并且由于应用程序是用JavaScript实现的,因此我们需要做出访问决策。一种方法是通过路由器在其中嵌入带有计算视图的主页:

appponent.html

<span style="color:#34302d"><span style="color:#333333"><code class="language-html"><span style="color:#000088"><div</span> <span style="color:#660066">class</span><span style="color:#666600">=</span><span style="color:#008800">"container"</span><span style="color:#000088">></span>
	<span style="color:#000088"><h1></span><span style="color:#000000">Admin</span><span style="color:#000088"></h1></span>
	<span style="color:#000088"><router-outlet></router-outlet></span>
<span style="color:#000088"></div></span></code></span></span>

组件加载时计算路径:

appponent.ts

<span style="color:#34302d"><span style="color:#333333"><code class="language-javascript"><span style="color:#006666">@Component</span><span style="color:#666600">({</span><span style="color:#000000">
  selector</span><span style="color:#666600">:</span> <span style="color:#008800">'app-root'</span><span style="color:#666600">,</span><span style="color:#000000">
  templateUrl</span><span style="color:#666600">:</span> <span style="color:#008800">'./appponent.html'</span><span style="color:#666600">,</span><span style="color:#000000">
  styleUrls</span><span style="color:#666600">:</span> <span style="color:#666600">[</span><span style="color:#008800">'./appponent.css'</span><span style="color:#666600">]</span>
<span style="color:#666600">})</span>
<span style="color:#000088">export</span> <span style="color:#000088">class</span> <span style="color:#660066">AppComponent</span> <span style="color:#666600">{</span><span style="color:#000000">

  user</span><span style="color:#666600">:</span> <span style="color:#666600">{};</span><span style="color:#000000">

  constructor</span><span style="color:#666600">(</span><span style="color:#000088">private</span><span style="color:#000000"> app</span><span style="color:#666600">:</span> <span style="color:#660066">AppService</span><span style="color:#666600">,</span> <span style="color:#000088">private</span><span style="color:#000000"> http</span><span style="color:#666600">:</span> <span style="color:#660066">HttpClient</span><span style="color:#666600">,</span> <span style="color:#000088">private</span><span style="color:#000000"> router</span><span style="color:#666600">:</span> <span style="color:#660066">Router</span><span style="color:#666600">)</span> <span style="color:#666600">{</span><span style="color:#000000">
    app</span><span style="color:#666600">.</span><span style="color:#000000">authenticate</span><span style="color:#666600">(</span><span style="color:#000000">response </span><span style="color:#666600">=></span> <span style="color:#666600">{</span>
      <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">user </span><span style="color:#666600">=</span><span style="color:#000000"> response</span><span style="color:#666600">;</span>
      <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">message</span><span style="color:#666600">();</span>
    <span style="color:#666600">});</span>
  <span style="color:#666600">}</span><span style="color:#000000">

  logout</span><span style="color:#666600">()</span> <span style="color:#666600">{</span>
    <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">http</span><span style="color:#666600">.</span><span style="color:#000000">post</span><span style="color:#666600">(</span><span style="color:#008800">'logout'</span><span style="color:#666600">,</span> <span style="color:#666600">{}).</span><span style="color:#000000">subscribe</span><span style="color:#666600">(</span><span style="color:#000088">function</span><span style="color:#666600">()</span> <span style="color:#666600">{</span>
        <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">app</span><span style="color:#666600">.</span><span style="color:#000000">authenticated </span><span style="color:#666600">=</span> <span style="color:#000088">false</span><span style="color:#666600">;</span>
        <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">router</span><span style="color:#666600">.</span><span style="color:#000000">navigateByUrl</span><span style="color:#666600">(</span><span style="color:#008800">'/login'</span><span style="color:#666600">);</span>
    <span style="color:#666600">});</span>
  <span style="color:#666600">}</span><span style="color:#000000">

  message</span><span style="color:#666600">()</span> <span style="color:#666600">{</span>
    <span style="color:#000088">if</span> <span style="color:#666600">(!</span><span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">app</span><span style="color:#666600">.</span><span style="color:#000000">authenticated</span><span style="color:#666600">)</span> <span style="color:#666600">{</span>
      <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">router</span><span style="color:#666600">.</span><span style="color:#000000">navigate</span><span style="color:#666600">([</span><span style="color:#008800">'/unauthenticated'</span><span style="color:#666600">]);</span>
    <span style="color:#666600">}</span> <span style="color:#000088">else</span> <span style="color:#666600">{</span>
      <span style="color:#000088">if</span> <span style="color:#666600">(</span><span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">app</span><span style="color:#666600">.</span><span style="color:#000000">writer</span><span style="color:#666600">)</span> <span style="color:#666600">{</span>
        <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">router</span><span style="color:#666600">.</span><span style="color:#000000">navigate</span><span style="color:#666600">([</span><span style="color:#008800">'/write'</span><span style="color:#666600">]);</span>
      <span style="color:#666600">}</span> <span style="color:#000088">else</span> <span style="color:#666600">{</span>
        <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">router</span><span style="color:#666600">.</span><span style="color:#000000">navigate</span><span style="color:#666600">([</span><span style="color:#008800">'/read'</span><span style="color:#666600">]);</span>
      <span style="color:#666600">}</span>
    <span style="color:#666600">}</span>
  <span style="color:#666600">}</span>
<span style="color:#666600">...</span>
<span style="color:#666600">}</span></code></span></span>

应用程序所做的第一件事就是检查用户是否经过身份验证,并通过查看用户数据来计算路由。路径在主模块中声明:

app.module.ts

<span style="color:#34302d"><span style="color:#333333"><code class="language-javascript"><span style="color:#000088">const</span><span style="color:#000000"> routes</span><span style="color:#666600">:</span> <span style="color:#660066">Routes</span> <span style="color:#666600">=</span> <span style="color:#666600">[</span>
  <span style="color:#666600">{</span><span style="color:#000000"> path</span><span style="color:#666600">:</span> <span style="color:#008800">''</span><span style="color:#666600">,</span><span style="color:#000000"> pathMatch</span><span style="color:#666600">:</span> <span style="color:#008800">'full'</span><span style="color:#666600">,</span><span style="color:#000000"> redirectTo</span><span style="color:#666600">:</span> <span style="color:#008800">'read'</span><span style="color:#666600">},</span>
  <span style="color:#666600">{</span><span style="color:#000000"> path</span><span style="color:#666600">:</span> <span style="color:#008800">'read'</span><span style="color:#666600">,</span><span style="color:#000000"> component</span><span style="color:#666600">:</span> <span style="color:#660066">ReadComponent</span><span style="color:#666600">},</span>
  <span style="color:#666600">{</span><span style="color:#000000"> path</span><span style="color:#666600">:</span> <span style="color:#008800">'write'</span><span style="color:#666600">,</span><span style="color:#000000"> component</span><span style="color:#666600">:</span> <span style="color:#660066">WriteComponent</span><span style="color:#666600">},</span>
  <span style="color:#666600">{</span><span style="color:#000000"> path</span><span style="color:#666600">:</span> <span style="color:#008800">'unauthenticated'</span><span style="color:#666600">,</span><span style="color:#000000"> component</span><span style="color:#666600">:</span> <span style="color:#660066">UnauthenticatedComponent</span><span style="color:#666600">},</span>
  <span style="color:#666600">{</span><span style="color:#000000"> path</span><span style="color:#666600">:</span> <span style="color:#008800">'changes'</span><span style="color:#666600">,</span><span style="color:#000000"> component</span><span style="color:#666600">:</span> <span style="color:#660066">ChangesComponent</span><span style="color:#666600">}</span>
<span style="color:#666600">];</span></code></span></span>

每个组件(每个路由一个)必须单独实现。这ReadComponent是一个例子:

readponent.ts

<span style="color:#34302d"><span style="color:#333333"><code class="language-javascript"><span style="color:#000088">import</span> <span style="color:#666600">{</span> <span style="color:#660066">Component</span> <span style="color:#666600">}</span> <span style="color:#000088">from</span> <span style="color:#008800">'@angular/core'</span><span style="color:#666600">;</span>
<span style="color:#000088">import</span> <span style="color:#666600">{</span> <span style="color:#660066">HttpClient</span> <span style="color:#666600">}</span> <span style="color:#000088">from</span> <span style="color:#008800">'@angular/common/http'</span><span style="color:#666600">;</span>

<span style="color:#006666">@Component</span><span style="color:#666600">({</span><span style="color:#000000">
  templateUrl</span><span style="color:#666600">:</span> <span style="color:#008800">'./readponent.html'</span>
<span style="color:#666600">})</span>
<span style="color:#000088">export</span> <span style="color:#000088">class</span> <span style="color:#660066">ReadComponent</span> <span style="color:#666600">{</span><span style="color:#000000">

  greeting </span><span style="color:#666600">=</span> <span style="color:#666600">{};</span><span style="color:#000000">

  constructor</span><span style="color:#666600">(</span><span style="color:#000088">private</span><span style="color:#000000"> http</span><span style="color:#666600">:</span> <span style="color:#660066">HttpClient</span><span style="color:#666600">)</span> <span style="color:#666600">{</span><span style="color:#000000">
    http</span><span style="color:#666600">.</span><span style="color:#000088">get</span><span style="color:#666600">(</span><span style="color:#008800">'/resource'</span><span style="color:#666600">).</span><span style="color:#000000">subscribe</span><span style="color:#666600">(</span><span style="color:#000000">data </span><span style="color:#666600">=></span> <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">greeting </span><span style="color:#666600">=</span><span style="color:#000000"> data</span><span style="color:#666600">);</span>
  <span style="color:#666600">}</span>

<span style="color:#666600">}</span></code></span></span>

readponent.html

<span style="color:#34302d"><span style="color:#333333"><code class="language-html"><span style="color:#000088"><h1></span><span style="color:#000000">Greeting</span><span style="color:#000088"></h1></span>
<span style="color:#000088"><div></span>
	<span style="color:#000088"><p></span><span style="color:#000000">The ID is {{greeting.id}}</span><span style="color:#000088"></p></span>
	<span style="color:#000088"><p></span><span style="color:#000000">The content is {{greeting.content}}</span><span style="color:#000088"></p></span>
<span style="color:#000088"></div></span></code></span></span>

WriteComponent是类似的,但具有这样的形式在后端改变信息:

writeponent.ts

<span style="color:#34302d"><span style="color:#333333"><code class="language-javascript"><span style="color:#000088">import</span> <span style="color:#666600">{</span> <span style="color:#660066">Component</span> <span style="color:#666600">}</span> <span style="color:#000088">from</span> <span style="color:#008800">'@angular/core'</span><span style="color:#666600">;</span>
<span style="color:#000088">import</span> <span style="color:#666600">{</span> <span style="color:#660066">HttpClient</span> <span style="color:#666600">}</span> <span style="color:#000088">from</span> <span style="color:#008800">'@angular/common/http'</span><span style="color:#666600">;</span>

<span style="color:#006666">@Component</span><span style="color:#666600">({</span><span style="color:#000000">
  templateUrl</span><span style="color:#666600">:</span> <span style="color:#008800">'./writeponent.html'</span>
<span style="color:#666600">})</span>
<span style="color:#000088">export</span> <span style="color:#000088">class</span> <span style="color:#660066">WriteComponent</span> <span style="color:#666600">{</span><span style="color:#000000">

  greeting </span><span style="color:#666600">=</span> <span style="color:#666600">{};</span><span style="color:#000000">

  constructor</span><span style="color:#666600">(</span><span style="color:#000088">private</span><span style="color:#000000"> http</span><span style="color:#666600">:</span> <span style="color:#660066">HttpClient</span><span style="color:#666600">)</span> <span style="color:#666600">{</span>
    <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">http</span><span style="color:#666600">.</span><span style="color:#000088">get</span><span style="color:#666600">(</span><span style="color:#008800">'http://localhost:9000'</span><span style="color:#666600">).</span><span style="color:#000000">subscribe</span><span style="color:#666600">(</span><span style="color:#000000">data </span><span style="color:#666600">=></span> <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">greeting </span><span style="color:#666600">=</span><span style="color:#000000"> data</span><span style="color:#666600">);</span>
  <span style="color:#666600">}</span><span style="color:#000000">

  update</span><span style="color:#666600">()</span> <span style="color:#666600">{</span>
    <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">http</span><span style="color:#666600">.</span><span style="color:#000000">post</span><span style="color:#666600">(</span><span style="color:#008800">'/resource'</span><span style="color:#666600">,</span> <span style="color:#666600">{</span><span style="color:#000000">content</span><span style="color:#666600">:</span> <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">greeting</span><span style="color:#666600">[</span><span style="color:#008800">'content'</span><span style="color:#666600">]}).</span><span style="color:#000000">subscribe</span><span style="color:#666600">(</span><span style="color:#000000">response </span><span style="color:#666600">=></span> <span style="color:#666600">{</span>
      <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">greeting </span><span style="color:#666600">=</span><span style="color:#000000"> response</span><span style="color:#666600">;</span>
    <span style="color:#666600">});</span>
  <span style="color:#666600">}</span>

<span style="color:#666600">}</span></code></span></span>

writeponent.html

<span style="color:#34302d"><span style="color:#333333"><code class="language-html"><span style="color:#000088"><form</span><span style="color:#000000"> (</span><span style="color:#660066">submit</span><span style="color:#000000">)</span><span style="color:#666600">=</span><span style="color:#008800">"update()"</span><span style="color:#000088">></span>
	<span style="color:#000088"><p></span><span style="color:#000000">The ID is {{greeting.id}}</span><span style="color:#000088"></p></span>
	<span style="color:#000088"><div</span> <span style="color:#660066">class</span><span style="color:#666600">=</span><span style="color:#008800">"form-group"</span><span style="color:#000088">></span>
		<span style="color:#000088"><label</span> <span style="color:#660066">for</span><span style="color:#666600">=</span><span style="color:#008800">"username"</span><span style="color:#000088">></span><span style="color:#000000">Content:</span><span style="color:#000088"></label></span> <span style="color:#000088"><input</span> <span style="color:#660066">type</span><span style="color:#666600">=</span><span style="color:#008800">"text"</span>
			<span style="color:#660066">class</span><span style="color:#666600">=</span><span style="color:#008800">"form-control"</span> <span style="color:#660066">id</span><span style="color:#666600">=</span><span style="color:#008800">"content"</span> <span style="color:#660066">name</span><span style="color:#666600">=</span><span style="color:#008800">"content"</span><span style="color:#000000"> [(</span><span style="color:#660066">ngModel</span><span style="color:#000000">)]</span><span style="color:#666600">=</span><span style="color:#008800">"greeting.content"</span><span style="color:#000088">/></span>
	<span style="color:#000088"></div></span>
	<span style="color:#000088"><button</span> <span style="color:#660066">type</span><span style="color:#666600">=</span><span style="color:#008800">"submit"</span> <span style="color:#660066">class</span><span style="color:#666600">=</span><span style="color:#008800">"btn btn-primary"</span><span style="color:#000088">></span><span style="color:#000000">Submit</span><span style="color:#000088"></button></span>
<span style="color:#000088"></form></span></code></span></span>

AppService还需要提供数据来计算的路线,所以在authenticate()函数中我们看到:

app.service.ts

<span style="color:#34302d"><span style="color:#333333"><code class="language-javascript"><span style="color:#000000">        http</span><span style="color:#666600">.</span><span style="color:#000088">get</span><span style="color:#666600">(</span><span style="color:#008800">'/user'</span><span style="color:#666600">).</span><span style="color:#000000">subscribe</span><span style="color:#666600">(</span><span style="color:#000088">function</span><span style="color:#666600">(</span><span style="color:#000000">response</span><span style="color:#666600">)</span> <span style="color:#666600">{</span>
            <span style="color:#000088">var</span><span style="color:#000000"> user </span><span style="color:#666600">=</span><span style="color:#000000"> response</span><span style="color:#666600">.</span><span style="color:#000000">json</span><span style="color:#666600">();</span>
            <span style="color:#000088">if</span> <span style="color:#666600">(</span><span style="color:#000000">user</span><span style="color:#666600">.</span><span style="color:#000000">name</span><span style="color:#666600">)</span> <span style="color:#666600">{</span>
                <span style="color:#000088">self</span><span style="color:#666600">.</span><span style="color:#000000">authenticated </span><span style="color:#666600">=</span> <span style="color:#000088">true</span><span style="color:#666600">;</span>
                <span style="color:#000088">self</span><span style="color:#666600">.</span><span style="color:#000000">writer </span><span style="color:#666600">=</span><span style="color:#000000"> user</span><span style="color:#666600">.</span><span style="color:#000000">roles </span><span style="color:#666600">&&</span><span style="color:#000000"> user</span><span style="color:#666600">.</span><span style="color:#000000">roles</span><span style="color:#666600">.</span><span style="color:#000000">indexOf</span><span style="color:#666600">(</span><span style="color:#008800">"ROLE_WRITER"</span><span style="color:#666600">)></span><span style="color:#006666">0</span><span style="color:#666600">;</span>
            <span style="color:#666600">}</span> <span style="color:#000088">else</span> <span style="color:#666600">{</span>
                <span style="color:#000088">self</span><span style="color:#666600">.</span><span style="color:#000000">authenticated </span><span style="color:#666600">=</span> <span style="color:#000088">false</span><span style="color:#666600">;</span>
                <span style="color:#000088">self</span><span style="color:#666600">.</span><span style="color:#000000">writer </span><span style="color:#666600">=</span> <span style="color:#000088">false</span><span style="color:#666600">;</span>
            <span style="color:#666600">}</span><span style="color:#000000">
            callback </span><span style="color:#666600">&&</span><span style="color:#000000"> callback</span><span style="color:#666600">(</span><span style="color:#000000">response</span><span style="color:#666600">);</span>
        <span style="color:#666600">})</span></code></span></span>

要在后端支持此功能,我们需要/user端点,例如在我们的主应用程序类中:

AdminApplication.java

<span style="color:#34302d"><span style="color:#333333"><code class="language-java"><span style="color:#006666">@SpringBootApplication</span>
<span style="color:#006666">@RestController</span>
<span style="color:#000088">public</span> <span style="color:#000088">class</span> <span style="color:#660066">AdminApplication</span> <span style="color:#666600">{</span>

  <span style="color:#006666">@RequestMapping</span><span style="color:#666600">(</span><span style="color:#008800">"/user"</span><span style="color:#666600">)</span>
  <span style="color:#000088">public</span> <span style="color:#660066">Map</span><span style="color:#666600"><</span><span style="color:#660066">String</span><span style="color:#666600">,</span> <span style="color:#660066">Object</span><span style="color:#666600">></span><span style="color:#000000"> user</span><span style="color:#666600">(</span><span style="color:#660066">Principal</span><span style="color:#000000"> user</span><span style="color:#666600">)</span> <span style="color:#666600">{</span>
    <span style="color:#660066">Map</span><span style="color:#666600"><</span><span style="color:#660066">String</span><span style="color:#666600">,</span> <span style="color:#660066">Object</span><span style="color:#666600">></span><span style="color:#000000"> map </span><span style="color:#666600">=</span> <span style="color:#000088">new</span> <span style="color:#660066">LinkedHashMap</span><span style="color:#666600"><</span><span style="color:#660066">String</span><span style="color:#666600">,</span> <span style="color:#660066">Object</span><span style="color:#666600">>();</span><span style="color:#000000">
    map</span><span style="color:#666600">.</span><span style="color:#000000">put</span><span style="color:#666600">(</span><span style="color:#008800">"name"</span><span style="color:#666600">,</span><span style="color:#000000"> user</span><span style="color:#666600">.</span><span style="color:#000000">getName</span><span style="color:#666600">());</span><span style="color:#000000">
    map</span><span style="color:#666600">.</span><span style="color:#000000">put</span><span style="color:#666600">(</span><span style="color:#008800">"roles"</span><span style="color:#666600">,</span> <span style="color:#660066">AuthorityUtils</span><span style="color:#666600">.</span><span style="color:#000000">authorityListToSet</span><span style="color:#666600">(((</span><span style="color:#660066">Authentication</span><span style="color:#666600">)</span><span style="color:#000000"> user</span><span style="color:#666600">)</span>
        <span style="color:#666600">.</span><span style="color:#000000">getAuthorities</span><span style="color:#666600">()));</span>
    <span style="color:#000088">return</span><span style="color:#000000"> map</span><span style="color:#666600">;</span>
  <span style="color:#666600">}</span>

  <span style="color:#000088">public</span> <span style="color:#000088">static</span> <span style="color:#000088">void</span><span style="color:#000000"> main</span><span style="color:#666600">(</span><span style="color:#660066">String</span><span style="color:#666600">[]</span><span style="color:#000000"> args</span><span style="color:#666600">)</span> <span style="color:#666600">{</span>
    <span style="color:#660066">SpringApplication</span><span style="color:#666600">.</span><span style="color:#000000">run</span><span style="color:#666600">(</span><span style="color:#660066">AdminApplication</span><span style="color:#666600">.</span><span style="color:#000088">class</span><span style="color:#666600">,</span><span style="color:#000000"> args</span><span style="color:#666600">);</span>
  <span style="color:#666600">}</span>

<span style="color:#666600">}</span></code></span></span>
 角色名称从“/ user”端点返回,带有“ROLE_”前缀,因此我们可以将它们与其他类型的权限区分开来(这是Spring Security的事情)。因此,JavaScript中需要“ROLE_”前缀,但在Spring Security配置中不需要,从方法名称可以清楚地看出“角色”是操作的焦点。

支持管理UI的网关更改

我们将使用角色在网关中做出访问决策(因此我们可以有条件地显示指向管理UI的链接),因此我们应该将“角色”添加到网关中的“/ user”端点。一旦到位,我们可以添加一些JavaScript来设置一个标志,以指示当前用户是“ADMIN”。在authenticated()功能中:

appponent.ts

<span style="color:#34302d"><span style="color:#333333"><code class="language-javascript"><span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">http</span><span style="color:#666600">.</span><span style="color:#000088">get</span><span style="color:#666600">(</span><span style="color:#008800">'user'</span><span style="color:#666600">,</span> <span style="color:#666600">{</span><span style="color:#000000">headers</span><span style="color:#666600">:</span><span style="color:#000000"> headers</span><span style="color:#666600">}).</span><span style="color:#000000">subscribe</span><span style="color:#666600">(</span><span style="color:#000000">data </span><span style="color:#666600">=></span> <span style="color:#666600">{</span>
  <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">authenticated </span><span style="color:#666600">=</span><span style="color:#000000"> data </span><span style="color:#666600">&&</span><span style="color:#000000"> data</span><span style="color:#666600">[</span><span style="color:#008800">'name'</span><span style="color:#666600">];</span>
  <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">user </span><span style="color:#666600">=</span> <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">authenticated </span><span style="color:#666600">?</span><span style="color:#000000"> data</span><span style="color:#666600">[</span><span style="color:#008800">'name'</span><span style="color:#666600">]</span> <span style="color:#666600">:</span> <span style="color:#008800">''</span><span style="color:#666600">;</span>
  <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">admin </span><span style="color:#666600">=</span> <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">authenticated </span><span style="color:#666600">&&</span><span style="color:#000000"> data</span><span style="color:#666600">[</span><span style="color:#008800">'roles'</span><span style="color:#666600">]</span> <span style="color:#666600">&&</span><span style="color:#000000"> data</span><span style="color:#666600">[</span><span style="color:#008800">'roles'</span><span style="color:#666600">].</span><span style="color:#000000">indexOf</span><span style="color:#666600">(</span><span style="color:#008800">'ROLE_ADMIN'</span><span style="color:#666600">)</span> <span style="color:#666600">></span> <span style="color:#666600">-</span><span style="color:#006666">1</span><span style="color:#666600">;</span>
<span style="color:#666600">});</span></code></span></span>

我们还需要在用户注销时将admin标志重置为false

appponent.ts

<span style="color:#34302d"><span style="color:#333333"><code class="language-javascript"><span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">logout </span><span style="color:#666600">=</span> <span style="color:#000088">function</span><span style="color:#666600">()</span> <span style="color:#666600">{</span><span style="color:#000000">
    http</span><span style="color:#666600">.</span><span style="color:#000000">post</span><span style="color:#666600">(</span><span style="color:#008800">'logout'</span><span style="color:#666600">,</span> <span style="color:#666600">{}).</span><span style="color:#000000">subscribe</span><span style="color:#666600">(</span><span style="color:#000088">function</span><span style="color:#666600">()</span> <span style="color:#666600">{</span>
        <span style="color:#000088">self</span><span style="color:#666600">.</span><span style="color:#000000">authenticated </span><span style="color:#666600">=</span> <span style="color:#000088">false</span><span style="color:#666600">;</span>
        <span style="color:#000088">self</span><span style="color:#666600">.</span><span style="color:#000000">admin </span><span style="color:#666600">=</span> <span style="color:#000088">false</span><span style="color:#666600">;</span>
    <span style="color:#666600">});</span>
<span style="color:#666600">}</span></code></span></span>

然后在HTML中我们可以有条件地显示一个新链接:

appponent.html

<span style="color:#34302d"><span style="color:#333333"><code class="language-html"><span style="color:#000088"><div</span> <span style="color:#660066">class</span><span style="color:#666600">=</span><span style="color:#008800">"container"</span><span style="color:#000000"> [</span><span style="color:#660066">hidden</span><span style="color:#000000">]</span><span style="color:#666600">=</span><span style="color:#008800">"!authenticated"</span><span style="color:#000088">></span>
	<span style="color:#000088"><a</span> <span style="color:#660066">class</span><span style="color:#666600">=</span><span style="color:#008800">"btn btn-primary"</span> <span style="color:#660066">href</span><span style="color:#666600">=</span><span style="color:#008800">"/ui/"</span><span style="color:#000088">></span><span style="color:#000000">Go To User Interface</span><span style="color:#000088"></a></span>
<span style="color:#000088"></div></span>
<span style="color:#000088"><br</span> <span style="color:#000088">/></span>
<span style="color:#000088"><div</span> <span style="color:#660066">class</span><span style="color:#666600">=</span><span style="color:#008800">"container"</span><span style="color:#000000"> [</span><span style="color:#660066">hidden</span><span style="color:#000000">]</span><span style="color:#666600">=</span><span style="color:#008800">"!authenticated || !admin"</span><span style="color:#000088">></span>
	<span style="color:#000088"><a</span> <span style="color:#660066">class</span><span style="color:#666600">=</span><span style="color:#008800">"btn btn-primary"</span> <span style="color:#660066">href</span><span style="color:#666600">=</span><span style="color:#008800">"/admin/"</span><span style="color:#000088">></span><span style="color:#000000">Go To Admin Interface</span><span style="color:#000088"></a></span>
<span style="color:#000088"></div></span></code></span></span>

运行所有应用程序并转到http:// localhost:8080以查看结果。一切都应该正常工作,UI应该根据当前认证的用户而改变。

我们为什么在这里?

现在我们有一个很好的小系统,有2个独立的用户界面和一个后端资源服务器,所有这些都受到网关中相同的身份验证的保护。Gateway充当微代理的事实使得后端安全问题的实现非常简单,并且他们可以专注于自己的业务问题。Spring Session的使用(再次)避免了大量的麻烦和潜在的错误。

一个强大的功能是后端可以独立进行任何类型的身份验证(例如,如果您知道其物理地址和一组本地凭据,则可以直接进入UI)。网关强加了一组完全不相关的约束,只要它可以对用户进行身份验证并为其提供满足后端访问规则的元数据。这是一个出色的设计,能够独立开发和测试后端组件。如果我们想要,我们可以返回到外部OAuth2服务器(如第V部分,甚至是完全不同的东西),以便在网关上进行身份验证,并且不需要触及后端。

此体系结构的一个额外功能(单个网关控制身份验证,以及跨所有组件的共享会话令牌)是“单一注销”,我们认为在第V节中难以实现的功能是免费提供的。更准确地说,单个注销的用户体验的一种特定方法在我们完成的系统中自动可用:如果用户退出任何UI(网关,UI后端或管理员后端),他将退出所有其他人,假设每个单独的UI以相同的方式实现“注销”功能(使会话无效)。

谢谢:我再次感谢所有帮助我开发此系列的人,特别是Rob Winch和ThorstenSpäth,他们仔细审查了部分和源代码。自第一部分出版以来,它没有太大变化,但所有其他部分都是为了回应读者的评论和见解而发展而来,所以非常感谢阅读这些部分并麻烦参与讨论的人。

(七)测试Angular应用程序

在本节中,我们将继续讨论如何在“单页面应用程序”中使用带有Angular的Spring Security。这里我们展示如何使用Angular测试框架编写和运行客户端代码的单元测试。您可以通过阅读第一部分来了解应用程序的基本构建块或从头开始构建它,或者您可以直接访问Github中的源代码(与第一部分相同的源代码,但现在添加了测试)。这部分使用Spring或Spring Security实际上只有很少的代码,但是它以一种在通常的Angular社区资源中可能不那么容易找到的方式涵盖了客户端测试,并且我们认为这对大多数人来说都很舒服Spring用户

提醒:如果您正在使用示例应用程序完成此部分,请务必清除Cookie和HTTP Basic凭据的浏览器缓存。在Chrome中,为单个服务器执行此操作的最佳方法是打开新的隐身窗口。

编写规范

我们在“基本”应用程序中的“app”组件非常简单,因此彻底测试它不会花费太多。这里提醒代码:

appponent.ts

<span style="color:#34302d"><span style="color:#333333"><code class="language-javascript"><span style="color:#000000">include</span><span style="color:#666600">::</span><span style="color:#000000">basic</span><span style="color:#666600">/</span><span style="color:#000000">src</span><span style="color:#666600">/</span><span style="color:#000000">app</span><span style="color:#666600">/</span><span style="color:#000000">app</span><span style="color:#666600">.</span><span style="color:#000000">component</span><span style="color:#666600">.</span><span style="color:#000000">ts</span></code></span></span>

我们面临的主要挑战是http在测试中提供对象,因此我们可以对它们在组件中的使用方式进行断言。实际上,即使在我们面临这一挑战之前,我们也需要能够创建一个组件实例,以便我们可以测试加载时会发生什么。这是你如何做到这一点。

在已创建的应用程序中的Angular构建ng new已经有一个规范和一些配置来运行它。生成的规范在“src / app”中,它的开头如下:

appponent.ts

<span style="color:#34302d"><span style="color:#333333"><code class="language-javascript"><span style="color:#000088">import</span> <span style="color:#666600">{</span> <span style="color:#660066">TestBed</span><span style="color:#666600">,</span><span style="color:#000000"> async </span><span style="color:#666600">}</span> <span style="color:#000088">from</span> <span style="color:#008800">'@angular/core/testing'</span><span style="color:#666600">;</span>
<span style="color:#000088">import</span> <span style="color:#666600">{</span> <span style="color:#660066">AppComponent</span> <span style="color:#666600">}</span> <span style="color:#000088">from</span> <span style="color:#008800">'./appponent'</span><span style="color:#666600">;</span><span style="color:#000000">
describe</span><span style="color:#666600">(</span><span style="color:#008800">'AppComponent'</span><span style="color:#666600">,</span> <span style="color:#666600">()</span> <span style="color:#666600">=></span> <span style="color:#666600">{</span><span style="color:#000000">
  beforeEach</span><span style="color:#666600">(</span><span style="color:#000000">async</span><span style="color:#666600">(()</span> <span style="color:#666600">=></span> <span style="color:#666600">{</span>
    <span style="color:#660066">TestBed</span><span style="color:#666600">.</span><span style="color:#000000">configureTestingModule</span><span style="color:#666600">({</span><span style="color:#000000">
      imports</span><span style="color:#666600">:</span> <span style="color:#666600">[],</span><span style="color:#000000">
      declarations</span><span style="color:#666600">:</span> <span style="color:#666600">[</span>
        <span style="color:#660066">AppComponent</span>
      <span style="color:#666600">]</span>
    <span style="color:#666600">}).</span><span style="color:#000000">compileComponents</span><span style="color:#666600">();</span>
  <span style="color:#666600">}));</span><span style="color:#000000">
  it</span><span style="color:#666600">(</span><span style="color:#008800">'should create the app'</span><span style="color:#666600">,</span><span style="color:#000000"> async</span><span style="color:#666600">(()</span> <span style="color:#666600">=></span> <span style="color:#666600">{</span>
    <span style="color:#000088">const</span><span style="color:#000000"> fixture </span><span style="color:#666600">=</span> <span style="color:#660066">TestBed</span><span style="color:#666600">.</span><span style="color:#000000">createComponent</span><span style="color:#666600">(</span><span style="color:#660066">AppComponent</span><span style="color:#666600">);</span>
    <span style="color:#000088">const</span><span style="color:#000000"> app </span><span style="color:#666600">=</span><span style="color:#000000"> fixture</span><span style="color:#666600">.</span><span style="color:#000000">debugElement</span><span style="color:#666600">.</span><span style="color:#000000">componentInstance</span><span style="color:#666600">;</span><span style="color:#000000">
    expect</span><span style="color:#666600">(</span><span style="color:#000000">app</span><span style="color:#666600">).</span><span style="color:#000000">toBeTruthy</span><span style="color:#666600">();</span>
  <span style="color:#666600">}));</span>
  <span style="color:#666600">...</span>
<span style="color:#666600">}</span></code></span></span>

在这个非常基本的测试套件中,我们有以下重要元素:

  1. 我们describe()正在测试的东西(在这种情况下是“AppComponent”)带有一个函数。

  2. 在该函数内部,我们提供了一个beforeEach()回调函数,用于加载Angular组件。

  3. 行为通过调用来表达it(),其中我们用语言表达期望是什么,然后提供一个产生断言的函数。

  4. 在发生任何其他事件之前初始化测试环境。这是大多数Angular应用程序的样板。

这里的测试函数是如此微不足道,它实际上只断言组件存在,所以如果失败那么测试将失败。

改进单元测试:模拟HTTP后端

为了将规格提高到生产等级,我们需要实际断言控制器加载时会发生什么。因为它调用http.get()我们需要模拟该调用以避免仅为单元测试运行整个应用程序。为此,我们使用Angular HttpClientTestingModule

appponent.spec

<span style="color:#34302d"><span style="color:#333333"><code class="language-javascript"><span style="color:#660066">Unresolved</span><span style="color:#000000"> directive </span><span style="color:#000088">in</span><span style="color:#000000"> testing</span><span style="color:#666600">.</span><span style="color:#000000">adoc </span><span style="color:#666600">-</span><span style="color:#000000"> include</span><span style="color:#666600">::</span><span style="color:#000000">basic</span><span style="color:#666600">/</span><span style="color:#000000">src</span><span style="color:#666600">/</span><span style="color:#000000">app</span><span style="color:#666600">/</span><span style="color:#000000">app</span><span style="color:#666600">.</span><span style="color:#000000">component</span><span style="color:#666600">.</span><span style="color:#000000">spec</span><span style="color:#666600">[</span><span style="color:#000000">indent</span><span style="color:#666600">=</span><span style="color:#006666">0</span><span style="color:#666600">]</span></code></span></span>

这里的新作品是:

  • 的声明HttpClientTestingModule作为一个进口TestBedbeforeEach()

  • 在测试函数中,我们在创建组件之前设置对后端的期望,告诉它期望调用'resource /',以及响应应该是什么。

运行规范

要运行我们的测试“代码,我们可以使用在项目设置时创建的便利脚本来执行./ng test(或./ng build)。它也作为Maven生命周期的一部分运行,因此./mvnw install也是运行测试的好方法,这就是在您的CI构建中发生。

端到端测试

Angular还使用浏览器和生成的JavaScript为“端到端测试”设置了标准构建。这些在顶级e2e目录中写为“specs” 。本教程中的所有示例都包含一个在Maven生命周期中运行的非常简单的端到端测试(因此,如果您mvn install在任何“ui”应用程序中运行,您将看到一个浏览器窗口弹出窗口)。

结论

能够在现代Web应用程序中运行Javascript的单元测试很重要,这是我们在本系列中一直忽略(或躲避)的主题。在这一部分中,我们介绍了如何编写测试的基本要素,如何在开发时运行它们,以及重要的是,在持续集成设置中。我们采取的方法并不适合每个人,所以请不要以不同的方式做到这一点,但要确保你拥有所有这些成分。我们在这里做的方式可能会让传统的Java企业开发人员感到舒服,并且与他们现有的工具和流程很好地集成,所以如果你属于那个类别,我希望你会发现它有用作为一个起点。更多使用Angular和Jasmine进行测试的例子可以在互联网上的很多地方找到,这个系列中的“单个”示例,现在有一些最新的测试代码,这比我们在本教程中为“基本”示例编写的代码要简单一些。

(八)从OAuth2客户端应用程序注销

在本节中,我们将继续讨论如何将Spring Security与Angular一起使用在“单页应用程序”中。在这里,我们将展示如何获取OAuth2示例并添加不同的注销体验。许多实施OAuth2单点登录的人发现他们有一个难题来解决如何“干净地”注销?这是一个难题的原因是没有一种正确的方法可以做到这一点,您选择的解决方案将取决于您正在寻找的用户体验,以及您愿意承担的复杂程度。复杂性的原因源于这样一个事实,即系统中可能存在多个浏览器会话,所有浏览器会话都有不同的后端服务器,因此当用户从其中一个退出时,其他人会发生什么?这是教程的第9部分,您可以了解应用程序的基本构建块,或者通过阅读第一部分,或者您可以直接访问Github中的源代码。

注销模式

oauth2在本教程中注销示例的用户体验是您注销UI应用程序,而不是从authserver注销,因此当您重新登录UI应用程序时,authenticserver不会再次质询凭据。当authenticserver是外部的时,这是完全可取的,正常的和可取的 - 谷歌和其他外部authserver提供者既不想要也不允许你从不受信任的应用程序从他们的服务器注销 - 但如果authserver真的是真的,它不是最好的用户体验与UI相同的系统的一部分。

从广义上讲,有三种模式可以从经过身份验证的OAuth2客户端的UI应用程序注销:

  1. 外部Authserver(EA,原始样本)。用户将authserver视为第三方(例如,使用Facebook或Google进行身份验证)。应用会话结束时,您不希望退出authserver。您确实需要批准所有拨款。本教程中的oauth2(和oauth2-vanilla)示例实现了此模式。

  2. 网关和内部Authserver(GIA)。您只需要注销2个应用程序,它们就是用户所感知的同一系统的一部分。通常您希望自动批准所有拨款。

  3. 单点注销(SL)。一个authserver和多个UI应用程序都有自己的身份验证,当用户注销一个时,您希望他们都遵循。由于网络分区和服务器故障,可能会因天真的实现而失败 - 您基本上需要全局一致的存储。

有时,即使您有外部authserver,也希望控制身份验证并添加内部访问控制层(例如,authserver不支持的范围或角色)。然后使用EA进行身份验证是一个好主意,但是有一个内部authserver可以将所需的其他详细信息添加到令牌中。在auth-server此另一个样本的OAuth2教程告诉您如何做,在一个非常简单的方法。然后,您可以将GIA或SL模式应用于包含内部authserver的系统。

如果您不想要EA,可以选择以下选项:

  • 从authserver注册以及浏览器客户端中的UI应用程序。简单的方法,并与一些仔细的CRSF和CORS配置一起使用。没有SL。

  • 只要令牌可用,就从authserver注销。难以在获取令牌的UI中实现,因为您没有authserver的会话cookie。Spring OAuth中有一个功能请求,它显示了一种有趣的方法:一旦生成auth代码,就会使authserver中的会话无效。Github问题包含一个实现会话失效的方面,但它更容易做到HandlerInterceptor。没有SL。

  • 代理authserver通过与UI相同的网关,并希望一个cookie足以管理整个系统的状态。不起作用,因为除非存在共享会话,否则会在某种程度上使对象失败(否则authserver没有会话存储)。SL仅在所有应用程序之间共享会话时。

  • 网关中的Cookie中继。您正在使用网关作为身份验证的真实来源,并且authserver具有所需的所有状态,因为网关管理cookie而不是浏览器。浏览器永远不会有来自多个服务器的cookie。没有SL。

  • 将令牌用作全局身份验证,并在用户注销UI应用程序时使其无效。下行:要求令牌被客户端应用程序无效,这实际上并不是他们的目的。SL可能,但通常的限制适用。

  • 在authserver中创建和管理全局会话令牌(除用户令牌之外)。这是OpenId Connect采用的方法,它确实为SL提供了一些选项,代价是一些额外的机器。所有选项都不受通常的分布式系统限制的影响:如果网络和应用程序节点不稳定,则无法保证在需要时在所有参与者之间共享注销信号。所有注销规范仍处于草稿形式,以下是规范的一些链接:会话管理,前台通道注销和后台通道注销。

请注意,在SL很难或不可能的情况下,最好将所有UI放在单个网关之后。然后你可以使用更简单的GIA来控制你整个庄园的注销。

最简单的两个选项,可以很好地应用于GIA模式,可以在教程示例中实现如下(oauth2从那里获取示例和工作)。

从浏览器注销两个服务器

一旦UI应用程序注销,就可以很容易地向浏览器客户端添加几行代码,以便从authserver注销。例如

<span style="color:#34302d"><span style="color:#333333"><code class="language-javascript"><span style="color:#000000">logout</span><span style="color:#666600">()</span> <span style="color:#666600">{</span>
    <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">http</span><span style="color:#666600">.</span><span style="color:#000000">post</span><span style="color:#666600">(</span><span style="color:#008800">'logout'</span><span style="color:#666600">,</span> <span style="color:#666600">{}).</span><span style="color:#000088">finally</span><span style="color:#666600">(()</span> <span style="color:#666600">=></span> <span style="color:#666600">{</span>
        <span style="color:#000088">self</span><span style="color:#666600">.</span><span style="color:#000000">authenticated </span><span style="color:#666600">=</span> <span style="color:#000088">false</span><span style="color:#666600">;</span>
        <span style="color:#000088">this</span><span style="color:#666600">.</span><span style="color:#000000">http</span><span style="color:#666600">.</span><span style="color:#000000">post</span><span style="color:#666600">(</span><span style="color:#008800">'http://localhost:9999/uaa/logout'</span><span style="color:#666600">,</span> <span style="color:#666600">{},</span> <span style="color:#666600">{</span><span style="color:#000000">withCredentials</span><span style="color:#666600">:</span><span style="color:#000088">true</span><span style="color:#666600">})</span>
            <span style="color:#666600">.</span><span style="color:#000000">subscribe</span><span style="color:#666600">(()</span> <span style="color:#666600">=></span> <span style="color:#666600">{</span><span style="color:#000000">
                console</span><span style="color:#666600">.</span><span style="color:#000000">log</span><span style="color:#666600">(</span><span style="color:#008800">'Logged out'</span><span style="color:#666600">);</span>
        <span style="color:#666600">});</span>
    <span style="color:#666600">}).</span><span style="color:#000000">subscribe</span><span style="color:#666600">();</span>
<span style="color:#666600">};</span></code></span></span>

在此示例中,我们将authserver注销端点URL硬编码到JavaScript中,但如果需要,可以很容易地将其外部化。它必须是一个直接发送到authserver的POST,因为我们也想要会话cookie。如果我们特别要求,XHR请求将仅通过附带cookie的浏览器发出withCredentials:true

相反,在服务器上我们需要一些CORS配置,因为请求来自不同的域。例如在WebSecurityConfigurerAdapter

<span style="color:#34302d"><span style="color:#333333"><code class="language-java"><span style="color:#006666">@Override</span>
<span style="color:#000088">protected</span> <span style="color:#000088">void</span><span style="color:#000000"> configure</span><span style="color:#666600">(</span><span style="color:#660066">HttpSecurity</span><span style="color:#000000"> http</span><span style="color:#666600">)</span> <span style="color:#000088">throws</span> <span style="color:#660066">Exception</span> <span style="color:#666600">{</span><span style="color:#000000">
  http
	</span><span style="color:#666600">.</span><span style="color:#000000">requestMatchers</span><span style="color:#666600">().</span><span style="color:#000000">antMatchers</span><span style="color:#666600">(</span><span style="color:#008800">"/login"</span><span style="color:#666600">,</span> <span style="color:#008800">"/logout"</span><span style="color:#666600">,</span> <span style="color:#008800">"/oauth/authorize"</span><span style="color:#666600">,</span> <span style="color:#008800">"/oauth/confirm_access"</span><span style="color:#666600">)</span>
  <span style="color:#666600">.</span><span style="color:#000088">and</span><span style="color:#666600">()</span>
    <span style="color:#666600">.</span><span style="color:#000000">cors</span><span style="color:#666600">().</span><span style="color:#000000">configurationSource</span><span style="color:#666600">(</span><span style="color:#000000">configurationSource</span><span style="color:#666600">())</span>
    <span style="color:#666600">...</span>
<span style="color:#666600">}</span>

<span style="color:#000088">private</span> <span style="color:#660066">CorsConfigurationSource</span><span style="color:#000000"> configurationSource</span><span style="color:#666600">()</span> <span style="color:#666600">{</span>
  <span style="color:#660066">UrlBasedCorsConfigurationSource</span><span style="color:#000000"> source </span><span style="color:#666600">=</span> <span style="color:#000088">new</span> <span style="color:#660066">UrlBasedCorsConfigurationSource</span><span style="color:#666600">();</span>
  <span style="color:#660066">CorsConfiguration</span><span style="color:#000000"> config </span><span style="color:#666600">=</span> <span style="color:#000088">new</span> <span style="color:#660066">CorsConfiguration</span><span style="color:#666600">();</span><span style="color:#000000">
  config</span><span style="color:#666600">.</span><span style="color:#000000">addAllowedOrigin</span><span style="color:#666600">(</span><span style="color:#008800">"*"</span><span style="color:#666600">);</span><span style="color:#000000">
  config</span><span style="color:#666600">.</span><span style="color:#000000">setAllowCredentials</span><span style="color:#666600">(</span><span style="color:#000088">true</span><span style="color:#666600">);</span><span style="color:#000000">
  config</span><span style="color:#666600">.</span><span style="color:#000000">addAllowedHeader</span><span style="color:#666600">(</span><span style="color:#008800">"X-Requested-With"</span><span style="color:#666600">);</span><span style="color:#000000">
  config</span><span style="color:#666600">.</span><span style="color:#000000">addAllowedHeader</span><span style="color:#666600">(</span><span style="color:#008800">"Content-Type"</span><span style="color:#666600">);</span><span style="color:#000000">
  config</span><span style="color:#666600">.</span><span style="color:#000000">addAllowedMethod</span><span style="color:#666600">(</span><span style="color:#660066">HttpMethod</span><span style="color:#666600">.</span><span style="color:#000000">POST</span><span style="color:#666600">);</span><span style="color:#000000">
  source</span><span style="color:#666600">.</span><span style="color:#000000">registerCorsConfiguration</span><span style="color:#666600">(</span><span style="color:#008800">"/logout"</span><span style="color:#666600">,</span><span style="color:#000000"> config</span><span style="color:#666600">);</span>
  <span style="color:#000088">return</span><span style="color:#000000"> source</span><span style="color:#666600">;</span>
<span style="color:#666600">}</span></code></span></span>

“/ logout”端点已经过一些特殊处理。允许从任何来源调用它,并明确允许发送凭证(例如cookie)。允许的标题只是Angular在示例应用程序中发送的标题。

除了CORS配置之外,我们还需要为注销端点禁用CSRF,因为Angular不会X-XSRF-TOKEN在跨域请求中发送头。authserver之前不需要任何CSRF配置,但是很容易为logout端点添加一个ignore:

<span style="color:#34302d"><span style="color:#333333"><code class="language-java"><span style="color:#006666">@Override</span>
<span style="color:#000088">protected</span> <span style="color:#000088">void</span><span style="color:#000000"> configure</span><span style="color:#666600">(</span><span style="color:#660066">HttpSecurity</span><span style="color:#000000"> http</span><span style="color:#666600">)</span> <span style="color:#000088">throws</span> <span style="color:#660066">Exception</span> <span style="color:#666600">{</span><span style="color:#000000">
  http
    </span><span style="color:#666600">.</span><span style="color:#000000">csrf</span><span style="color:#666600">()</span>
      <span style="color:#666600">.</span><span style="color:#000000">ignoringAntMatchers</span><span style="color:#666600">(</span><span style="color:#008800">"/logout/**"</span><span style="color:#666600">)</span>
    <span style="color:#666600">...</span>
<span style="color:#666600">}</span></code></span></span>
 删除CSRF保护并不是真的可取,但您可能已准备好对此限制用例容忍它。

通过这两个简单的更改,一个在UI应用程序客户端,一个在authserver中,您会发现一旦您注销UI应用程序,当您重新登录时,将始终提示您输入密码。

另一个有用的更改是将OAuth2客户端设置为自动批准,以便用户不必批准令牌授权。这在内部authserver中很常见,用户不会将其视为单独的系统。在AuthorizationServerConfigurerAdapter初始化客户端时,您只需要一个标志:

<span style="color:#34302d"><span style="color:#333333"><code class="language-java"><span style="color:#006666">@Override</span>
<span style="color:#000088">public</span> <span style="color:#000088">void</span><span style="color:#000000"> configure</span><span style="color:#666600">(</span><span style="color:#660066">ClientDetailsServiceConfigurer</span><span style="color:#000000"> clients</span><span style="color:#666600">)</span> <span style="color:#000088">throws</span> <span style="color:#660066">Exception</span> <span style="color:#666600">{</span><span style="color:#000000">
  clients</span><span style="color:#666600">.</span><span style="color:#000000">inMemory</span><span style="color:#666600">().</span><span style="color:#000000">withClient</span><span style="color:#666600">(</span><span style="color:#008800">"acme"</span><span style="color:#666600">)</span>
    <span style="color:#666600">...</span>
  <span style="color:#666600">.</span><span style="color:#000000">autoApprove</span><span style="color:#666600">(</span><span style="color:#000088">true</span><span style="color:#666600">);</span>
<span style="color:#666600">}</span></code></span></span>

在Authserver中使会话无效

如果您不想放弃注销端点上的CSRF保护,您可以尝试另一种简单的方法,即在授予令牌后立即使authserver中的用户会话无效(实际上只要一个身份验证代码生成)。这也非常容易实现:从oauth2示例开始,只需添加一个HandlerInterceptorOAuth2端点即可。

<span style="color:#34302d"><span style="color:#333333"><code class="language-java"><span style="color:#006666">@Override</span>
<span style="color:#000088">public</span> <span style="color:#000088">void</span><span style="color:#000000"> configure</span><span style="color:#666600">(</span><span style="color:#660066">AuthorizationServerEndpointsConfigurer</span><span style="color:#000000"> endpoints</span><span style="color:#666600">)</span>
    <span style="color:#000088">throws</span> <span style="color:#660066">Exception</span> <span style="color:#666600">{</span>
  <span style="color:#666600">...</span><span style="color:#000000">
  endpoints</span><span style="color:#666600">.</span><span style="color:#000000">addInterceptor</span><span style="color:#666600">(</span><span style="color:#000088">new</span> <span style="color:#660066">HandlerInterceptorAdapter</span><span style="color:#666600">()</span> <span style="color:#666600">{</span>
    <span style="color:#006666">@Override</span>
    <span style="color:#000088">public</span> <span style="color:#000088">void</span><span style="color:#000000"> postHandle</span><span style="color:#666600">(</span><span style="color:#660066">HttpServletRequest</span><span style="color:#000000"> request</span><span style="color:#666600">,</span>
        <span style="color:#660066">HttpServletResponse</span><span style="color:#000000"> response</span><span style="color:#666600">,</span> <span style="color:#660066">Object</span><span style="color:#000000"> handler</span><span style="color:#666600">,</span>
        <span style="color:#660066">ModelAndView</span><span style="color:#000000"> modelAndView</span><span style="color:#666600">)</span> <span style="color:#000088">throws</span> <span style="color:#660066">Exception</span> <span style="color:#666600">{</span>
      <span style="color:#000088">if</span> <span style="color:#666600">(</span><span style="color:#000000">modelAndView </span><span style="color:#666600">!=</span> <span style="color:#000088">null</span>
          <span style="color:#666600">&&</span><span style="color:#000000"> modelAndView</span><span style="color:#666600">.</span><span style="color:#000000">getView</span><span style="color:#666600">()</span> <span style="color:#000088">instanceof</span> <span style="color:#660066">RedirectView</span><span style="color:#666600">)</span> <span style="color:#666600">{</span>
        <span style="color:#660066">RedirectView</span><span style="color:#000000"> redirect </span><span style="color:#666600">=</span> <span style="color:#666600">(</span><span style="color:#660066">RedirectView</span><span style="color:#666600">)</span><span style="color:#000000"> modelAndView</span><span style="color:#666600">.</span><span style="color:#000000">getView</span><span style="color:#666600">();</span>
        <span style="color:#660066">String</span><span style="color:#000000"> url </span><span style="color:#666600">=</span><span style="color:#000000"> redirect</span><span style="color:#666600">.</span><span style="color:#000000">getUrl</span><span style="color:#666600">();</span>
        <span style="color:#000088">if</span> <span style="color:#666600">(</span><span style="color:#000000">url</span><span style="color:#666600">.</span><span style="color:#000000">contains</span><span style="color:#666600">(</span><span style="color:#008800">"code="</span><span style="color:#666600">)</span> <span style="color:#666600">||</span><span style="color:#000000"> url</span><span style="color:#666600">.</span><span style="color:#000000">contains</span><span style="color:#666600">(</span><span style="color:#008800">"error="</span><span style="color:#666600">))</span> <span style="color:#666600">{</span>
          <span style="color:#660066">HttpSession</span><span style="color:#000000"> session </span><span style="color:#666600">=</span><span style="color:#000000"> request</span><span style="color:#666600">.</span><span style="color:#000000">getSession</span><span style="color:#666600">(</span><span style="color:#000088">false</span><span style="color:#666600">);</span>
          <span style="color:#000088">if</span> <span style="color:#666600">(</span><span style="color:#000000">session </span><span style="color:#666600">!=</span> <span style="color:#000088">null</span><span style="color:#666600">)</span> <span style="color:#666600">{</span><span style="color:#000000">
            session</span><span style="color:#666600">.</span><span style="color:#000000">invalidate</span><span style="color:#666600">();</span>
          <span style="color:#666600">}</span>
        <span style="color:#666600">}</span>
      <span style="color:#666600">}</span>
    <span style="color:#666600">}</span>
  <span style="color:#666600">});</span>
<span style="color:#666600">}</span></code></span></span>

此拦截器查找a RedirectView,这是用户被重定向回客户端应用程序的信号,并检查该位置是否包含身份验证代码或错误。如果您使用隐式授权,也可以添加“token =”。

通过这个简单的更改,只要您进行身份验证,authserver中的会话就已经死了,因此无需尝试从客户端进行管理。当您注销UI应用程序,然后重新登录时,authserver无法识别您并提示输入凭据。此模式是本教程oauth2-logout的源代码中的示例实现的模式。这种方法的缺点是你不再真正拥有真正的单一签名 - 任何其他属于你的系统的应用程序都会发现authserver会话已经死亡,他们必须再次提示进行身份验证 - 它不是如果有多个应用,则可以获得出色的用户体验

结论

在本节中,我们已经了解了如何实现从OAuth2客户端应用程序注销的几种不同模式(以第五部分中的应用程序为起点)(教程),并讨论了其他模式的一些选项。这些选项并非详尽无遗,但应该让您对所涉及的权衡有一个很好的了解,以及一些思考最佳解决方案的工具。本节中只有几行JavaScript,并不是特定于Angular(它为XHR请求添加了一个标志),因此所有课程和模式都适用于本指南中示例应用程序的狭窄范围。一个反复出现的主题是所有单点注销(SL)的方法,其中有多个UI应用程序和单个authserver往往以某种方式存在缺陷:您可以做的最好的选择是让您的用户最不舒服的方法。如果您有内部authserver和由许多组件组成的系统,

想要撰写新指南或为现有指南做出贡献?查看我们的贡献指南。

 所有指南均附带代码的ASLv2许可证,以及Attribution,NoDerivatives创作公共许可证。

更多推荐

Spring Security和Angular教程

本文发布于:2023-04-15 23:19:00,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/37803479a4c3857d39baa5ce2ad915a0.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:教程   Spring   Security   Angular

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!

  • 79498文章数
  • 59阅读数
  • 0评论数