Spring安全和角度

时间:2022-12-28 11:00:50

Spring安全和角度

安全的单页应用程序

在本教程中,我们展示了Spring Security,Spring Boot和Angular的一些不错的功能,它们协同工作以提供愉快和安全的用户体验。对于使用Spring和Angular的初学者来说,它应该是可用的,但也有很多细节对任何专家都有用。这实际上是关于Spring Security和Angular的一系列部分中的第一个,每个部分中都依次公开了新功能。我们将在第二和随后的分期付款,但在此之后的主要变化是架构而不是功能。

Spring 和单页应用程序

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

Spring一直是构建后端功能(特别是在企业中)的流行技术,并且随着弹簧启动事情从未如此简单。让我们来看看如何使用Spring Boot,Angular和Twitter Bootstrap从头开始构建一个新的单页应用程序。选择该特定堆栈没有特别的理由,但它非常受欢迎,尤其是在企业 Java 商店中的核心 Spring 选区中,因此它是一个值得的起点。

创建新项目

我们将逐步创建此应用程序,以便任何不完全了解Spring和Angular的人都可以关注正在发生的事情。如果你喜欢切入追逐,你可以跳到最后应用程序在哪里工作,并查看它们如何组合在一起。创建新项目有多种选项:

  • 在命令行上使用 curl
  • 使用 Spring Boot CLI
  • 使用 Spring Initializr 网站
  • 使用弹簧工具套件

我们将要构建的完整项目的源代码位于Github在这里,因此您可以根据需要克隆项目并直接从那里工作。然后跳转到下一节.

使用卷曲

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

$ mkdir ui && cd ui
$ curl https://start.spring.io/starter.tgz -d dependencies=web,security -d name=ui | tar -xzvf -

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

使用 Spring Boot CLI

您可以使用弹簧引导命令行界面喜欢这个:

$ spring init --dependencies web,security ui/ && cd ui

然后跳转到下一节.

使用 Initializr 网站

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

使用弹簧工具套件

在弹簧工具套件(一组 Eclipse 插件)您还可以使用位于 的向导创建和导入项目。然后跳转到​​File->New->Spring Starter Project​​下一节.IntelliJ IDEA和NetBeans具有类似的功能。

添加角度应用

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

运行应用程序

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

$ mvn spring-boot:run

并转到浏览器​​http://localhost:8080​​.加载主页时,您应该会看到一个浏览器对话框,要求输入用户名和密码(用户名为“user”,密码在启动时打印在控制台日志中)。实际上还没有内容(或者可能是 CLI 中默认的“英雄”教程内容),所以你应该得到一个空白页。​​ng​

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

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

要打包并作为独立 JAR 运行,您可以执行以下操作:

$ mvn package
$ java -jar target/*.jar

自定义角度应用程序

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

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

app.component.ts

import { Component } from '@angular/core';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'Demo';
greeting = {'id': 'XXX', 'content': 'Hello World'};
}

此 TypeScript 中的大多数代码都是样板代码。有趣的东西都将在我们定义“选择器”(HTML元素的名称)和通过注释呈现的HTML片段中。我们还需要编辑 HTML 模板(“app.component.html”):​​AppComponent​​​​@Component​

app.component.html

<div style="text-align:center"class="container">
<h1>
Welcome {{title}}!
</h1>
<div class="container">
<p>Id: <span>{{greeting.id}}</span></p>
<p>Message: <span>{{greeting.content}}!</span></p>
</div>
</div>

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

添加动态内容

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

Ui应用程序.java

@SpringBootApplication
@RestController
public class UiApplication {

@RequestMapping("/resource")
public Map<String,Object> home() {
Map<String,Object> model = new HashMap<String,Object>();
model.put("id", UUID.randomUUID().toString());
model.put("content", "Hello World");
return model;
}

public static void main(String[] args) {
SpringApplication.run(UiApplication.class, args);
}

}

根据创建新项目的方式,可能不会将其称为 。​​UiApplication​

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

$ curl localhost:8080/resource
{"timestamp":1420442772928,"status":401,"error":"Unauthorized","message":"Full authentication is required to access this resource","path":"/resource"}

从 Angular 加载动态资源

因此,让我们在浏览器中获取该消息。修改 以使用 XHR 加载受保护的资源:​​AppComponent​

app.component.ts

import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'Demo';
greeting = {};
constructor(private http: HttpClient) {
http.get('resource').subscribe(data => this.greeting = data);
}
}

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

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

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { HttpClientModule } from '@angular/common/http';

@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
HttpClientModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

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

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

它是如何工作的?

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

动词

路径

地位

响应

获取

/

401

浏览器提示进行身份验证

获取

/

200

索引.html

获取

/*。.js

200

从角度加载第三资产

获取

/main.bundle.js

200

应用程序逻辑

获取

/资源

200

JSON问候语

您可能看不到 401,因为浏览器将主页加载视为单个交互,并且您可能会看到 2 个对“/resource”的请求,因为存在科尔斯谈判。

更仔细地查看请求,您会发现所有请求都有一个“授权”标头,如下所示:

Authorization: Basic dXNlcjpwYXNzd29yZA==

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

这是怎么回事?

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

  • 基本身份验证仅限于用户名和密码身份验证。
  • 身份验证 UI 无处不在,但很丑陋(浏览器对话框)。
  • 没有保护跨站点请求伪造(企业社会责任基金)。

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

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

谢谢:我要感谢所有帮助我开发这个系列的人,特别是罗伯·温奇和托斯滕·斯佩思感谢他们对文本和源代码的仔细审查,并教我一些我甚至不知道的技巧,甚至我不知道我最熟悉的部分。

登录页面

在本节中,我们继续我们的讨论如何使用弹簧安全跟角在“单页应用程序”中。在这里,我们展示了如何使用 Angular 通过表单对用户进行身份验证,并获取要在 UI 中呈现的安全资源。这是一系列部分中的第二部分,您可以通过阅读第一部分,或者您可以直接转到Github中的源代码.在第一部分中,我们构建了一个简单的应用程序,该应用程序使用 HTTP 基本身份验证来保护后端资源。在这个中,我们添加一个登录表单,让用户对是否进行身份验证进行一些控制,并修复第一次迭代的问题(主要是缺乏 CSRF 保护)。


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


向主页添加导航

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

app.component.html

<div class="container">
<ul class="nav nav-pills">
<li><a routerLinkActive="active" routerLink="/home">Home</a></li>
<li><a routerLinkActive="active" routerLink="/login">Login</a></li>
<li><a (click)="logout()">Logout</a></li>
</ul>
</div>
<div class="container">
<router-outlet></router-outlet>
</div>

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

选择器由 Angular 提供,需要连接到主模块中的组件。每个路由(每个菜单链接)将有一个组件,以及一个帮助程序服务将它们粘合在一起,并共享一些状态()。下面是将所有部分组合在一起的模块的实现:​​<router-outlet/>​​​​AppService​

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { RouterModule, Routes } from '@angular/router';
import { AppService } from './app.service';
import { HomeComponent } from './home.component';
import { LoginComponent } from './login.component';
import { AppComponent } from './app.component';

const routes: Routes = [
{ path: '', pathMatch: 'full', redirectTo: 'home'},
{ path: 'home', component: HomeComponent},
{ path: 'login', component: LoginComponent}
];

@NgModule({
declarations: [
AppComponent,
HomeComponent,
LoginComponent
],
imports: [
RouterModule.forRoot(routes),
BrowserModule,
HttpClientModule,
FormsModule
],
providers: [AppService]
bootstrap: [AppComponent]
})
export class AppModule { }

我们添加了对一个名为 Angular 模块的依赖“路由器模块”这使我们能够为 .在导入内部使用,以设置指向“/”(“主”控制器)和“/login”(“登录”控制器)的链接。​​router​​​​AppComponent​​​​routes​​​​AppModule​

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

UI 组件都是“声明”,服务胶水是“提供程序”。实际上并没有做太多事情。与应用程序根目录一起使用的 TypeScript 组件在这里:​​AppComponent​

app.component.ts

import { Component } from '@angular/core';
import { AppService } from './app.service';
import { HttpClient } from '@angular/common/http';
import { Router } from '@angular/router';
import 'rxjs/add/operator/finally';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
constructor(private app: AppService, private http: HttpClient, private router: Router) {
this.app.authenticate(undefined, undefined);
}
logout() {
this.http.post('logout', {}).finally(() => {
this.app.authenticated = false;
this.router.navigateByUrl('/login');
}).subscribe();
}

}

显著特点:

  • 还有一些依赖注入,这次是AppService
  • 有一个注销函数作为组件的属性公开,我们稍后可以使用它来向后端发送注销请求。它在服务中设置一个标志,并将用户发送回登录屏幕(它通过回调无条件地执行此操作)。appfinally()
  • 我们正在使用将模板 HTML 外部化为一个单独的文件。templateUrl
  • 当加载控制器以查看用户是否实际上已经过身份验证(例如,他是否在会话中刷新了浏览器)时,将调用该函数。我们需要函数进行远程调用,因为实际的身份验证是由服务器完成的,我们不想信任浏览器来跟踪它。authenticate()authenticate()

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

app.service.ts

import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';

@Injectable()
export class AppService {

authenticated = false;

constructor(private http: HttpClient) {
}

authenticate(credentials, callback) {

const headers = new HttpHeaders(credentials ? {
authorization : 'Basic ' + btoa(credentials.username + ':' + credentials.password)
} : {});

this.http.get('user', {headers: headers}).subscribe(response => {
if (response['name']) {
this.authenticated = true;
} else {
this.authenticated = false;
}
return callback && callback();
});

}

}

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

问候语

旧主页中的问候语内容可以放在“src/app”中的“app.component.html”旁边:

首页.组件.html

<h1>Greeting</h1>
<div [hidden]="!authenticated()">
<p>The ID is {{greeting.id}}</p>
<p>The content is {{greeting.content}}</p>
</div>
<div [hidden]="authenticated()">
<p>Login to see your greeting</p>
</div>

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

必须获取问候语,并提供实用程序函数,将标志从 :​​HomeComponent​​​​authenticated()​​​​AppService​

Home.component.ts

import { Component, OnInit } from '@angular/core';
import { AppService } from './app.service';
import { HttpClient } from '@angular/common/http';

@Component({
templateUrl: './home.component.html'
})
export class HomeComponent {

title = 'Demo';
greeting = {};

constructor(private app: AppService, private http: HttpClient) {
http.get('resource').subscribe(data => this.greeting = data);
}

authenticated() { return this.app.authenticated; }

}

登录表格

登录表单也获得自己的组件:

login.component.html

<div class="alert alert-danger" [hidden]="!error">
There was a problem logging in. Please try again.
</div>
<form role="form" (submit)="login()">
<div class="form-group">
<label for="username">Username:</label> <input type="text"
class="form-control" name="username" [(ngModel)]="credentials.username"/>
</div>
<div class="form-group">
<label for="password">Password:</label> <input type="password"
class="form-control" name="password" [(ngModel)]="credentials.password"/>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>

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

身份验证过程

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

提交登录表单

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

login.component.ts

import { Component, OnInit } from '@angular/core';
import { AppService } from './app.service';
import { HttpClient } from '@angular/common/http';
import { Router } from '@angular/router';

@Component({
templateUrl: './login.component.html'
})
export class LoginComponent {

credentials = {username: '', password: ''};

constructor(private app: AppService, private http: HttpClient, private router: Router) {
}

login() {
this.app.authenticate(this.credentials, () => {
this.router.navigateByUrl('/');
});
return false;
}

}

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

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

当前经过身份验证的用户

为了维护函数,我们需要向后端添加新的端点:​​authenticate()​

Ui应用程序.java

@SpringBootApplication
@RestController
public class UiApplication {

@RequestMapping("/user")
public Principal user(Principal user) {
return user;
}

...

}

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

处理服务器上的登录请求

Spring 安全性使处理登录请求变得容易。我们只需要在我们的配置中添加一些配置主要应用类(例如,作为内部类):

Ui应用程序.java

@SpringBootApplication
@RestController
public class UiApplication {

...

@Configuration
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
protected static class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic()
.and()
.authorizeRequests()
.antMatchers("/index.html", "/", "/home", "/login").permitAll()
.anyRequest().authenticated();
}
}

}

这是一个标准的 Spring Boot 应用程序,具有 Spring 安全性自定义,只允许匿名访问静态 (HTML) 资源。HTML 资源需要可供匿名用户使用,而不仅仅是被 Spring Security 忽略,原因将变得很清楚。

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

应用程序.yml

security:
ignored:
- "*.bundle.*"

添加默认 HTTP 请求标头

如果此时运行应用程序,您会发现浏览器弹出基本身份验证对话框(用于用户和密码)。它这样做是因为它看到来自 XHR 请求的 401 响应,并带有“WWW-Authenticate”标头。抑制此弹出窗口的方法是抑制来自 Spring 安全性的标头。抑制 reponse 标头的方法是发送一个特殊的、传统的请求标头“X-Request-With=XMLHttpRequest”。它曾经是 Angular 中的默认值,但它们​​/user​​​​/resource​​在 1.3.0 中取出.因此,以下是在 Angular XHR 请求中设置默认标头的方法。

首先扩展 Angular HTTP 模块提供的默认值:​​RequestOptions​

app.module.ts

@Injectable()
export class XhrInterceptor implements HttpInterceptor {

intercept(req: HttpRequest<any>, next: HttpHandler) {
const xhr = req.clone({
headers: req.headers.set('X-Requested-With', 'XMLHttpRequest')
});
return next.handle(xhr);
}
}

这里的语法是样板。的属性是它的基类,除了构造函数之外,我们真正需要做的就是覆盖始终由 Angular 调用的函数,该函数可用于添加额外的标头。​​implements​​​​Class​​​​intercept()​

要安装这个新工厂,我们需要在 :​​RequestOptions​​​​providers​​​​AppModule​

app.module.ts

@NgModule({
...
providers: [AppService, { provide: HTTP_INTERCEPTORS, useClass: XhrInterceptor, multi: true }],
...
})
export class AppModule { }

注销

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

企业社会责任保护

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

POST /logout HTTP/1.1
...
Content-Type: application/x-www-form-urlencoded

username=user&password=password

HTTP/1.1 403 Forbidden
Set-Cookie: JSESSIONID=3941352C51ABB941781E1DF312DA474E; Path=/; HttpOnly
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
...

{"timestamp":1420467113764,"status":403,"error":"Forbidden","message":"Expected CSRF token not found. Has your session expired?","path":"/login"}

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

因此,在服务器上,我们需要一个自定义过滤器来发送cookie。Angular 希望 cookie 名称为“XSRF-TOKEN”,Spring Security 默认将其作为请求属性提供,因此我们只需要将值从请求属性转移到 cookie。幸运的是,Spring 安全性(从 4.1.0 开始)提供了一个特殊功能,正是这样做的:​​CsrfTokenRepository​

Ui应用程序.java

@Configuration
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
protected static class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
...
.and().csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
}

有了这些更改,我们就不需要在客户端做任何事情,登录表单现在可以工作了。

它是如何工作的?

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

动词

路径

地位

响应

获取

/

200

索引.html

获取

/*。.js

200

来自角度的资产

获取

/用户

401

未授权(忽略)

获取

/家

200

主页

获取

/用户

401

未授权(忽略)

获取

/资源

401

未授权(忽略)

获取

/用户

200

发送凭据并获取 JSON

获取

/资源

200

JSON问候语

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

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

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

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

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

“可是...“ 您将回答,”我现在如何水平扩展我的应用程序?这是您上面提出的“真实”问题,但它往往会缩短为“会话状态不好,我必须是无状态的”。不要惊慌。这里要考虑的要点是安全性有状态的。不能拥有安全的无状态应用程序。那么,您要在哪里存储状态?仅此而已。罗伯·温奇在2014年春季交易所解释状态的必要性(以及它的普遍性 - TCP 和 SSL 是有状态的,所以无论你是否知道,你的系统都是有状态的),如果你想更深入地研究这个主题,这可能值得一看。

好消息是你有一个选择。最简单的选择是将会话数据存储在内存中,并依靠负载均衡器中的粘性会话将来自同一会话的请求路由回同一 JVM(它们都以某种方式支持这一点)。这足以让你起步,并且适用于大量的用例。另一种选择是在应用程序的实例之间共享会话数据。只要您严格并且只存储安全数据,它就很小并且很少更改(仅在用户登录和注销时,或者他们的会话超时时),因此应该不会有任何重大的基础结构问题。这也很容易做到春季会议.我们将在本系列的下一节中使用 Spring Session,因此无需在此处详细介绍如何设置它,但它实际上只需几行代码和一个 Redis 服务器,速度非常快。

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

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

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

结论

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

资源服务器

在本节中,我们继续我们的讨论如何使用弹簧安全跟角在“单页应用程序”中。在这里,我们首先将用作应用程序中动态内容的“问候”资源分解到单独的服务器中,首先作为未受保护的资源,然后由不透明令牌保护。这是一系列部分中的第三部分,您可以通过阅读第一部分,或者你可以直接转到 Github 中的源代码,它分为两部分:一部分是资源不受保护,以及它所在的一个受令牌保护.

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

单独的资源服务器

客户端更改

在客户端,将资源移动到其他后端没有太多工作要做。这是最后一节:

Home.component.ts

@Component({
templateUrl: './home.component.html'
})
export class HomeComponent {

title = 'Demo';
greeting = {};

constructor(private app: AppService, private http: HttpClient) {
http.get('resource').subscribe(data => this.greeting = data);
}

authenticated() { return this.app.authenticated; }

}

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

Home.component.ts

http.get('http://localhost:9000').subscribe(data => this.greeting = data);

服务器端更改

这用户界面服务器更改很简单:我们只需要删除问候语资源(它是“/resource”)。然后我们需要创建一个新的资源服务器,我们可以像在​​@RequestMapping​​第一部分使用Spring Boot Initializr.例如,在类似UN*X的系统上使用curl:

$ mkdir resource && cd resource
$ curl https://start.spring.io/starter.tgz -d dependencies=web -d name=resource | tar -xzvf -

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

只需添加一个​​@RequestMapping​​主要应用类,从旧用户界面:

资源应用.java

@SpringBootApplication
@RestController
class ResourceApplication {

@RequestMapping("/")
public Message home() {
return new Message("Hello World");
}

public static void main(String[] args) {
SpringApplication.run(ResourceApplication.class, args);
}

}

class Message {
private String id = UUID.randomUUID().toString();
private String content;
public Message(String content) {
this.content = content;
}
// ... getters and setters and default constructor
}

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

$ mvn spring-boot:run -Dserver.port=9000

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

应用程序属性

server.port: 9000

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

CORS谈判

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

为了解决这个问题,我们需要支持 CORS 协议,该协议涉及“预飞行”OPTIONS 请求和一些标头来列出调用方允许的行为。春天 4.2 有一些不错的细粒度的 CORS 支持,因此我们可以在控制器映射中添加注释,例如:

资源应用.java

@RequestMapping("/")
@CrossOrigin(origins="*", maxAge=3600)
public Message home() {
return new Message("Hello World");
}

轻率使用既快速又脏,并且可以工作,但它并不不安全,不以任何方式推荐。​​origins=*​

保护资源服务器

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

添加弹簧安全性

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

绒球.xml

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
...
</dependencies>

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

$ curl -v localhost:9000
< HTTP/1.1 302 Found
< Location: http://localhost:9000/login
...

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

$ curl -v -H "Accept: application/json" \
-H "X-Requested-With: XMLHttpRequest" localhost:9000
< HTTP/1.1 401 Unauthorized
...

因此,我们需要做的就是教客户端在每个请求中发送凭据。

令牌身份验证

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

记住来自第二部分默认情况下,Spring 安全性使用来存储身份验证数据的该系列。不过,它不直接与会话交互:有一个抽象层(​​HttpSession​​SecurityContextRepository) 之间可用于更改存储后端。如果我们可以将资源服务器中的该存储库指向具有由 UI 验证的身份验证的存储,那么我们就有办法在两个服务器之间共享身份验证。UI 服务器已经有这样的存储 (的 ),因此,如果我们可以分发该存储并将其开放给资源服务器,我们就有了大部分解决方案。​​HttpSession​

春季会议

解决方案的这一部分非常简单春季会议.我们所需要的只是一个共享数据存储(开箱即用地支持 Redis 和 JDBC),并在服务器中进行几行配置以设置 .​​Filter​

在 UI 应用程序中,我们需要向聚 甲醛:

绒球.xml

<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

Spring 引导和 Spring 会话协同工作以连接到 Redis 并集中存储会话数据。

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

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

从 UI 发送自定义令牌

唯一缺少的部分是存储中数据密钥的传输机制。密钥是 ID,因此如果我们可以在 UI 客户端中获取该密钥,则可以将其作为自定义标头发送到资源服务器。因此,“主”控制器需要更改,以便它将标头作为问候资源的 HTTP 请求的一部分发送。例如:​​HttpSession​

Home.component.ts

constructor(private app: AppService, private http: HttpClient) {
http.get('token').subscribe(data => {
const token = data['token'];
http.get('http://localhost:9000', {headers : new HttpHeaders().set('X-Auth-Token', token)})
.subscribe(response => this.greeting = response);
}, () => {});
}

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

而不是直接去”​​http://localhost:9000​​“ 我们已将该调用包装在对 UI 服务器上新自定义终结点的调用的成功回调中,位于”/token”。它的实现是微不足道的:

Ui应用程序.java

@SpringBootApplication
@RestController
public class UiApplication {

public static void main(String[] args) {
SpringApplication.run(UiApplication.class, args);
}

...

@RequestMapping("/token")
public Map<String,String> token(HttpSession session) {
return Collections.singletonMap("token", session.getId());
}

}

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

资源服务器中的身份验证

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

资源应用.java

@RequestMapping("/")
@CrossOrigin(origins = "*", maxAge = 3600,
allowedHeaders={"x-auth-token", "x-requested-with", "x-xsrf-token"})
public Message home() {
return new Message("Hello World");
}

来自浏览器的预检现在将由Spring MVC处理,但我们需要告诉Spring Security允许它通过:

资源应用.java

public class ResourceApplication extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().authorizeRequests()
.anyRequest().authenticated();
}

...

无需访问所有资源,并且可能存在无意中发送敏感数据的处理程序,因为它不知道请求是预报的。配置实用程序通过处理过滤器层中的所有预检请求来缓解此问题。​​permitAll()​​​​cors()​

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

资源应用.java

@SpringBootApplication
@RestController
class ResourceApplication {

...

@Bean
HeaderHttpSessionStrategy sessionStrategy() {
return new HeaderHttpSessionStrategy();
}

}

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

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

应用程序.yml

security:
sessions: NEVER

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

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

为什么不能全部使用 Cookie?

我们不得不使用自定义标头并在客户端中编写代码来填充标头,这并不复杂,但它似乎与第二部分尽可能使用 Cookie 和会话。那里的论点是,不这样做会带来额外的不必要的复杂性,而且可以肯定的是,我们现在拥有的实现是迄今为止我们看到的最复杂的:解决方案的技术部分远远超过业务逻辑(诚然,业务逻辑很小)。这绝对是一个公平的批评(我们计划在本系列的下一节中解决),但让我们简要地看看为什么它不像只使用 cookie 和会话那么简单。

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

TL;DR UI 和资源服务器没有共同的来源,因此它们无法共享 cookie(即使我们可以使用 Spring 会话强制它们共享会话)。

结论

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

我们在这里使用 Spring 会话在逻辑上不是同一应用程序的 2 台服务器之间共享会话。这是一个巧妙的技巧,“常规”JEE分布式会话是不可能的。

接口网关

在本节中,我们继续我们的讨论如何使用弹簧安全跟角在“单页应用程序”中。在这里,我们将展示如何构建 API 网关来控制身份验证和对后端资源的访问春云.这是一系列部分中的第四部分,您可以通过阅读第一部分,或者您可以直接转到Github中的源代码.在最后一节我们构建了一个简单的分布式应用程序,它使用春季会议对后端资源进行身份验证。在这个中,我们将 UI 服务器变成后端资源服务器的反向代理,修复了最后一个实现的问题(自定义令牌身份验证引入的技术复杂性),并为我们提供了许多用于控制浏览器客户端访问的新选项。


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


创建 API 网关

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

如果您按照代码进行操作,您将知道应用程序实现在最后一节有点复杂,所以它不是一个迭代的好地方。但是,我们可以更轻松地从中间点开始,即后端资源尚未使用Spring Security进行保护。它的源代码是一个单独的项目在 Github 中因此,我们将从那里开始。它有一个UI服务器和一个资源服务器,它们正在相互通信。资源服务器还没有 Spring 安全性,因此我们可以先让系统工作,然后再添加该层。

一行中的声明性反向代理

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

Ui应用程序.java

@SpringBootApplication
@RestController
@EnableZuulProxy
public class UiApplication {
...
}

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

应用程序.yml

security:
...
zuul:
routes:
resource:
path: /resource/**
url: http://localhost:9000

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

我们所需要的只是类路径上的正确内容。为此,我们在Maven POM中增加了几行新行:

绒球.xml

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Dalston.SR4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
...
</dependencies>

请注意“spring-cloud-starter-zuul”的使用 - 它是一个入门POM,就像Spring Boot一样,但它控制了我们这个Zuul代理所需的依赖项。我们也在使用,因为我们希望能够依赖于所有版本的传递依赖项都是正确的。​​<dependencyManagement>​

在客户端中使用代理

有了这些更改,我们的应用程序仍然可以工作,但在修改客户端之前,我们还没有实际使用新的代理。幸运的是,这是微不足道的。我们只需要恢复我们所做的更改,从“单一”到“原版”样本最后一节:

Home.component.ts

constructor(private app: AppService, private http: HttpClient) {
http.get('resource').subscribe(data => this.greeting = data);
}

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

进一步简化

更好的是:我们不再需要资源服务器中的 CORS 过滤器。无论如何,我们很快就把那个放在了一起,这应该是一个红灯,我们必须手动做任何技术上集中精力的事情(特别是在涉及安全性的情况下)。幸好现在多余了,我们可以把它扔掉,晚上回去睡觉!

保护资源服务器

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


旁白:如果您的网络体系结构反映了应用程序体系结构,则缺乏软件安全性甚至可能不是问题(您可以使除 UI 服务器之外的任何人都无法访问资源服务器)。作为一个简单的演示,我们可以使资源服务器只能在本地主机*问。只需将其添加到资源服务器中:​​application.properties​


应用程序属性

server.address: 127.0.0.1


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


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

绒球.xml

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

这足以让我们获得一个安全的资源服务器,但它还没有让我们得到一个工作的应用程序,原因与它没有的原因相同。第三部分:两个服务器之间没有共享的身份验证状态。

共享身份验证状态

我们可以使用与上一次相同的机制来共享身份验证(和 CSRF)状态,即春季会议.我们像以前一样将依赖项添加到两个服务器:

绒球.xml

<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
</dependency>

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

应用程序.yml

zuul:
routes:
resource:
sensitive-headers:

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

资源应用.java

@SpringBootApplication
@RestController
class ResourceApplication extends WebSecurityConfigurerAdapter {

...

@Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic().disable();
http.authorizeRequests().anyRequest().authenticated();
}

}


旁白:另一种也会阻止身份验证对话框的方法是保留HTTP Basic,但将401质询更改为“Basic”以外的内容。您可以使用配置回调中的单行实现来执行此操作。​​AuthenticationEntryPoint​​​​HttpSecurity​


另一种是在以下文件中明确要求非无状态会话创建策略:​​application.properties​

应用程序属性

security.sessions: NEVER

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

它是如何工作的?

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

动词

路径

地位

响应

获取

/

200

索引.html

获取

/*。.js

200

资产形成角度

获取

/用户

401

未授权(忽略)

获取

/资源

401

对资源的未经身份验证的访问

获取

/用户

200

经过 JSON 身份验证的用户

获取

/资源

200

(代理)JSON问候语

这与结尾的序列相同第二部分除了cookie名称略有不同(“SESSION”而不是“JSESSIONID”)的事实,因为我们使用的是Spring Session。但是体系结构是不同的,对“/resource”的最后一个请求是特殊的,因为它被代理到资源服务器。

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

尝试使用不同的浏览器,这样就没有身份验证交叉的机会(例如,如果使用 Chrome 测试用户界面,请使用 Firefox) - 它不会阻止应用程序工作,但如果它们包含来自同一浏览器的混合身份验证,则会使痕迹更难阅读。

/跟踪

{
"timestamp": 1420558194546,
"info": {
"method": "GET",
"path": "/",
"query": ""
"remote": true,
"proxy": "resource",
"headers": {
"request": {
"accept": "application/json, text/plain, */*",
"x-xsrf-token": "542c7005-309c-4f50-8a1d-d6c74afe8260",
"cookie": "SESSION=c18846b5-f805-4679-9820-cd13bd83be67; XSRF-TOKEN=542c7005-309c-4f50-8a1d-d6c74afe8260",
"x-forwarded-prefix": "/resource",
"x-forwarded-host": "localhost:8080"
},
"response": {
"Content-Type": "application/json;charset=UTF-8",
"status": "200"
}
},
}
},
{
"timestamp": 1420558200232,
"info": {
"method": "GET",
"path": "/resource/",
"headers": {
"request": {
"host": "localhost:8080",
"accept": "application/json, text/plain, */*",
"x-xsrf-token": "542c7005-309c-4f50-8a1d-d6c74afe8260",
"cookie": "SESSION=c18846b5-f805-4679-9820-cd13bd83be67; XSRF-TOKEN=542c7005-309c-4f50-8a1d-d6c74afe8260"
},
"response": {
"Content-Type": "application/json;charset=UTF-8",
"status": "200"
}
}
}
},

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

结论

我们在本节中涵盖了很多内容,但我们到达了一个非常好的地方,我们的两个服务器中的样板代码数量最少,它们都非常安全,用户体验不会受到影响。仅此一项就是使用 API 网关模式的一个原因,但实际上我们只是触及了它可能用于什么的表面(Netflix 将其用于很多事情).继续阅读春云以了解有关如何轻松向网关添加更多功能的更多信息。这下一节在本系列中,通过将身份验证职责提取到单独的服务器(单一登录模式)来稍微扩展应用程序体系结构。