セキュアな単一ページアプリケーション

このチュートリアルでは、快適で安全なユーザーエクスペリエンスを提供するためにSpring Security、Spring Boot、およびAngularの優れた機能が連携して機能することを示します。SpringとAngularの初心者がアクセスできるようにする必要がありますが、どちらの専門家にも役立つ詳細がたくさんあります。これは、実際にはSpring SecurityおよびAngularの一連のセクションの最初であり、それぞれに新しい機能が連続して公開されています。2回目以降の分割払いでアプリケーションを改善しますが、この後の主な変更は機能的というよりもアーキテクチャ上の変更です。

Springとシングルページアプリケーション

HTML5、豊富なブラウザベースの機能、および「シングルページアプリケーション」は現代の開発者にとって非常に価値のあるツールですが、意味のあるやり取りにはバックエンドサーバーと静的コンテンツ(HTML、CSS、JavaScript)が関係します。バックエンドサーバーが必要です。バックエンドサーバーは、静的コンテンツの提供、動的HTMLのレンダリング、ユーザーの認証、保護されたリソースへのアクセスの保護、JavaScriptとの対話など、いくつかのロールのいずれかまたはすべてを実行できます。HTTPおよびJSON(REST APIと呼ばれることもあります)を介してブラウザで。

Springは常にバックエンド機能 (特に企業で) を構築するための人気のある技術であり、Spring Bootの出現により、物事はかつてないほど簡単になりました。Spring Boot、Angular、Twitter Bootstrapを使って、ゼロから新しいシングルページアプリケーションを構築する方法を見てみましょう。特定のスタックを選択する特別な理由はありませんが、特にエンタープライズJavaショップのコアSpring支持者には非常に人気があるため、出発点として価値があります。

新規プロジェクトの作成

SpringとAngularを完全に使いこなしていない人でも、何が起こっているのかを追うことができるように、このアプリケーションの作成を少し詳しく説明します。追いかけたい場合は、アプリケーションが動作している最後までスキップして、すべてがどのように適合するかを確認できます。新しいプロジェクトを作成するためのさまざまなオプションがあります。

ビルドするプロジェクト全体のソースコードはGithubはこちら(英語) にあるため、必要に応じてプロジェクトを複製し、そこから直接作業することができます。次に、次のセクションにジャンプします。

Curlを使用する

開始する新しいプロジェクトを作成する最も簡単な方法は、Spring Boot Initializr(英語) を使用することです。例: UN*Xのようなシステムでcurlを使用:

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

次に、そのプロジェクト(デフォルトでは通常のMaven Javaプロジェクト)をお気に入りのIDEにインポートするか、コマンドラインでファイルと「mvn」を操作するだけです。次に、次のセクションにジャンプします。

Spring Boot CLIの使用

次のように、Spring Boot CLIを使用して同じプロジェクトを作成できます。

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

次に、次のセクションにジャンプします。

Initializr Webサイトの使用

必要に応じて、Spring Boot Initializr(英語) から.zipファイルと同じコードを直接取得することもできます。ブラウザで開き、依存関係「Web」と「セキュリティ」を選択して、「プロジェクトを生成」をクリックします。.zipファイルには、ルートディレクトリに標準のMavenまたはGradleプロジェクトが含まれているため、展開する前に空のディレクトリを作成することをお勧めします。次に、次のセクションにジャンプします。

Spring Tool Suiteを使用する

Pleiades All in One (STS, Lombok 付属) または Spring Tool Suite(英語) (Eclipseプラグインのセット)では、File->New->Spring Starter Projectのウィザードを使用してプロジェクトを作成およびインポートすることもできます。次に、次のセクションにジャンプします。IntelliJ IDEAとNetBeansには同様の機能があります。

Angularアプリを追加する

Angular(または最新のフロントエンドフレームワーク)の単一ページアプリケーションの中核は、最近Node.jsビルドになります。Angularには、これをすばやくセットアップするためのツールがいくつかあるため、使用できます。また、他のSpring Bootアプリケーションと同様に、Mavenでビルドするオプションも保持できます。Angularアプリのセットアップ方法の詳細は別の場所(GitHub) で説明されています。または、githubからこのチュートリアルのコードをチェックアウトすることもできます。

アプリケーションの実行

Angularアプリの準備が整うと、アプリケーションはブラウザーにロード可能になります(まだあまり機能していません)。コマンドラインでこれを行うことができます

$ mvn spring-boot:run

http://localhost:8080のブラウザに移動します。ホームページをロードすると、ユーザー名とパスワードを要求するブラウザダイアログが表示されます(ユーザー名は「user」であり、パスワードは起動時にコンソールログに出力されます)。実際にはまだコンテンツがありません(または ng CLIのデフォルトの「ヒーロー」チュートリアルコンテンツ)。本質的に空白のページを取得する必要があります。

コンソールログでパスワードを取得したくない場合は、これを「application.properties」(「src / main / resources」内)に追加します: security.user.password=password (および独自のパスワードを選択します)。サンプルコードでは、「application.yml」を使用してこれを行いました。

IDEでは、アプリケーションクラスで main() メソッドを実行するだけです(クラスは1つだけで、上記の「curl」コマンドを使用した場合は UiApplication と呼ばれます)。

スタンドアロンJARとしてパッケージ化して実行するには、次のようにします。

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

Angularアプリケーションのカスタマイズ

「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のコードのほとんどはボイラープレートです。興味深いものはすべて、AppComponent にあります。AppComponent では、「セレクタ」(HTML要素の名前)と、@Component アノテーションを介してレンダリングするHTMLのスニペットを定義します。HTMLテンプレート( "app.component.html" )を編集する必要もあります。

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!」と表示されます。 greeting は、handlebarプレースホルダー {{greeting.id}} および {{greeting.content}}を使用して、HTMLのAngularによってレンダリングされます。

動的コンテンツの追加

これまでに、グリーティングがハードコードされたアプリケーションがあります。これは、物事がどのように組み合わされるかを学習できますが、実際にはコンテンツがバックエンドサーバーから来ることを期待しているため、グリーティングを取得するために使用できるHTTPエンドポイントを作成しましょう。アプリケーションクラス(GitHub) (「src / main / java / demo」)で、@RestController アノテーションを追加し、新しい @RequestMappingを定義します。

UiApplication.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にしようとすると、デフォルトで安全であることがわかります。

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

Angularからの動的リソースのロード

ブラウザでそのメッセージを取得しましょう。 AppComponent を変更して、XHRを使用して保護されたリソースをロードします。

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);
  }
}

Angularによって http モジュールを介して提供される http サービス(英語) を注入し、それを使用してリソースを取得しました。Angularが応答を渡し、JSONをプルしてグリーティングに割り当てます。

カスタムコンポーネントへの http サービスの依存性注入を有効にするには、コンポーネントを含む AppModule で宣言する必要があります(最初のドラフトと比較して、imports ではもう1行だけです)。

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が表示されます。そのため、リソースは保護されており、直接curlすることはできませんが、ブラウザはコンテンツにアクセスできました。100行未満のコードで安全な単一ページのアプリケーションがあります!

静的リソースを変更した後、ブラウザに強制的にリロードさせる必要がある場合があります。Chrome(およびプラグイン付きFirefox)では、「開発者ツール」(F12)を使用できますが、それで十分かもしれません。または、CTRL + F5を使用する必要がある場合があります。

仕組みは?

一部の開発者ツールを使用すると、ブラウザーとバックエンド間の相互作用をブラウザーで確認できます(通常、F12はこれを開き、デフォルトでChromeで動作し、Firefoxでプラグインが必要になる場合があります)。概要は次のとおりです。

動詞パス状況レスポンス

GET

/

401

認証のためのブラウザプロンプト

GET

/

200

index.html

GET

/*.js

200

Angularからの3番目のアセットのロード

GET

/main.bundle.js

200

アプリケーションロジック

GET

/resource

200

JSONグリーティング

ブラウザはホームページの読み込みを単一の対話として扱うため、401は表示されない可能性があります。CORS(英語) ネゴシエーションがあるため、「/resource」に対する2つのリクエストが表示される場合があります。

リクエストをより詳細に見ると、すべてのリクエストに「Authorization」ヘッダーがあることがわかります。次のようなものです。

Authorization: Basic dXNlcjpwYXNzd29yZA==

ブラウザーは、すべてのリクエストでユーザー名とパスワードを送信しています(そのため、本番環境でHTTPSのみを使用することを忘れないでください)。それについて「Angular」はないため、JavaScriptフレームワークまたは選択した非フレームワークで動作します。

それのどこが悪いんだい?

一見、かなり良いジョブをしたようです。簡潔で実装しやすく、すべてのデータは秘密のパスワードで保護されています。フロントエンドまたはバックエンドのテクノロジーを変更しても機能します。しかし、いくつかの課題があります。

CSRFは、バックエンドリソースを取得するだけでよいため(つまり、サーバーの状態が変更されないため)、実際のアプリケーションでは課題になりません。アプリケーションにPOST、PUT、またはDELETEがあるとすぐに、合理的な最新の手段ではもはや安全ではなくなります。

このシリーズの次のセクションでは、フォームベース認証を使用するようにアプリケーションを継承します。これは、HTTP Basicよりもはるかに柔軟です。フォームを作成したら、CSRF保護が必要になります。Spring SecurityとAngularの両方には、これを支援するためのすぐに使える機能がいくつかあります。ネタバレ: HttpSessionを使用する必要があります。

ありがとう。このシリーズの開発を手伝ってくれた皆さん、特にRob Winch(英語) Thorsten Spaeth(英語) には、テキストとソースコードの入念なレビューと、最もよく知っていると思った部分についても、知らなかったいくつかのトリックを教えてくれたことに感謝します。

ログインページ

このセクションでは、「シングルページアプリケーション」でSpring SecurityAngular(英語) と使用する方法について引き続き説明します。ここでは、Angularを使用して、フォームを介してユーザーを認証し、安全なリソースをフェッチしてUIでレンダリングする方法を示します。これは一連のセクションの2番目であり、アプリケーションの基本的な構成要素に追いつくか、最初のセクションを読んでゼロからビルドするか、Githubのソースコードに直接(英語) 進むことができます。最初のセクションでは、HTTP基本認証を使用してバックエンドリソースを保護する単純なアプリケーションを構築しました。これでは、ログインフォームを追加し、ユーザーに認証するかどうかを制御し、最初の反復での課題を修正します(主にCSRF保護の欠如)。

注意:このセクションでサンプルアプリケーションを使用している場合は、ブラウザのキャッシュのCookieとHTTP Basic資格情報を必ずクリアしてください。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/> で、ログインリンクとログアウトリンクのあるナビゲーションバーがあります。

<router-outlet/> セレクターはAngularによって提供され、メインモジュールのコンポーネントに接続する必要があります。ルートごと(メニューリンクごと)に1つのコンポーネントがあり、1つにくっつけて状態を共有するヘルパーサービス(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 { }

"RouterModule" (英語) と呼ばれるAngularモジュールへの依存関係を追加しました。これにより、AppComponentのコンストラクターにマジック router を注入できました。 routes は、AppModule のインポート内で「/」(「ホーム」コントローラー)および「/login」(「ログイン」コントローラー)へのリンクをセットアップするために使用されます。

また、そこに FormsModule を忍び込ませました。これは、ユーザーがログインしたときに送信したいフォームにデータをバインドするために後で必要になるためです。

UIコンポーネントはすべて「宣言」であり、サービスグルーは「プロバイダー」です。 AppComponent は実際にはあまり機能しません。アプリのルートに付属するTypeScriptコンポーネントは次のとおりです。

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の依存性注入がいくつかあります

  • コンポーネントのプロパティとして公開されたログアウト関数があります。これは後でログアウト要求をバックエンドに送信するために使用できます。 app サービスにフラグを設定し、ユーザーをログイン画面に送り返します(これを finally() コールバックを介して無条件に行います)。

  • templateUrl を使用して、テンプレートHTMLを別のファイルに外部化します。

  • 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();
        });

    }

}

authenticated フラグは単純です。 authenticate() 関数は、提供されている場合はHTTP基本認証資格情報を送信し、提供されていない場合は送信しません。また、オプションの callback 引数があり、認証が成功した場合にコードを実行するために使用できます。

挨拶

古いホームページのグリーティングコンテンツは、「src / app」の「app.component.html」のすぐ隣に移動できます。

home.component.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 はグリーティングを取得する必要があり、AppServiceからフラグをプルする authenticated() ユーティリティ機能も提供する必要があります。

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" id="username" name="username" [(ngModel)]="credentials.username"/>
	</div>
	<div class="form-group">
		<label for="password">Password:</label> <input type="password"
			class="form-control" id="password" name="password" [(ngModel)]="credentials.password"/>
	</div>
	<button type="submit" class="btn btn-primary">Submit</button>
</form>

これは非常に標準的なログインフォームで、ユーザー名とパスワードの2つの入力と、Angularイベントハンドラー (submit)を介してフォームを送信するためのボタンがあります。formタグでアクションを実行する必要はありません。そのため、アクションをまったく挿入しない方がよいでしょう。Angularモデルに errorが含まれる場合にのみ表示されるエラーメッセージもあります。フォームコントロールはAngularフォーム(英語) ngModel を使用してHTMLとAngularコントローラー間でデータを渡します。この場合、credentials オブジェクトを使用してユーザー名とパスワードを保持します。

認証プロセス

追加したログインフォームをサポートするには、さらに機能を追加する必要があります。クライアント側ではこれらは LoginComponentに実装され、サーバーではSpring Security構成になります。

ログインフォームの送信

フォームを送信するには、すでに ng-submitを介してフォームで参照した login() 関数と、ng-modelを介して参照した credentials オブジェクトを定義する必要があります。「ログイン」コンポーネントを具体化します。

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() を定義します。

authenticate() は、相対リソース(アプリケーションのデプロイルートを基準とする)「/user」に対してGETを行います。 login() 関数から呼び出されると、ヘッダーにBase64エンコードされた資格情報を追加し、サーバー上で認証を行い、代わりにCookieを受け入れます。 login() 関数は、認証の結果を取得するときに、それに応じてローカル $scope.error フラグも設定します。これは、ログインフォームの上にエラーメッセージの表示を制御するために使用されます。

現在認証されているユーザー

authenticate() 関数を処理するには、新しいエンドポイントをバックエンドに追加する必要があります。

UiApplication.java
@SpringBootApplication
@RestController
public class UiApplication {

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

  ...

}

これは、Spring Securityアプリケーションで役立つトリックです。「/user」リソースに到達できる場合、現在認証されているユーザー( Authentication (GitHub) )を返します。そうでない場合、Spring Securityは要求をインターセプトし、 AuthenticationEntryPoint (GitHub) を介して401応答を送信します。

サーバーでのログインリクエストの処理

Spring Securityを使用すると、ログイン要求を簡単に処理できます。メインアプリケーションクラス(GitHub) にいくつかの構成を追加するだけです(たとえば、内部クラスとして)。

UiApplication.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 Securityをカスタマイズした標準Spring Bootアプリケーションであり、静的(HTML)リソースへの匿名アクセスのみを許可します。HTMLリソースは、明確になる理由のため、Spring Securityによって無視されるだけでなく、匿名ユーザーが利用できる必要があります。

覚えておく必要がある最後のことは、Angularが提供するJavaScriptコンポーネントをアプリケーションで匿名で利用できるようにすることです。上記の HttpSecurity 構成でそれを行うことができますが、静的コンテンツであるため、単純に無視する方が良いです:

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

デフォルトのHTTPリクエストヘッダーの追加

この時点でアプリを実行すると、ブラウザが(ユーザーとパスワードの)基本認証ダイアログをポップアップすることがわかります。これは、「WWW-Authenticate」ヘッダーを持つ /user および /resource へのXHR要求からの401応答を確認するためです。このポップアップを抑制する方法は、Spring Securityから来るヘッダーを抑制することです。また、応答ヘッダーを抑制する方法は、特別な従来の要求ヘッダー「X-Requested-With = XMLHttpRequest」を送信することです。以前はAngularのデフォルトでしたが、1.3.0では削除されました(GitHub) 。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);
  }
}

ここの構文は定型です。 Classimplements プロパティはその基本クラスであり、コンストラクターに加えて、本当に必要なことは、Angularによって常に呼び出され、ヘッダーを追加するために使用できる intercept() 関数をオーバーライドすることだけです。

この新しい RequestOptions ファクトリーをインストールするには、AppModuleproviders で宣言する必要があります。

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

ログアウト

アプリケーションはほぼ機能的に終了しています。最後に行う必要があるのは、ホームページでスケッチしたログアウト機能を実装することです。ユーザーが認証された場合、「ログアウト」リンクを表示し、それを AppComponentlogout() 関数にフックします。サーバーに実装する必要がある「/logout」にHTTP POSTを送信することを忘れないでください。これは、Spring Securityによって既に追加されているためです(つまり、この単純な使用例では何もする必要はありません)。ログアウトの動作をさらに制御するには、WebSecurityAdapterHttpSecurity コールバックを使用して、たとえばログアウト後にいくつかのビジネスロジックを実行できます。

CSRFの保護

アプリケーションはほとんど使用する準備ができており、実際に実行すると、ログアウトリンクを除いて、これまでに作成したすべてが実際に機能することがわかります。それを使用してみて、ブラウザで応答を参照してください。その理由がわかります。

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トークンの値は、ホームページをロードした最初の要求からの HttpRequest 属性でサーバー側で利用可能でした。クライアントに渡すには、サーバー上の動的なHTMLページを使用してレンダリングするか、カスタムエンドポイントを介して公開するか、Cookieとして送信します。AngularにはCookieに基づいたCSRF(英語) (「XSRF」と呼ばれる)のサポートが組み込まれ(英語) ているため、最後の選択が最適です。

そのため、サーバーには、Cookieを送信するカスタムフィルターが必要です。AngularはCookie名を「XSRF-TOKEN」にすることを望んでおり、Spring Securityはデフォルトでそれを要求属性として提供するため、要求属性からCookieに値を転送するだけです。幸い、Spring Security(4.1.0以降)は、これを正確に行う特別な CsrfTokenRepository を提供します。

UiApplication.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でプラグインが必要になる場合があります)。概要は次のとおりです。

動詞パス状況レスポンス

GET

/

200

index.html

GET

/*.js

200

角度からのアセット

GET

/user

401

不許可 (無視されました)

GET

/home

200

ホームページ

GET

/user

401

不許可 (無視されました)

GET

/resource

401

不許可 (無視されました)

GET

/user

200

資格情報を送信してJSONを取得する

GET

/resource

200

JSONグリーティング

上記の「無視」とマークされた応答は、XHR呼び出しでAngularが受信したHTML応答であり、そのデータを処理していないため、HTMLはフロアにドロップされます。「/user」リソースの場合、認証されたユーザーを探しますが、最初の呼び出しには存在しないため、その応答はドロップされます。

リクエストをよく見ると、すべてのリクエストにCookieが含まれていることがわかります。クリーンなブラウザ(たとえばChromeのシークレットモード)で起動した場合、最初のリクエストではサーバーに送信されるCookieはありませんが、サーバーは "JSESSIONID"(通常の HttpSession)および "X- XSRF-TOKEN」(上記で設定したCRSF Cookie)。後続のリクエストにはすべてこれらのCookieがあり、それらは重要です。アプリケーションはそれらのCookieなしでは機能せず、いくつかの本当に基本的なセキュリティ機能(認証およびCSRF保護)を提供しています。Cookieの値は、ユーザーが(POST後に)認証すると変更されます。これは、別の重要なセキュリティ機能です( セッション固定攻撃(英語) を防止します)。

アプリケーションからロードされたページにない場合でもブラウザーが自動的に送信するため、サーバーに送り返されるCookieに依存するCSRF保護には不十分です(クロスサイトスクリプティング攻撃、別名XSS(英語) )。ヘッダーは自動的に送信されないため、オリジンは制御されます。このアプリケーションでは、CSRFトークンがCookieとしてクライアントに送信されるため、ブラウザーによって自動的に返送されますが、保護を提供するのはヘッダーです。

ヘルプ、私のアプリケーションはどのように拡張されますか?

「しかし... 1ページのアプリケーションでセッションステートを使用するのは非常に悪いことではありませんか? 」この質問に対する答えは、「ほとんど」使用します。なぜなら、認証とCSRF保護のためにセッションを使用することは、間違いなく良いことです。この状態はどこかに保存する必要があり、セッションから取り出す場合は、別の場所に置き、サーバーとクライアントの両方で手動で管理する必要があります。これは単にコードが増えただけであり、メンテナンスも増えただけであり、全体として完全に車輪を再発明したことになります。

「しかし、しかし…」と答えるでしょう。「アプリケーションを今どのように水平にスケーリングするのですか?」これは上で尋ねていた「本当の」質問ですが、「セッション状態が悪い、ステートレスでなければなりません」に短縮される傾向があります。パニックにならないでください。ここで重要なのは、セキュリティステートフルであることです。安全でステートレスなアプリケーションを持つことはできません。状態をどこに保存しますか?これですべてです。Rob Winch(英語) Spring Exchange 2014(英語) で非常に有益でインサイトに富んだ講演を行い、状態の必要性(およびその普遍性-TCPとSSLはステートフルであるため、システムはそれを知っているかどうかに関係なくステートフルです)。このトピックをさらに詳しく調べます。

良いニュースは、選択肢があることです。最も簡単な選択は、セッションデータをメモリに保存し、ロードバランサーのスティッキーセッションに依存して、同じセッションからのリクエストを同じJVMにルーティングすることです(すべてサポートしています)。これで十分に理解でき、非常に多くのユースケースで機能します。もう1つの選択肢は、アプリケーションのインスタンス間でセッションデータを共有することです。厳格でセキュリティデータのみを保存している限り、データは小さく、変更はめったに行われません(ユーザーがログインまたはログアウトしたとき、またはセッションがタイムアウトしたときのみ)。インフラストラクチャに大きな問題はありません。Spring Session(GitHub) を使用するのも簡単です。このシリーズの次のセクションでSpring Sessionを使用するため、ここで設定する方法について詳しく説明する必要はありませんが、文字通り数行のコードとRedisサーバーであり、非常に高速です。

共有セッション状態をセットアップするもう1つの簡単な方法は、アプリケーションをWARファイルとしてCloud Foundry Pivotal Webサービス(英語) にデプロイし、それをRedisサービスにバインドすることです。

しかし、カスタムトークンの実装(ステートレス、外観)はどうですか?

それが最後のセクションへの応答であった場合、それをもう一度参照してください。トークンをどこかに保存した場合はおそらくステートレスではありませんが、保存していない場合(たとえば、JWTエンコードトークンを使用している場合)、CSRF保護をどのように提供しますか?それは重要です。経験則(Rob Winchに起因)は次のとおりです。アプリケーションまたはAPIがブラウザーからアクセスされる場合、CSRF保護が必要です。セッションなしでそれができないということではなく、自分ですべてのコードを書かなければならないということです。そして、すでに実装されており、HttpSession の上で完全にうまく機能するため、何がポイントになるでしょう使用し、最初から仕様に焼き付けているコンテナーの?) CSRFを必要とせず、完全に「ステートレス」(非セッションベース)のトークン実装を持っていると判断したとしても、それを消費して使用するためにクライアントに追加のコードを記述する必要がありました。ブラウザーとサーバーの独自の組み込み機能:ブラウザーは常にCookieを送信し、サーバーは常にセッションを保持します(スイッチをオフにしない限り)。そのコードはビジネスロジックではなく、収益を上げるものではなく、単なるオーバーヘッドであるため、さらに悪いことにお金がかかります。

結論

現在のアプリケーションは、ユーザーがライブ環境の「実際の」アプリケーションに期待するものに近いものであり、おそらく、そのアーキテクチャを備えたより機能豊富なアプリケーション(静的な単一サーバーコンテンツとJSONリソース)。 HttpSession を使用してセキュリティデータを保存し、クライアントが送信するCookieを考慮して使用することを頼りにしていますが、独自のビジネスドメインに集中できるため、それに満足しています。次のセクションでは、アーキテクチャを個別の認証およびUIサーバーに加えて、JSON用のスタンドアロンリソースサーバーに拡張します。これは明らかに、複数のリソースサーバーに簡単に一般化できます。また、Spring Sessionをスタックに導入し、それを使用して認証データを共有する方法を示します。

リソースサーバー

このセクションでは、「シングルページアプリケーション」でSpring SecurityAngular(英語) と使用する方法について引き続き説明します。ここでは、アプリケーションの動的コンテンツとして使用している「あいさつ」リソースを、まず保護されていないリソースとして別のサーバーに分割し、次にOpaqueトークンで保護します。これは一連のセクションの3番目であり、アプリケーションの基本的な構成要素に追いつくか、最初のセクションを読んでゼロからビルドするか、Githubのソースコードに直接進むことができます。2つの部分: リソースが保護されていない(GitHub) 部分と、トークンによって保護されている部分(GitHub)

このセクションでサンプルアプリケーションを使用している場合は、ブラウザのキャッシュのCookieとHTTP基本認証情報を必ずクリアしてください。Chromeでは、単一サーバーでこれを行う最良の方法は、新しいシークレットウィンドウを開くことです。

別のリソースサーバー

クライアント側の変更

クライアント側では、リソースを別のバックエンドに移動するために行うことはあまりありません。最後のセクション(GitHub) の「ホーム」コンポーネントは次のとおりです。

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);

サーバー側の変更

UIサーバー(GitHub) の変更は簡単です。グリーティングリソースの @RequestMapping を削除するだけです(「/resource」でした)。次に、最初のセクションSpring Boot Initializr(英語) を使用して行ったように、新しいリソースサーバーを作成する必要があります。たとえば、UN*Xのようなシステムでcurlを使用する場合:

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

その後、そのプロジェクト(デフォルトでは通常のMaven Javaプロジェクト)をお気に入りのIDEにインポートするか、コマンドラインでファイルと「mvn」を操作するだけです。

メインアプリケーションクラス(GitHub) @RequestMapping を追加し、古いUI(GitHub) から実装をコピーするだけです:

ResourceApplication.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が表示されます。 application.properties (「src / main / resources」内)でポートの変更をベイクできます。

application.properties
server.port: 9000

ブラウザのUI(ポート8080)からそのリソースをロードしようとすると、ブラウザがXHRリクエストを許可しないため、機能しないことがわかります。

CORSネゴシエーション

ブラウザーは、クロスオリジンリソース共有(英語) プロトコルに従ってリソースサーバーへのアクセスが許可されているかどうかを確認するために、リソースサーバーとネゴシエートしようとします。Angularの責任ではないため、Cookie契約と同様に、ブラウザ内のすべてのJavaScriptでこのように機能します。2つのサーバーは共通の発信元を宣言していないため、ブラウザーは要求の送信を拒否し、UIは壊れています。

これを修正するには、「プリフライト」OPTIONSリクエストと呼び出し元の許可された動作をリストするヘッダーを含むCORSプロトコルをサポートする必要があります。Spring 4.2にはきめ細かいCORSサポート(英語) がいくつかあるため、コントローラーマッピングにアノテーションを追加するだけです。

ResourceApplication.java
@RequestMapping("/")
@CrossOrigin(origins="*", maxAge=3600)
public Message home() {
  return new Message("Hello World");
}
origins=* を簡単に使用するのは簡単で汚れており、動作しますが、安全ではなく、決して推奨されません。

リソースサーバーのセキュリティ保護

すごい! 新しいアーキテクチャーで動作するアプリケーションがあります。唯一の問題は、リソースサーバーにセキュリティがないことです。

Spring Securityの追加

UIサーバーのように、フィルターレイヤーとしてリソースサーバーにセキュリティを追加する方法も確認できます。最初のステップは本当に簡単です:Spring SecurityをMaven POMのクラスパスに追加するだけです:

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

リソースサーバーを再起動します。安全です:

$ 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 (GitHub) および TokenService (GitHub) を参照)。ただし、Spring Securityには正規の実装はありません。その理由の1つは、おそらくさらに簡単な方法があるからです。

このシリーズのパートIIから、Spring Securityはデフォルトで HttpSession を使用して認証データを保管することを思い出してください。ただし、セッションと直接対話することはありません。その間にはストレージバックエンドを変更するために使用できる抽象化レイヤー( SecurityContextRepository (GitHub) )があります。リソースサーバーで、UIによって認証が検証されたストアをそのリポジトリにポイントできる場合、2つのサーバー間で認証を共有する方法があります。UIサーバーには既にそのようなストア( HttpSession)があります。そのため、そのストアを配布してリソースサーバーに開くことができる場合、ほとんどのソリューションがあります。

Spring Session

ソリューションのその部分はSpring Session(GitHub) で非常に簡単です。必要なのは、共有データストア(RedisとJDBCはすぐにサポートされています)、および Filterをセットアップするためのサーバーの構成行です。

UIアプリケーションでは、POM(GitHub) にいくつかの依存関係を追加する必要があります。

pom.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 BootとSpring Sessionは連携してRedisに接続し、セッションデータを一元的に保存します。

その1行のコードとlocalhostで実行されているRedisサーバーを使用して、UIアプリケーションを実行し、有効なユーザー資格情報でログインすると、セッションデータ(認証)がredisに保存されます。

ローカルで実行しているredisサーバーがない場合は、Docker(英語) で簡単に起動できます(WindowsまたはMacOSではVMが必要です)。Githubのソースコード(GitHub) には docker-compose.yml (英語) ファイルがあり、docker-compose upを使用してコマンドラインで簡単に実行できます。VMでこれを行うと、Redisサーバーはlocalhostとは異なるホストで実行されるため、localhostにトンネリングするか、application.propertiesの正しい spring.redis.host を指すようにアプリを構成する必要があります。

UIからカスタムトークンを送信する

不足している唯一の部分は、ストア内のデータへのキーの転送メカニズムです。キーは HttpSession IDであるため、UIクライアントでそのキーを取得できる場合、リソースサーバーにカスタムヘッダーとして送信できます。そのため、「ホーム」コントローラーは、グリーティングリソースのHTTPリクエストの一部としてヘッダーを送信するように変更する必要があります。例:

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[ http://localhost:9000]」に直接移動する代わりに、「/token」のUIサーバー上の新しいカスタムエンドポイントへの呼び出しの成功コールバックでその呼び出しをラップしました。その実装は簡単です。

UiApplication.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アプリケーションの準備が整い、バックエンドへのすべての呼び出しに対して「X-Auth-Token」というヘッダーにセッションIDが含まれます。

リソースサーバーでの認証

リソースサーバーがカスタムヘッダーを受け入れることができるように、リソースサーバーに小さな変更が1つあります。CORS構成では、そのヘッダーをリモートクライアントからの許可されたヘッダーとして指定する必要があります。

ResourceApplication.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に通過を許可することを伝える必要があります。

ResourceApplication.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 SessionとRedisの依存関係を追加する必要があり、次に Filterをセットアップできます。

ResourceApplication.java
@SpringBootApplication
@RestController
class ResourceApplication {

  ...

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

}

作成されたこの Filter は、UIサーバーにあるもののミラーイメージであるため、Redisをセッションストアとして確立します。唯一の違いは、デフォルト(「JSESSIONID」という名前のCookie)ではなく、ヘッダー(デフォルトでは「X-Auth-Token」)を調べるカスタム HttpSessionStrategy を使用することです。また、ブラウザが認証されていないクライアントでダイアログをポップアップしないようにする必要があります。アプリは安全ですが、デフォルトで WWW-Authenticate: Basic で401を送信するため、ブラウザはユーザー名とパスワードのダイアログで応答します。これを実現する方法は複数ありますが、すでにAngularが「X-Requested-With」ヘッダーを送信するようにしているため、Spring Securityはデフォルトでそれを処理します。

リソースサーバーには、新しい認証スキームで動作するように、最後に1つの変更があります。Spring Bootのデフォルトセキュリティはステートレスであり、セッションに認証を保存するために、application.yml (または application.properties)で明示的にする必要があります。

application.yml
security:
  sessions: NEVER

これは、Spring Securityに対して「セッションを作成することはありませんが、セッションが存在する場合はセッションを使用する」と言います(UIでの認証のために既に存在します)。

リソースサーバーを再起動し、新しいブラウザーウィンドウでUIを開きます。

なぜすべてがCookieで機能しないのですか?

カスタムヘッダーを使用し、クライアントにヘッダーを入力するコードを記述する必要がありました。これはそれほど複雑ではありませんが、可能な限りCookieとセッションを使用するというパートIIのアドバイスと矛盾するようです。そうしないと、不必要な複雑さが追加されるという議論がありましたが、確かに、現在の実装はこれまで見てきた中で最も複雑です:ソリューションの技術的な部分は、ビジネスロジック(明らかに小さい)をはるかに上回っています。これは間違いなく公正な批判です(そして、このシリーズの次のセクションで説明する予定です)が、すべての目的で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%宣言型) ソリューションを使用できます。新しいコードをすべてライブラリ (Spring構成とAngularカスタムディレクティブ) に抽出することで、ソリューション100%を宣言型にすることもできます。この興味深いタスクは、次の2回の記事以降に延期します。次のセクションでは、現在の実装の複雑さをすべて軽減するための、別の本当に優れた方法として、API ゲートウェイパターン (クライアントはすべての要求を1つの場所に送信し、その場所で認証処理) を見ていきます。

ここでは、Spring Sessionを使用して、論理的に同じアプリケーションではない2つのサーバー間でセッションを共有しました。これは巧妙なトリックであり、「通常の」JEE分散セッションでは不可能です。

API Gateway

このセクションでは、「シングルページアプリケーション」でSpring SecurityAngular(英語) と使用する方法について引き続き説明します。ここでは、Spring Cloudを使用して認証とバックエンドリソースへのアクセスを制御するAPIゲートウェイを構築する方法を示します。これは一連のセクションの4番目であり、アプリケーションの基本的な構成要素に追いつくか、最初のセクションを読んでゼロから構築するか、Githubのソースコードに直接(英語) 進むことができます。前のセクションでは、Spring Session(GitHub) を使用してバックエンドリソースを認証する単純な分散アプリケーションを構築しました。これでは、UIサーバーをバックエンドリソースサーバーへのリバースプロキシにし、最後の実装(カスタムトークン認証によって導入された技術的な複雑さ)の課題を修正し、ブラウザークライアントからのアクセスを制御するための多くの新しいオプションを提供します。

注意:このセクションでサンプルアプリケーションを使用している場合は、ブラウザのキャッシュのCookieとHTTP Basic資格情報を必ずクリアしてください。Chromeでは、単一サーバーでこれを行う最良の方法は、新しいシークレットウィンドウを開くことです。

API Gatewayの作成

API Gatewayは、フロントエンドクライアントの単一のエントリポイント(および制御)であり、ブラウザベース(このセクションの例のような)またはモバイルの場合があります。クライアントは1つのサーバーのURLを知るだけでよく、バックエンドを変更せずに自由にリファクタリングできます。これは大きな利点です。集中化と制御に関して、レート制限、認証、監査、ログ記録などの利点があります。Spring Cloudを使用すると、単純なリバースプロキシの実装は非常に簡単です。

コードを順守している場合、最後のセクションの最後のアプリケーション実装が少し複雑であることがわかるため、それを繰り返すのに最適な場所ではありません。ただし、より簡単に開始できる中間点があり、バックエンドリソースはまだSpring Securityで保護されていませんでした。このソースコードはGithub(英語) の別個のプロジェクトなので、そこから始めます。UIサーバーとリソースサーバーがあり、お互いに通信しています。リソースサーバーにはSpring Securityがまだないため、最初にシステムを動作させてから、そのレイヤーを追加できます。

1行の宣言的リバースプロキシ

これをAPI Gatewayに変換するには、UIサーバーに1つの小さな調整が必要です。Spring構成のどこかで、たとえばメイン(のみ) アプリケーションクラス(GitHub) @EnableZuulProxy アノテーションを追加する必要があります。

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

外部設定ファイルでは、UIサーバーのローカルリソースを外部設定(GitHub) の リモートリソース( "application.yml")にマップする必要があります。

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

これは、「このサーバーのパターン/resource/**のパスを、localhost:9000のリモートサーバーの同じパスにマップする」ことを示しています。シンプルでありながら効果的です(OK。YAMLを含む6行ですが、必ずしも必要ではありません)!

この作業を行うために必要なのは、クラスパス上の適切なものだけです。そのために、Maven POMにいくつかの新しい行があります。

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」の使用に注意してください-Spring BootのようなスターターPOMですが、このZuulプロキシに必要な依存関係を管理します。また、推移的な依存関係のすべてのバージョンが正しいことに依存できるようにしたいため、<dependencyManagement> も使用しています。

クライアントでプロキシを使用する

これらの変更を適用しても、アプリケーションは引き続き機能しますが、クライアントを変更するまで、実際には新しいプロキシを使用していません。幸いなことにそれは簡単です。最後のセクションの「単一」から「vanilla」サンプルに行った変更を元に戻す必要があります。

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

サーバーを起動すると、すべてが機能し、リクエストはUI(API Gateway)を介してリソースサーバーにプロキシされます。

さらなる簡素化

さらに良いことは、リソースサーバーでCORSフィルターが不要になることです。すぐにそれを一緒に投げました、それは手で技術的に焦点を合わせたもの(特にセキュリティに関する場合)をしなければならなかったことは赤信号だったはずです。幸いなことにそれは冗長になったため、それを捨てて、夜に眠りに戻ることができます!

リソースサーバーのセキュリティ保護

中間状態では、リソースサーバーにセキュリティが設定されていないことを覚えているかもしれません。

余談:ネットワークアーキテクチャがアプリケーションアーキテクチャを反映している場合、ソフトウェアセキュリティの欠如は問題にならないかもしれません(リソースサーバーにUIサーバー以外の人が物理的にアクセスできないようにすることができます)。その簡単なデモンストレーションとして、リソースサーバーにローカルホストでのみアクセスできるようにします。これをリソースサーバーの application.properties に追加するだけです。

application.properties
server.address: 127.0.0.1

うわー、簡単でした! データセンターでのみ表示されるネットワークアドレスを使用して、すべてのリソースサーバーとすべてのユーザーデスクトップで機能するセキュリティソリューションを実現します。

ソフトウェアレベルでセキュリティが必要であると判断したと仮定します(多くの理由により、かなりの可能性があります)。問題になることはありません。依存関係としてSpring Securityを( リソースサーバーPOM(GitHub) に )追加するだけですから:

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

セキュアなリソースサーバーを取得するにはこれで十分ですが、パートIIIにはなかったのと同じ理由で、まだ動作しているアプリケーションは取得できません。2つのサーバー間で共有認証状態はありません。

認証状態の共有

認証(およびCSRF)状態を共有するために、前回と同じメカニズム、つまりSpring Session(GitHub) を使用できます。以前のように、両方のサーバーに依存関係を追加します。

pom.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>

ただし、同じ Filter 宣言を両方に追加できるため、今回は構成がはるかに簡単になります。最初にUIサーバー。すべてのヘッダーを転送することを明示的に宣言します(つまり、「機密」はありません)。

application.yml
zuul:
  routes:
    resource:
      sensitive-headers:

その後、リソースサーバーに移動できます。2つの小さな変更が必要です。1つは、リソースサーバーでHTTP Basicを明示的に無効にすることです(ブラウザが認証ダイアログをポップアップするのを防ぐため)。

ResourceApplication.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」以外のものに変更します。 HttpSecurity 構成コールバックの AuthenticationEntryPoint の1行の実装でそれを行うことができます。

もう1つは、application.propertiesで非ステートレスセッション作成ポリシーを明示的に要求することです。

application.properties
security.sessions: NEVER

redisがバックグラウンドで実行されている限り(起動する場合は docker-compose.yml (GitHub) を使用してください)、システムは動作します。http://localhost:8080でUIのホームページをロードしてログインすると、バックエンドからのメッセージがホームページに表示されます。

仕組みは?

バックグラウンドで何が起こっていますか?最初に、UIサーバー(およびAPI Gateway)でHTTPリクエストを確認できます。

動詞パス状況レスポンス

GET

/

200

index.html

GET

/*.js

200

角度からのアセット

GET

/user

401

不許可 (無視されました)

GET

/resource

401

リソースへの認証されていないアクセス

GET

/user

200

JSON認証済みユーザー

GET

/resource

200

(プロキシ)JSONグリーティング

これは、Spring Sessionを使用しているため、Cookie名がわずかに異なる(「JSESSIONID」ではなく「SESSION」)ことを除いて、パートIIの最後のシーケンスと同じです。ただし、アーキテクチャは異なり、「/resource」に対する最後の要求はリソースサーバーにプロキシされたため、特別です。

UIサーバーの「/trace」エンドポイント(Spring Cloud依存関係で追加したSpring Boot Actuatorから)を見ると、リバースプロキシの動作を確認できます。新しいブラウザでhttp://localhost:8080/traceに移動します(ブラウザ用のJSONプラグインをまだ入手していない場合は、ブラウザで見やすく読みやすくします)。HTTP Basic(ブラウザポップアップ)で認証する必要がありますが、ログインフォームと同じ資格情報が有効です。開始時またはその近くで、次のようなリクエストのペアが表示されます。

認証のクロスオーバーの可能性がないように別のブラウザーを使用してみてください(たとえば、UIのテストにChromeを使用しない場合はFirefoxを使用します)-アプリの動作を停止しませんが、含まれている場合はトレースを読みにくくします。同じブラウザからの認証の混合。
/trace
{
  "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"
      }
    }
  }
},

2番目のエントリは、クライアントから「/resource」のゲートウェイへのリクエストであり、Cookie(ブラウザによって追加)とCSRFヘッダー(パートIIで説明したようにAngularによって追加)を確認できます。最初のエントリには remote: true があり、これはリソースサーバーへの呼び出しをトレースしていることを意味します。uriパス "/"に送信されたことがわかり、(重要な)CookieとCSRFヘッダーも送信されていることがわかります。Spring Sessionがなければ、これらのヘッダーはリソースサーバーにとって意味がありませんが、設定方法では、これらのヘッダーを使用して認証およびCSRFトークンデータでセッションを再構成できます。リクエストは許可されており、ビジネスをしています!

結論

このセクションではかなり多くのことを説明しましたが、2台のサーバーに最小限のボイラープレートコードがあり、どちらも安全であり、ユーザーエクスペリエンスが損なわれない、本当に素晴らしい場所に到達しました。それだけがAPI Gatewayパターンを使用する理由になりますが、実際には、使用される可能性のある表面のほんの一部に過ぎません(Netflixは多くのこ(GitHub) とに使用しています)。Spring Cloudを読んで、より多くの機能をゲートウェイに簡単に追加する方法を確認してください。このシリーズの次のセクションでは、認証の責任を別のサーバーに抽出することで、アプリケーションアーキテクチャを少し拡張します(シングルサインオンパターン)。

OAuth2を使用したシングルサインオン

このセクションでは、「シングルページアプリケーション」でSpring SecurityAngular(英語) と使用する方法について引き続き説明します。ここでは、Spring Security OAuthSpring Cloudを使用してAPI Gatewayを継承し、シングルサインオンとOAuth2トークン認証をバックエンドリソースに実行する方法を示します。これは一連のセクションの5番目であり、アプリケーションの基本的な構成要素に追いつくか、最初のセクションを読んでゼロからビルドするか、Githubのソースコードに直接(英語) 進むことができます。最後のセクションでは、Spring Session(GitHub) を使用してバックエンドリソースを認証し、Spring Cloudを使用してUIサーバーに組み込みAPIゲートウェイを実装する小さな分散アプリケーションを構築しました。このセクションでは、認証サーバーへの多くのシングルサインオンアプリケーションの最初のUIサーバーを作成するために、認証の責任を別のサーバーに抽出します。これは、企業およびソーシャルスタートアップの両方で、最近の多くのアプリケーションで一般的なパターンです。OAuth2サーバーをオーセンティケーターとして使用するため、OAuth2サーバーを使用してバックエンドリソースサーバーのトークンを付与することもできます。Spring Cloudはアクセストークンをバックエンドに自動的に中継し、UIサーバーとリソースサーバーの両方の実装をさらに簡素化できるようにします。

注意:このセクションでサンプルアプリケーションを使用している場合は、ブラウザのキャッシュのCookieとHTTP Basic資格情報を必ずクリアしてください。Chromeでは、単一サーバーでこれを行う最良の方法は、新しいシークレットウィンドウを開くことです。

OAuth2認可サーバーの作成

最初のステップは、認証とトークン管理を処理する新しいサーバーを作成することです。パートIの手順に従って、Spring Boot Initializr(英語) から始めることができます。例: UN*Xのようなシステムでcurlを使用:

$ curl https://start.spring.io/starter.tgz -d style=web \
-d style=security -d name=authserver | tar -xzvf -

その後、そのプロジェクト(デフォルトでは通常のMaven Javaプロジェクト)をお気に入りのIDEにインポートするか、コマンドラインでファイルと「mvn」を操作するだけです。

OAuth2依存関係の追加

Spring OAuth依存関係を追加する必要があるため、POM(GitHub) に以下を追加します。

pom.xml
<dependency>
  <groupId>org.springframework.security.oauth</groupId>
  <artifactId>spring-security-oauth2</artifactId>
</dependency>

認可サーバーの実装は非常に簡単です。最小バージョンは次のようになります。

AuthserverApplication.java
@SpringBootApplication
@EnableAuthorizationServer
public class AuthserverApplication extends WebMvcConfigurerAdapter {

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

}

@EnableAuthorizationServerを追加した後)あと1つだけ行う必要があります。

application.properties
---
...
security.oauth2.client.clientId: acme
security.oauth2.client.clientSecret: acmesecret
security.oauth2.client.authorized-grant-types: authorization_code,refresh_token,password
security.oauth2.client.scope: openid
---

これにより、クライアント「acme」がシークレットと「authorization_code」を含むいくつかの認可された認可タイプで登録されます。

次に、テスト用の予測可能なパスワードを使用して、ポート9999で実行します。

application.properties
server.port=9999
security.user.password=password
server.contextPath=/uaa
...

また、デフォルト( "/")を使用しないようにコンテキストパスを設定します。そうしないと、ローカルホスト上の他のサーバーのCookieが間違ったサーバーに送信される可能性があります。サーバーを実行すると、サーバーが機能していることを確認できます。

$ mvn spring-boot:run

または、IDEで main() メソッドを開始します。

認可サーバーのテスト

サーバーはSpring Bootのデフォルトのセキュリティ設定を使用しているため、パートIのサーバーと同様に、HTTP Basic認証によって保護されます。認可コードトークンの付与(英語) を開始するには、認可エンドポイントにアクセスします。http://localhost:9999/uaa/oauth/authorize?response_type=code&client_id=acme&redirect_uri=http://example.comで認証されると、認証コードが添付されたexample.comへのリダイレクトが取得されます。例:http://example.com/?code=jYWioI(英語)

このサンプルアプリケーションの目的のために、リダイレクトが登録されていないクライアント「acme」を作成しました。これにより、example.comへのリダイレクトを取得できます。本番アプリケーションでは、常にリダイレクトを登録する(およびHTTPSを使用する)必要があります。

トークンエンドポイントの「acme」クライアント資格情報を使用して、コードをアクセストークンに交換できます。

$ curl acme:[email protected](英語)  :9999/uaa/oauth/token  \
-d grant_type=authorization_code -d client_id=acme     \
-d redirect_uri=http://example.com -d code=jYWioI
{"access_token":"2219199c-966e-4466-8b7e-12bb9038c9bb","token_type":"bearer","refresh_token":"d193caf4-5643-4988-9a4a-1c03c9d657aa","expires_in":43199,"scope":"openid"}

アクセストークンはUUID( "2219199c…")であり、サーバー内のメモリ内トークンストアによってバックアップされます。また、現在のトークンが期限切れになったときに新しいアクセストークンを取得するために使用できるリフレッシュトークンも取得しました。

「acme」クライアントに「パスワード」付与を認可したため、認証コードの代わりにcurlとユーザー資格情報を使用して、トークンエンドポイントからトークンを直接取得することもできます。これはブラウザベースのクライアントには適していませんが、テストには役立ちます。

上記のリンクをたどると、Spring OAuthが提供するホワイトラベルUIが表示されます。まず、これを使用します。後から戻って、パートIIの自己完結型サーバーの場合と同じように強化することができます。

リソースサーバーの変更

パートIVから続けると、リソースサーバーは認証にSpring Session(GitHub) を使用しているため、それを取り出してSpring OAuthに置き換えることができます。Spring SessionとRedisの依存関係も削除する必要があるため、これを置き換えます。

pom.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>

これとともに:

pom.xml
<dependency>
  <groupId>org.springframework.security.oauth</groupId>
  <artifactId>spring-security-oauth2</artifactId>
</dependency>

次に、セッション Filterメインアプリケーションクラス(GitHub) から削除し、便利な @EnableResourceServer アノテーション(Spring Security OAuth2から)に置き換えます。

ResourceApplication.java
@SpringBootApplication
@RestController
@EnableResourceServer
class ResourceApplication {

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

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

この1つの変更により、アプリはHTTP Basicではなくアクセストークンを要求する準備ができましたが、実際にプロセスを完了するには構成の変更が必要です。リソースサーバーが与えられたトークンをデコードし、ユーザーを認証できるように、少量の外部構成を( "application.properties" に)追加します。

application.properties
...
security.oauth2.resource.userInfoUri: http://localhost:9999/uaa/user

これは、トークンを使用して「/user」エンドポイントにアクセスし、それを使用して認証情報を取得できることをサーバーに伝えます(Facebook APIの「/me」エンドポイント(英語) に少し似ています)。事実上、Spring OAuth2の ResourceServerTokenServices インターフェースで表されるように、リソースサーバーがトークンをデコードする方法を提供します。

アプリケーションを実行し、コマンドラインクライアントでホームページにアクセスします。

$ curl -v localhost:9000
> GET / HTTP/1.1
> User-Agent: curl/7.35.0
> Host: localhost:9000
> Accept: */*
>
< 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"}

また、ベアラートークンが必要であることを示す「WWW-Authenticate」ヘッダーを持つ401が表示されます。

userInfoUri は、トークンをデコードする方法でリソースサーバーを接続する唯一の方法ではありません。実際、これは最も一般的な分母のようなもので(仕様の一部ではありません)、OAuth2プロバイダー(Facebook、Cloud Foundry、Githubなど)から非常に頻繁に利用でき、他の選択肢も利用できます。たとえば、トークン自体でユーザー認証をエンコードする(例:JWT(英語) を使用)か、共有バックエンドストアを使用します。CloudFoundryには /token_info エンドポイントもあり、ユーザー情報エンドポイントよりも詳細な情報を提供しますが、より完全な認証が必要です。さまざまなオプションが(当然)さまざまな利点とトレードオフを提供しますが、それらの詳細な説明はこのセクションの範囲外です。

ユーザーエンドポイントの実装

認可サーバーでは、そのエンドポイントを簡単に追加できます

AuthserverApplication.java
@SpringBootApplication
@RestController
@EnableAuthorizationServer
@EnableResourceServer
public class AuthserverApplication {

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

  ...

}

パートIIのUIサーバーと同じ @RequestMapping を追加し、Spring OAuthの @EnableResourceServer アノテーションも追加しました。これは、デフォルトで「/oauth/*」エンドポイントを除く認可サーバーのすべてを保護します。

エンドポイントを配置したら、認可サーバーによって作成されたベアラートークンを受け入れるようになったため、それとグリーティングリソースをテストできます。

$ TOKEN=2219199c-966e-4466-8b7e-12bb9038c9bb
$ curl -H "Authorization: Bearer $TOKEN" localhost:9000
{"id":"03af8be3-2fc3-4d75-acf7-c484d9cf32b1","content":"Hello World"}
$ curl -H "Authorization: Bearer $TOKEN" localhost:9999/uaa/user
{"details":...,"principal":{"username":"user",...},"name":"user"}

(自分で認証サーバーから取得したアクセストークンの値を代入して、それを自分で動作させます)。

UIサーバー

完了する必要があるこのアプリケーションの最後の部分は、認証部分を抽出し、認可サーバーに委譲するUIサーバーです。そのため、リソースサーバーと同様に、まずSpring SessionとRedisの依存関係を削除し、Spring OAuth2に置き換える必要があります。UI層でZuulを使用しているため、実際には spring-security-oauth2 ではなく spring-cloud-starter-oauth2 を直接使用します(これにより、プロキシを介してトークンを中継するための自動構成が設定されます)。

それが完了したら、セッションフィルターと「/user」エンドポイントも削除し、認可サーバーにリダイレクトするようにアプリケーションを設定できます( @EnableOAuth2Sso アノテーションを使用)。

UiApplication.java
@SpringBootApplication
@EnableZuulProxy
@EnableOAuth2Sso
public class UiApplication {

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

...

}

パートIVから、@EnableZuulProxyによってUIサーバーがAPI Gatewayとして機能し、YAMLでルートマッピングを宣言できることを思い出してください。そのため、「/user」エンドポイントを認可サーバーにプロキシできます。

application.yml
zuul:
  routes:
    resource:
      path: /resource/**
      url: http://localhost:9000
    user:
      path: /user/**
      url: http://localhost:9999/uaa/user

最後に、@EnableOAuth2Ssoによって設定されたSSOフィルターチェーンのデフォルトを変更するために使用されるため、アプリケーションを WebSecurityConfigurerAdapter に変更する必要があります。

SecurityConfiguration.java
@SpringBootApplication
@EnableZuulProxy
@EnableOAuth2Sso
public class UiApplication extends WebSecurityConfigurerAdapter {
    @Override
    public void configure(HttpSecurity http) throws Exception {
      http
          .logout().logoutSuccessUrl("/").and()
          .authorizeRequests().antMatchers("/index.html", "/app.html", "/")
          .permitAll().anyRequest().authenticated().and()
          .csrf()
            .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
    }

}

主な変更点(基本クラス名を除く)は、マッチャーが独自のメソッドに入ることであり、formLogin() はもう必要ありません。明示的な logout() 構成は、保護されていない成功URLを明示的に追加するため、/logout へのXHR要求は正常に戻ります。

@EnableOAuth2Sso アノテーションには、適切な認可サーバーと通信して認証できるようにするためのいくつかの必須の外部構成プロパティもあります。 application.ymlでこれが必要です:

application.yml
security:
  ...
  oauth2:
    client:
      accessTokenUri: http://localhost:9999/uaa/oauth/token
      userAuthorizationUri: http://localhost:9999/uaa/oauth/authorize
      clientId: acme
      clientSecret: acmesecret
    resource:
      userInfoUri: http://localhost:9999/uaa/user

その大部分は、OAuth2クライアント(「acme」)と認可サーバーの場所に関するものです。ユーザーがUIアプリ自体で認証できるように、userInfoUri もあります(リソースサーバーの場合と同様)。

UIアプリケーションで期限切れのアクセストークンを自動的にリフレッシュできるようにするには、中継を行うZuulフィルターに OAuth2RestOperations を挿入する必要があります。これを行うには、そのタイプのBeanを作成するだけです(詳細については OAuth2TokenRelayFilter を確認してください)。
@Bean
protected OAuth2RestTemplate OAuth2RestTemplate(
    OAuth2ProtectedResourceDetails resource, OAuth2ClientContext context) {
  return new OAuth2RestTemplate(resource, context);
}

クライアント

フロントエンドのUIアプリケーションには、認可サーバーへのリダイレクトをトリガーするためにまだ行う必要がある調整がいくつかあります。この簡単なデモでは、Angularアプリを必要最低限の要素にまとめて、何が起こっているかをより明確に確認できます。そのため、今のところ、フォームやルートの使用を控え、単一のAngularコンポーネントに戻ります。

app.component.ts
import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import 'rxjs/add/operator/finally';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {

  title = 'Demo';
  authenticated = false;
  greeting = {};

  constructor(private http: HttpClient) {
    this.authenticate();
  }

  authenticate() {

    this.http.get('user').subscribe(response => {
        if (response['name']) {
            this.authenticated = true;
            this.http.get('resource').subscribe(data => this.greeting = data);
        } else {
            this.authenticated = false;
        }
    }, () => { this.authenticated = false; });

  }
  logout() {
      this.http.post('logout', {}).finally(() => {
          this.authenticated = false;
      }).subscribe();
  }

}

AppComponent はすべてを処理し、ユーザーの詳細と、成功した場合は挨拶を取得します。また、logout 機能も提供します。

次に、この新しいコンポーネントのテンプレートを作成する必要があります。

app.component.html

<div class="container">
  <ul class="nav nav-pills">
    <li><a>Home</a></li>
    <li><a href="login">Login</a></li>
    <li><a (click)="logout()">Logout</a></li>
  </ul>
</div>
<div class="container">
<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>

それをホームページに <app-root/>として含めます。

「ログイン」のナビゲーションリンクは、href (Angularルートではない)との通常のリンクであることに注意してください。これが進む「/login」エンドポイントはSpring Securityによって処理され、ユーザーが認証されない場合、認可サーバーへのリダイレクトが行われます。

仕組みは?

すべてのサーバーを一緒に実行し、http://localhost:8080のブラウザーでUIにアクセスします。「ログイン」リンクをクリックすると、OAuth2からフェッチされたグリーティングを使用してUIのホームページにリダイレクトされる前に、認証サーバー(HTTP Basicポップアップ)にリダイレクトされ、トークン付与(ホワイトラベルHTML)が認可されます。UIを認証したのと同じトークンを使用するリソースサーバー。

一部の開発者ツールを使用すると、ブラウザーとバックエンド間の相互作用をブラウザーで確認できます(通常、F12はこれを開き、デフォルトでChromeで動作し、Firefoxでプラグインが必要になる場合があります)。概要は次のとおりです。

動詞パス状況レスポンス

GET

/

200

index.html

GET

/*.js

200

角度からのアセット

GET

/user

302

ログインページにリダイレクト

GET

/login

302

認証サーバーにリダイレクトする

GET

(uaa)/oauth/authorize

401

(無視)

GET

/login

302

認証サーバーにリダイレクトする

GET

(uaa)/oauth/authorize

200

HTTP基本認証はここで発生する

POST

(uaa)/oauth/authorize

302

ユーザーは許可を承認し、/loginにリダイレクトする

GET

/login

302

ホームページにリダイレクト

GET

/user

200

(プロキシ)JSON認証済みユーザー

GET

/app.html

200

ホームページのHTMLパーシャル

GET

/resource

200

(プロキシ)JSONグリーティング

接頭辞(uaa)が付いた要求は、認可サーバーへのものです。「無視」とマークされた応答は、XHR呼び出しでAngularが受信した応答であり、そのデータを処理していないため、床にドロップされます。「/user」リソースの場合、認証されたユーザーを探しますが、最初の呼び出しには存在しないため、その応答はドロップされます。

UIの「/trace」エンドポイント(一番下までスクロール)で、「/user」と「/resource」へのプロキシバックエンドリクエストが表示されます。remote:true とcookieの代わりにbearerトークン(パートIV)で認証に使用され、Spring Cloud Securityがこれを処理しました。@EnableOAuth2Sso@EnableZuulProxy があることを認識することで、(デフォルトで)プロキシされたバックエンドにトークンをリレーしたいことがわかります。

前のセクションと同様に、「/trace」に別のブラウザーを使用して、認証のクロスオーバーが発生しないようにしてください(たとえば、UIのテストにChromeを使用した場合はFirefoxを使用します)。

ログアウトエクスペリエンス

「ログアウト」リンクをクリックすると、ユーザーがUIサーバーで認証されなくなるため、ホームページが変更される(グリーティングが表示されなくなる)ことがわかります。「ログイン」をクリックしますが、実際には認証サーバーで認証と認可のサイクルに戻る必要ありません (ログアウトしていないため)。それが望ましいユーザーエクスペリエンスであるかどうかについて意見は分かれますが、悪名高いトリッキーな問題です(シングルサインアウト:Science Directの記事(英語) およびShibbolethのドキュメント(英語) )。理想的なユーザーエクスペリエンスは技術的に実現可能ではないかもしれません。また、ユーザーが望むことを本当に望んでいることを疑わなければならないこともあります。「「ログアウト」してログアウトしてほしい」というのは簡単ですが、「何からログアウトしますか?このSSOサーバーによって制御されているすべてのシステムからログアウトしますか、それともあなただけのシステムからログアウトしますか?」 「ログアウト」リンクをクリックしましたか?」興味がある場合は、このチュートリアルの後のセクションで詳細に説明されています。

結論

これで、Spring SecurityおよびAngularスタックの浅いツアーのほぼ終わりです。現在、UI / API Gateway、リソースサーバー、認可サーバー/トークン付与者という3つの個別のコンポーネントに明確な責任を負う素晴らしいアーキテクチャがあります。すべてのレイヤーの非ビジネスコードの量は最小限になり、より多くのビジネスロジックを使用して実装を継承および改善する場所を簡単に確認できます。次の手順では、認可サーバーでUIを整理し、おそらくJavaScriptクライアントでのテストを含むいくつかのテストを追加します。別の興味深いタスクは、すべてのボイラープレートコードを抽出し、Spring SecurityおよびSpring Sessionの自動構成と、Angularピースのナビゲーションコントローラー用のいくつかのwebjarリソースを含むライブラリ(たとえば、「spring-security-angular」)に入れることです。このシリーズのセクションを読んだ後、AngularまたはSpring Securityのいずれかの内部動作を学びたいと思っていた人はおそらく失望するでしょうが、それらがどのようにうまく機能し、少しの構成がどのように長くなるかを見たいなら方法、うまくいけば、良い経験をしたことでしょう。Spring Cloudは新規であり、これらのサンプルは作成時にスナップショットが必要でしたが、リリース候補版が利用可能になり、GAリリースが近日中にリリースされるため、Github(英語) またはgitter.im(英語) でフィードバックを送信してください。

シリーズの次のセクションでは、アクセス決定(認証以外)について説明し、同じプロキシの背後で複数のUIアプリケーションを使用します。

補遺: 認可サーバーのブートストラップUIおよびJWTトークン

Githubのソースコードには、パートII(英語) でログインページを作成したのと同様に、きれいなログインページとユーザー承認ページが実装されているこのアプリケーションの別のバージョンがあります。また、JWT(英語) を使用してトークンをエンコードするため、リソースサーバーは「/user」エンドポイントを使用する代わりに、トークン自体から十分な情報をプルして単純な認証を行うことができます。ブラウザークライアントは引き続きUIサーバーを介してプロキシを使用するため、ユーザーが認証されているかどうかを判断できます(実際のアプリケーションのリソースサーバーへの呼び出し回数と比較して、それほど頻繁に行う必要はありません) )。

複数のUIアプリケーションとゲートウェイ

このセクションでは、「シングルページアプリケーション」でSpring SecurityAngular(英語) と使用する方法について引き続き説明します。ここでは、Spring SessionSpring Cloudを使用して、パートIIとIVで構築したシステムの機能を組み合わせ、実際にはまったく異なる責任を持つ3つの単一ページアプリケーションを構築する方法を示します。目的は、APIリソースだけでなく、バックエンドサーバーからUIをロードするために使用される( パートIVのような)ゲートウェイを構築することです。ゲートウェイを使用して認証をバックエンドに渡すことにより、パートIIのトークンの問題を単純化します。次に、システムを継承して、ゲートウェイでIDと認証を制御しながら、バックエンドでローカルで詳細なアクセス決定を行う方法を示します。これは、一般に分散システムを構築するための非常に強力なモデルであり、構築するコードに機能を導入する際に検討できる多くの利点があります。

注意:このセクションでサンプルアプリケーションを使用している場合は、ブラウザのキャッシュのCookieとHTTP Basic資格情報を必ずクリアしてください。Chromeでこれを行う最善の方法は、新しいシークレットウィンドウを開くことです。

ターゲットアーキテクチャ

以下は、最初に構築する基本システムの写真です。

Components of the System

このシリーズの他のサンプルアプリケーションと同様に、UI(HTMLおよびJavaScript)とリソースサーバーがあります。セクションIVのサンプルと同様に、ゲートウェイがありますが、ここではUIの一部ではなく、別個のものです。UIは事実上バックエンドの一部になり、機能を再構成および再実装するための選択肢がさらに広がります。また、これから説明する他の利点ももたらします。

ブラウザはすべてのためにゲートウェイにアクセスし、バックエンドのアーキテクチャを知る必要はありません(基本的に、バックエンドがあることを知りません)。このゲートウェイでブラウザが行うことの1つに認証があります。セクションIIのようにユーザー名とパスワードを送信し、代わりにCookieを取得します。後続の要求では、Cookieが自動的に提示され、ゲートウェイはそれをバックエンドに渡します。Cookieの受け渡しを有効にするために、クライアントでコードを記述する必要はありません。バックエンドはCookieを使用して認証し、すべてのコンポーネントがセッションを共有するため、ユーザーに関する同じ情報を共有します。これと比較して、ゲートウェイでCookieをアクセストークンに変換する必要があり、アクセストークンはすべてのバックエンドコンポーネントで個別にデコードする必要があるセクションVと比較してください。

セクションIVと同様に、ゲートウェイはクライアントとサーバー間の対話を簡素化し、セキュリティを処理するための小さく明確に定義された表面を提供します。例:クロスオリジンリソース共有(英語) について心配する必要はありません。これは間違いを犯しやすいため、歓迎されます。

ビルドするプロジェクト全体のソースコードはGithubはこちら(英語) にあるため、必要に応じてプロジェクトを複製し、そこから直接作業することができます。このシステムの終了状態には追加のコンポーネント(「二重管理者」)があるため、今は無視してください。

バックエンドの構築

このアーキテクチャでは、バックエンドはセクションIIIで構築した「春のセッション」(GitHub) サンプルに非常に似ていますが、実際にはログインページは必要ありません。ここで必要なものに到達する最も簡単な方法は、おそらくセクションIIIから「リソース」サーバーをコピーし、セクションI「基本」(GitHub) サンプルからUIを取得することです。「基本」UIからここで必要なUIに到達するには、いくつかの依存関係を追加するだけです(セクションIIIでSpring Session(GitHub) を最初に使用したときのように)。

pom.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であるため、「/resource」エンドポイントは不要です。これを実行すると、非常に単純なAngularアプリケーション(「基本」サンプルと同じ)が作成され、その動作に関するテストと推論が大幅に簡素化されます。

最後に、このサーバーをバックエンドとして実行したいため、( application.propertiesで)リッスンするデフォルト以外のポートを指定します。

application.properties
server.port: 8081
security.sessions: NEVER

それがコンテンツ application.properties 全体である場合、アプリケーションは安全であり、ランダムなパスワードを持つ「user」というユーザーがアクセスできますが、起動時にコンソール(ログレベルINFO)に出力されます。「security.sessions」設定は、Spring Securityが認証トークンとしてCookieを受け入れますが、Cookieが既に存在しない限り作成しないことを意味します。

リソースサーバー

リソースサーバーは、既存のサンプルの1つから簡単に生成できます。セクションIIIの「Springセッション」リソースサーバーと同じです。分散セッションデータを取得するための単なる「/resource」エンドポイントSpring Sessionです。このサーバーにはリッスンするデフォルト以外のポートが必要であり、セッションで認証を検索できるようにするため、これが必要です( application.propertiesで)。

application.properties
server.port: 9000
security.sessions: NEVER

このチュートリアルの新しい機能であるメッセージリソースへの変更をPOSTする予定です。これは、バックエンドでCSRF保護が必要になることを意味し、Spring SecurityをAngularとうまく動作させるために通常のトリックを行う必要があります。

@Override
protected void configure(HttpSecurity http) throws Exception {
	http.csrf()
			.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}

覗いてみたいなら、完成したサンプルはgithubにあり(英語) ます。

ゲートウェイ

ゲートウェイの初期実装(おそらく動作する可能性のある最も単純なもの)では、空のSpring Boot Webアプリケーションを取得して @EnableZuulProxy アノテーションを追加するだけです。セクションIで見たように、それを行うにはいくつかの方法があり、その1つはSpring Initializr(英語) を使用してスケルトンプロジェクトを生成することです。さらに簡単なのは、Spring Cloud Initializr(英語) を使用することですが、これは同じことですが、Spring Cloud(英語) アプリケーション用です。セクションIと同じ一連のコマンドライン操作を使用します。

$ mkdir gateway && cd gateway
$ curl https://cloud-start.spring.io/starter.tgz -d style=web \
  -d style=security -d style=cloud-zuul -d name=gateway \
  -d style=redis | tar -xzvf -

次に、そのプロジェクト(デフォルトでは通常のMaven Javaプロジェクト)をお気に入りのIDEにインポートするか、コマンドラインでファイルと「mvn」を操作するだけです。そこから行きたい場合はgithub(英語) にバージョンがありますが、まだ必要のない追加機能がいくつかあります。

空白のInitializrアプリケーションから始めて、Spring Session依存関係を追加します(上記のUIのように)。ゲートウェイを実行する準備はできていますが、バックエンドサービスについてはまだ知らないため、application.yml でセットアップしてみましょう(上記のcurlを行った場合は application.properties から名前を変更します)。

application.yml
zuul:
  sensitive-headers:
  routes:
    ui:
      url: http://localhost:8081
   resource:
      url: http://localhost:9000
security:
  user:
    password:
      password
  sessions: ALWAYS

プロキシには2つのルートがあり、どちらも sensitive-headers プロパティを使用してCookieをダウンストリームで渡します。どちらもUIとリソースサーバーに1つずつあり、デフォルトのパスワードとセッション永続性戦略を設定しています(Spring Securityに常にセッションを作成するように伝えます)認証)。認証とセッションをゲートウェイで管理する必要があるため、この最後のビットは重要です。

稼働

現在、3つのポートで実行される3つのコンポーネントがあります。ブラウザをhttp://localhost:8080/ui/に向けると、HTTP Basicチャレンジを取得する必要があり、「ユーザー/パスワード」(ゲートウェイの資格情報)として認証できます。これを行うと、バックエンド経由でUIに挨拶が表示されます。プロキシを介してリソースサーバーに呼び出します。

一部の開発者ツールを使用すると、ブラウザーとバックエンド間の相互作用をブラウザーで確認できます(通常、F12はこれを開き、デフォルトでChromeで動作し、Firefoxでプラグインが必要になる場合があります)。概要は次のとおりです。

動詞パス状況レスポンス

GET

/ui/

401

認証のためのブラウザプロンプト

GET

/ui/

200

index.html

GET

/ui/*.js

200

Angularアセット

GET

/ui/js/hello.js

200

アプリケーションロジック

GET

/ui/user

200

認証

GET

/resource/

200

JSONグリーティング

ブラウザはホームページの読み込みを単一の対話として扱うため、401が表示されない場合があります。すべての要求はプロキシされます(管理用のアクチュエーターエンドポイントを超えて、ゲートウェイにはまだコンテンツがありません)。

ハラー、うまくいきました! 2つのバックエンドサーバーがあり、その1つはUIであり、それぞれが独立した機能を持ち、独立してテストすることができ、それらは制御し、認証を構成したセキュアゲートウェイと一緒に接続されます。ブラウザがバックエンドにアクセスできない場合は重要ではありません(実際、物理的なセキュリティをさらに制御できるため、おそらく利点です)。

ログインフォームの追加

セクションIの「基本」サンプルのように、ログインフォームをゲートウェイに追加できます。セクションIIからコードをコピーします。それを行うとき、ゲートウェイにいくつかの基本的なナビゲーション要素を追加することもできるため、ユーザーはプロキシのUIバックエンドへのパスを知る必要がありません。それでは、最初に静的アセットを「単一」UIからゲートウェイにコピーし、メッセージレンダリングを削除して、ログインフォームをホームページ( <app/> のどこか)に挿入します。

app.html
<div class="container" [hidden]="authenticated">
	<form role="form" (submit)="login()">
		<div class="form-group">
			<label for="username">Username:</label> <input type="text"
				class="form-control" id="username" name="username"
				[(ngModel)]="credentials.username" />
		</div>
		<div class="form-group">
			<label for="password">Password:</label> <input type="password"
				class="form-control" id="password" name="password"
				[(ngModel)]="credentials.password" />
		</div>
		<button type="submit" class="btn btn-primary">Submit</button>
	</form>
</div>

メッセージのレンダリングの代わりに、素敵な大きなナビゲーションボタンがあります。

index.html
<div class="container" [hidden]="!authenticated">
	<a class="btn btn-primary" href="/ui/">Go To User Interface</a>
</div>

githubでサンプルを見ている場合、「ログアウト」ボタンのある最小限のナビゲーションバーもあります。スクリーンショットのログインフォームは次のとおりです。

Login Page

ログインフォームをサポートするには、<form/>で宣言した login() 関数を実装するコンポーネントを含むTypeScriptが必要です。また、authenticated フラグを設定して、ユーザーが認証されているかどうかに応じてホームページのレンダリングが異なるようにする必要があります。例:

app.component.ts
include::src/app/app.component.ts

login() 関数の実装はセクションIIの実装に似ています。

この単純なアプリケーションにはコンポーネントが1つしかないため、self を使用して authenticated フラグを格納できます。

この強化されたゲートウェイを実行する場合、UIのURLを覚える必要はなく、ホームページを読み込んでリンクをたどることができます。認証済みユーザーのホームページは次のとおりです。

Home Page

バックエンドでのきめ細かいアクセス決定

これまでのアプリケーションは、機能的にはセクションIIIまたはセクションIVのものと非常によく似ていますが、専用のゲートウェイが追加されています。余分なレイヤーの利点はまだ明らかではないかもしれませんが、システムを少し拡張することで強調できます。ユーザーがメインUIでコンテンツを「管理」するために、そのゲートウェイを使用して別のバックエンドUIを公開し、この機能へのアクセスを特別なロールを持つユーザーに制限するとします。プロキシの背後に「管理」アプリケーションを追加すると、システムは次のようになります。

Components of the System

application.ymlのゲートウェイには、新しいコンポーネント(Admin)と新しいルートがあります。

application.yml
zuul:
  sensitive-headers:
  routes:
    ui:
      url: http://localhost:8081
    admin:
      url: http://localhost:8082
    resource:
      url: http://localhost:9000

「USER」ロールのユーザーが既存のUIを使用できることは、管理アプリケーションにアクセスするために「ADMIN」ロールが必要であるという事実と同様に、ゲートウェイボックス(緑色の文字)の上のブロック図に示されています。「ADMIN」ロールのアクセス決定は、ゲートウェイで適用できます。その場合、WebSecurityConfigurerAdapterに表示されるか、管理アプリケーション自体に適用できます(これを行う方法については後述します)。

最初に、新しいSpring Bootアプリケーションを作成するか、UIをコピーして編集します。UIアプリで変更する必要はありませんが、最初に名前を変更する必要があります。完成したアプリはGithubはこちら(英語) にあります。

管理アプリケーション内で、「READER」ロールと「WRITER」ロールを区別し、監査者であるユーザーに、メイン管理者ユーザーによる変更の表示を許可できるようにするとします。これはきめ細かいアクセス決定であり、ルールはバックエンドアプリケーションでのみ知られ、知られるべきです。ゲートウェイでは、ユーザーアカウントに必要なロールがあることを確認するだけでよく、この情報は利用可能ですが、ゲートウェイはそれを解釈する方法を知る必要はありません。ゲートウェイでは、ユーザーアカウントを作成して、サンプルアプリケーションを自己完結型に保ちます。

SecurityConfiguration.class
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

  @Autowired
  public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication()
      .withUser("user").password("password").roles("USER")
    .and()
      .withUser("admin").password("admin").roles("USER", "ADMIN", "READER", "WRITER")
    .and()
      .withUser("audit").password("audit").roles("USER", "ADMIN", "READER");
  }

}

「admin」ユーザーは3つの新しいロール(「ADMIN」、「READER」、「WRITER」)で拡張され、「ADMIN」アクセス権を持つ「audit」ユーザーも追加されましたが、「WRITER」アクセス権はありません。

本番システムでは、ユーザーアカウントデータは、Spring構成でハードコードされていないバックエンドデータベース(ほとんどの場合、ディレクトリサービス)で管理されます。このようなデータベースに接続するサンプルアプリケーションは、Spring Securityサンプル(GitHub) などのインターネットで簡単に見つけることができます。

アクセスの決定は、管理アプリケーションで行います。「ADMIN」ロール(このバックエンドにグローバルに必要)については、Spring Securityでそれを行います。

SecurityConfiguration.java
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

@Override
  protected void configure(HttpSecurity http) throws Exception {
    http
    ...
      .authorizeRequests()
        .antMatchers("/index.html", "/").permitAll()
        .antMatchers("/admin/**").hasRole("ADMIN")
        .anyRequest().authenticated()
    ...
  }

}

「READER」ロールと「WRITER」ロールの場合、アプリケーション自体が分割されます。アプリケーションはJavaScriptに実装されているため、ここでアクセスを決定する必要があります。これを行う1つの方法は、計算されたビューがルーターを介して埋め込まれたホームページを持つことです。

app.component.html
<div class="container">
	<h1>Admin</h1>
	<router-outlet></router-outlet>
</div>

ルートは、コンポーネントがロードされるときに計算されます。

app.component.ts
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {

  user: {};

  constructor(private app: AppService, private http: HttpClient, private router: Router) {
    app.authenticate(response => {
      this.user = response;
      this.message();
    });
  }

  logout() {
    this.http.post('logout', {}).subscribe(function() {
        this.app.authenticated = false;
        this.router.navigateByUrl('/login');
    });
  }

  message() {
    if (!this.app.authenticated) {
      this.router.navigate(['/unauthenticated']);
    } else {
      if (this.app.writer) {
        this.router.navigate(['/write']);
      } else {
        this.router.navigate(['/read']);
      }
    }
  }
...
}

アプリケーションが最初に行うことは、ユーザーが認証されているかどうかを確認し、ユーザーデータを見てルートを計算することです。ルートはメインモジュールで宣言されます。

app.module.ts
const routes: Routes = [
  { path: '', pathMatch: 'full', redirectTo: 'read'},
  { path: 'read', component: ReadComponent},
  { path: 'write', component: WriteComponent},
  { path: 'unauthenticated', component: UnauthenticatedComponent},
  { path: 'changes', component: ChangesComponent}
];

これらの各コンポーネント(各ルートに1つ)は個別に実装する必要があります。例として ReadComponent を次に示します。

read.component.ts
import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Component({
  templateUrl: './read.component.html'
})
export class ReadComponent {

  greeting = {};

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

}
read.component.html
<h1>Greeting</h1>
<div>
	<p>The ID is {{greeting.id}}</p>
	<p>The content is {{greeting.content}}</p>
</div>

WriteComponent も同様ですが、バックエンドでメッセージを変更する形式があります。

write.component.ts
import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Component({
  templateUrl: './write.component.html'
})
export class WriteComponent {

  greeting = {};

  constructor(private http: HttpClient) {
    this.http.get('http://localhost:9000').subscribe(data => this.greeting = data);
  }

  update() {
    this.http.post('/resource', {content: this.greeting['content']}).subscribe(response => {
      this.greeting = response;
    });
  }

}
write.component.html
<form (submit)="update()">
	<p>The ID is {{greeting.id}}</p>
	<div class="form-group">
		<label for="username">Content:</label> <input type="text"
			class="form-control" id="content" name="content" [(ngModel)]="greeting.content"/>
	</div>
	<button type="submit" class="btn btn-primary">Submit</button>
</form>

AppService は、ルートを計算するためのデータも提供する必要があるため、authenticate() 関数では次のように表示されます。

app.service.ts
        http.get('/user').subscribe(function(response) {
            var user = response.json();
            if (user.name) {
                self.authenticated = true;
                self.writer = user.roles && user.roles.indexOf("ROLE_WRITER")>0;
            } else {
                self.authenticated = false;
                self.writer = false;
            }
            callback && callback(response);
        })

バックエンドでこの機能をサポートするには、/user エンドポイントが必要です。メインアプリケーションクラスで:

AdminApplication.java
@SpringBootApplication
@RestController
public class AdminApplication {

  @RequestMapping("/user")
  public Map<String, Object> user(Principal user) {
    Map<String, Object> map = new LinkedHashMap<String, Object>();
    map.put("name", user.getName());
    map.put("roles", AuthorityUtils.authorityListToSet(((Authentication) user)
        .getAuthorities()));
    return map;
  }

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

}
ロール名は「/user」エンドポイントから返され、「ROLE_」プレフィックスが付いているため、他の種類の機関と区別できます(Spring Securityのことです)。JavaScriptでは "ROLE_" プレフィックスが必要ですが、Spring Security構成では必要ありません。Spring Security構成では、"ロール"が操作の焦点であることはメソッド名から明らかです。

管理UIをサポートするためのゲートウェイの変更

ロールを使用してGatewayでもアクセスを決定します(したがって、管理UIへのリンクを条件付きで表示できます)。Gatewayの「/user」エンドポイントにも「ロール」を追加する必要があります。それができたら、JavaScriptを追加して、現在のユーザーが「ADMIN」であることを示すフラグを設定できます。 authenticated() 機能では:

app.component.ts
this.http.get('user', {headers: headers}).subscribe(data => {
  this.authenticated = data && data['name'];
  this.user = this.authenticated ? data['name'] : '';
  this.admin = this.authenticated && data['roles'] && data['roles'].indexOf('ROLE_ADMIN') > -1;
});

また、ユーザーがログアウトしたときに admin フラグを false にリセットする必要があります。

app.component.ts
this.logout = function() {
    http.post('logout', {}).subscribe(function() {
        self.authenticated = false;
        self.admin = false;
    });
}

その後、HTMLで条件付きで新しいリンクを表示できます。

app.component.html
<div class="container" [hidden]="!authenticated">
	<a class="btn btn-primary" href="/ui/">Go To User Interface</a>
</div>
<br />
<div class="container" [hidden]="!authenticated || !admin">
	<a class="btn btn-primary" href="/admin/">Go To Admin Interface</a>
</div>

すべてのアプリを実行し、http://localhost:8080に移動して結果を確認します。すべてが正常に機能し、現在認証されているユーザーに応じてUIが変更されます。

私達、どうしてここに?

これで、2つの独立したユーザーインターフェースとバックエンドリソースサーバーを備えたすてきな小さなシステムができました。これらはすべて、ゲートウェイで同じ認証によって保護されています。ゲートウェイがマイクロプロキシとして機能するという事実により、バックエンドセキュリティの関心事の実装が非常に簡単になり、ビジネス上の懸念に自由に集中できます。Spring Sessionの使用により、(再び)膨大な量の手間と潜在的なエラーが回避されました。

強力な機能は、バックエンドが任意の種類の認証を独自に持つことができることです(たとえば、物理アドレスと一連のローカル認証情報がわかっている場合は、UIに直接アクセスできます)。ゲートウェイは、ユーザーを認証し、バックエンドのアクセスルールを満たすメタデータをユーザーに割り当てることができる限り、完全に無関係な一連の制約を課します。これは、バックエンドコンポーネントを個別に開発およびテストできる優れた設計です。必要に応じて、ゲートウェイでの認証のために外部OAuth2サーバー(セクションVなど、またはまったく異なるもの)に戻ることができ、バックエンドに触れる必要はありません。

このアーキテクチャのボーナス機能(認証を制御する単一のゲートウェイ、およびすべてのコンポーネントにわたる共有セッショントークン)は、セクションVで実装が困難であると判断した機能である「シングルログアウト」が無料で提供されることです。より正確には、シングルログアウトのユーザーエクスペリエンスに対する特定のアプローチが完成したシステムで自動的に使用可能になります。ユーザーがUI(ゲートウェイ、UIバックエンド、または管理バックエンド)からログアウトすると、すべてのログアウトその他、個々のUIが「ログアウト」機能を同じ方法で実装したと仮定します(セッションを無効にします)。

ありがとう:このシリーズの開発を手伝ってくれたすべての人、特にセクションとソースコードを注意深くレビューしてくれたRob Winch(英語) トーステンスペート(英語) に感謝します。セクションIが公開されて以来、あまり変更されていませんが、他のすべての部分は読者からのコメントやインサイトに応じて進化しています。セクションを読んで議論に参加してくれた皆さんにも感謝します。

Angularアプリケーションのテスト

このセクションでは、「シングルページアプリケーション」でSpring SecurityAngular(英語) と使用する方法について引き続き説明します。ここでは、Angularテストフレームワークを使用して、クライアント側コードの単体テストを作成および実行する方法を示します。アプリケーションの基本的な構成要素に追いつくか、最初のセクションを読んでゼロからビルドするか、Githubのソースコードに直接(英語) 進むことができます(パートIと同じソースコードですが、テストが追加されています)。このセクションには、実際にはSpringまたはSpring Securityを使用したコードはほとんどありませんが、通常のAngularコミュニティリソースでは簡単に見つけられない方法でクライアント側のテストを扱います。Springユーザー。

注意:このセクションでサンプルアプリケーションを使用している場合は、ブラウザのキャッシュのCookieとHTTP Basic資格情報を必ずクリアしてください。Chromeでは、単一サーバーでこれを行う最良の方法は、新しいシークレットウィンドウを開くことです。

仕様を書く

「基本」アプリケーションの「アプリ」コンポーネントは非常にシンプルなので、徹底的にテストするのにそれほど時間はかかりません。コードのリマインダーは次のとおりです。

app.component.ts
include::basic/src/app/app.component.ts

直面する主な課題は、テストで http オブジェクトを提供することです。そのため、コンポーネントでの http オブジェクトの使用メソッドについてアサーションを作成できます。実際、その課題に直面する前であっても、コンポーネントインスタンスを作成できる必要があります。そのため、ロード時に何が起こるかをテストできます。以下にそのメソッドを示します。

ng new から作成されたアプリのAngularビルドには、すでに仕様とそれを実行するための構成があります。生成された仕様は「src / app」にあり、次のように始まります。

app.component.ts
import { TestBed, async } from '@angular/core/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [],
      declarations: [
        AppComponent
      ]
    }).compileComponents();
  }));
  it('should create the app', async(() => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.debugElement.componentInstance;
    expect(app).toBeTruthy();
  }));
  ...
}

この非常に基本的なテストスイートには、次の重要な要素があります。

  1. 関数を使用して、テスト対象の describe() (この場合は「AppComponent」)。

  2. その関数内で、Angularコンポーネントをロードする beforeEach() コールバックを提供します。

  3. 振る舞いは it()の呼び出しを通じて表現されます。ここでは、期待が何であるかを言葉で表明し、アサーションを行う関数を提供します。

  4. テスト環境は、他の何かが発生する前に初期化されます。これは、ほとんどのAngularアプリの定型です。

ここでのテスト関数は非常に単純なため、実際にはコンポーネントが存在することをアサートするだけなので、それが失敗するとテストは失敗します。

単体テストの改善: HTTPバックエンドのモック

仕様を製品グレードに改善するには、コントローラーのロード時に何が起こるかについて実際にアサートする必要があります。 http.get() を呼び出すため、単体テストのためだけにアプリケーション全体を実行する必要がないように、その呼び出しをモックする必要があります。そのためには、Angular HttpClientTestingModuleを使用します。

app.component.spec
Unresolved directive in testing.adoc - include::basic/src/app/app.component.spec[indent=0]

ここでの新しい部分は次のとおりです。

  • beforeEach()TestBed のインポートとしての HttpClientTestingModule の宣言。

  • テスト関数では、コンポーネントを作成する前にバックエンドに期待値を設定し、「resource /」への呼び出しを期待するように伝え、応答をどうするかを伝えます。

仕様の実行

「テストを実行する」コードは、プロジェクトのセットアップ時に作成された便利なスクリプトを使用して ./ng test (または ./ng build)を実行できます。これはMavenライフサイクルの一部としても実行されるため、./mvnw install はテストを実行する良い方法でもあります。CIビルドで何が起こるかです。

エンドツーエンドのテスト

Angularには、ブラウザーと生成されたJavaScriptを使用した「エンドツーエンドテスト」用にセットアップされた標準ビルドもあります。これらは、最上位の e2e ディレクトリに「仕様」として書き込まれます。このチュートリアルのすべてのサンプルには、Mavenライフサイクルで実行される非常に単純なエンドツーエンドテストが含まれています(したがって、「ui」アプリで mvn install を実行すると、ブラウザーウィンドウがポップアップ表示されます)。

結論

Javascriptの単体テストを実行できることは、最新のWebアプリケーションでは重要であり、このシリーズではこれまで無視(または回避)したトピックです。今回の記事では、テストを記述する方法、開発時にテストを実行する方法、さらに重要なこととして、継続的インテグレーション設定の基本的な要素を導入しました。取ったアプローチはすべての人に適しているわけではないため、別の方法でやるのを気にしないでください。ここで行った方法は、おそらく従来のJavaエンタープライズ開発者にとって快適であり、既存のツールやプロセスとうまく統合できるため、そのカテゴリにいるなら、出発点として役立つと思います。AngularおよびJasmineを使用したテストのその他の例は、インターネット上の多くの場所で見つけることができますが、最初のコールポイントはこのシリーズの「単一」サンプル(GitHub) である可能性があります。このチュートリアルの「基本」サンプル用に記述する必要があるコード。

OAuth2クライアントアプリケーションからのログアウト

このセクションでは、「シングルページアプリケーション」でSpring SecurityAngular(英語) と使用する方法について引き続き説明します。ここでは、OAuth2サンプルを取得し、別のログアウトエクスペリエンスを追加する方法を示します。OAuth2シングルサインオンを実装する多くの人々は、「きれいに」ログアウトする方法を解決するためのパズルがあることに気付いていますか?それがパズルである理由は、それを行うための単一の正しい方法がないことであり、選択する解決策は、探しているユーザーエクスペリエンスと、引き受けたい複雑さの量によって決定されます。複雑さの理由は、システム内に潜在的に複数のブラウザセッションがあり、すべてが異なるバックエンドサーバーであるという事実に起因するため、ユーザーがそのうちの1つからログアウトすると、他のユーザーはどうなりますか?これはチュートリアルの9番目のセクションであり、アプリケーションの基本的な構成要素に追いつくか、最初のセクションを読んでゼロからビルドするか、Githubのソースコードに直接(英語) 進むことができます。

ログアウトパターン

このチュートリアルでの oauth2 サンプルのログアウトのユーザーエクスペリエンスは、authserverからではなく、UIアプリからログアウトすることです。そのため、UIアプリに再度ログインしても、autheserverは資格情報を求めません。これは、autheserverが外部にある場合、完全に予期され、正常であり、望ましいです-Googleおよびその他の外部認証サーバープロバイダーは、信頼されていないアプリケーションからサーバーからログアウトすることを望んでおらず、許可もしません-しかし、認証サーバーが実際の場合、最高のユーザーエクスペリエンスではありませんUIと同じシステムの一部。

大まかに言えば、OAuth2クライアントとして認証されたUIアプリからログアウトするための3つのパターンがあります。

  1. 外部認証サーバー(EA、元のサンプル)。ユーザーは、認証サーバーをサードパーティーとして認識します(たとえば、FacebookまたはGoogleを使用して認証します)。アプリセッションの終了時に認証サーバーからログアウトする必要はありません。すべての助成金の承認が必要です。このチュートリアルの oauth2 (および oauth2-vanilla)サンプルは、このパターンを実装しています。

  2. ゲートウェイおよび内部認証サーバー(GIA)。ログアウトする必要があるのは2つのアプリのみであり、それらはユーザーが認識する同じシステムの一部です。通常、すべての助成金を自動承認する必要があります。

  3. シングルログアウト(SL)。1つの認証サーバーと複数のUIアプリはすべて独自の認証を備えており、ユーザーが1つからログアウトすると、すべてのアプリがそれに追従する必要があります。ネットワークパーティションとサーバーの障害のために、単純な実装で失敗する可能性が高い-基本的には、グローバルに一貫したストレージが必要です。

外部認証サーバーを使用している場合でも、認証を制御し、アクセス制御の内部レイヤー(認証サーバーがサポートしていないスコープやロールなど)を追加したい場合があります。次に、認証にEAを使用することをお勧めしますが、必要な追加の詳細をトークンに追加できる内部認証サーバーを用意します。この他のOAuth2チュートリアル(GitHub) auth-server サンプルは、非常に簡単な方法でそれを行う方法を示しています。その後、内部認証サーバーを含むシステムにGIAまたはSLパターンを適用できます。

EAが必要ない場合のオプションは次のとおりです。

  • ブラウザークライアントのauthserverおよびUIアプリからログアウトします。シンプルなアプローチであり、いくつかの注意深いCRSFおよびCORS構成で動作します。SLなし

  • トークンが利用可能になり次第、authserverからログアウトします。authserverのセッションCookieがないため、トークンを取得するUIでの実装は困難です。Spring OAuth(GitHub) には、興味深いアプローチを示す機能要求(GitHub) があります。認証コードが生成されるとすぐに、認証サーバーのセッションを無効にします。Githubの課題には、セッションの無効化を実装する側面が含まれていますが、HandlerInterceptorとして行う方が簡単です。SLなし

  • UIと同じゲートウェイを介して認証サーバーをプロキシし、1つのCookieでシステム全体の状態を管理するのに十分であることを望みます。共有セッションが存在しない限り機能しません。共有セッションは、オブジェクトをある程度無効にします(それ以外の場合、authserverのセッションストレージはありません)。セッションがすべてのアプリ間で共有される場合にのみSL。

  • ゲートウェイのCookieリレー。認証の真のソースとしてゲートウェイを使用しています。認証サーバーは、ブラウザーではなくCookieを管理するため、必要なすべての状態を保持しています。ブラウザに複数のサーバーからのCookieが含まれることはありません。SLなし

  • トークンをグローバル認証として使用し、ユーザーがUIアプリからログアウトするときにトークンを無効にします。欠点:クライアントアプリによってトークンを無効にする必要がありますが、これは実際には意図されたものではありません。SLは可能ですが、通常の制約が適用されます。

  • authserverで(ユーザートークンに加えて)グローバルセッショントークンを作成および管理します。これはOpenId接続(英語) が採用したアプローチであり、SLにいくつかのオプションを提供しますが、いくつかの追加の機械が必要になります。通常の分散システムの制限から影響を受けるオプションはありません。ネットワークとアプリケーションノードが安定していない場合、必要に応じてすべての参加者間でログアウト信号が共有されるという保証はありません。ログアウト仕様はすべてドラフト形式のままであり、仕様へのリンクは次のとおりです:セッション管理(英語) フロントチャンネルログアウト(英語) 、およびバックチャンネルログアウト(英語)

SLが困難または不可能な場合、すべてのUIを単一のゲートウェイの背後に配置する方がよい場合があることに注意してください。次に、より簡単なGIAを使用して、不動産全体からのログアウトを制御できます。

GIAパターンにうまく当てはまる最も簡単な2つのオプションは、チュートリアルサンプルで次のように実装できます( oauth2 サンプルを取得して、そこから作業します)。

ブラウザーからの両方のサーバーのログアウト

UIアプリがログアウトされるとすぐにauthserverからログアウトするコードをブラウザクライアントに追加するのは非常に簡単です。例:

logout() {
    this.http.post('logout', {}).finally(() => {
        self.authenticated = false;
        this.http.post('http://localhost:9999/uaa/logout', {}, {withCredentials:true})
            .subscribe(() => {
                console.log('Logged out');
        });
    }).subscribe();
};

このサンプルでは、authserverログアウトエンドポイントURLをJavaScriptにハードコードしましたが、必要に応じて外部化するのは簡単です。セッションCookieも一緒に送信するため、authserverに直接POSTする必要があります。XHRリクエストは、withCredentials:trueを明確に要求した場合にのみ、Cookieが添付されたブラウザから送信されます。

逆に、リクエストは別のドメインから送信されるため、サーバーではCORS構成が必要です。例: WebSecurityConfigurerAdapter

@Override
protected void configure(HttpSecurity http) throws Exception {
  http
	.requestMatchers().antMatchers("/login", "/logout", "/oauth/authorize", "/oauth/confirm_access")
  .and()
    .cors().configurationSource(configurationSource())
    ...
}

private CorsConfigurationSource configurationSource() {
  UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
  CorsConfiguration config = new CorsConfiguration();
  config.addAllowedOrigin("*");
  config.setAllowCredentials(true);
  config.addAllowedHeader("X-Requested-With");
  config.addAllowedHeader("Content-Type");
  config.addAllowedMethod(HttpMethod.POST);
  source.registerCorsConfiguration("/logout", config);
  return source;
}

「/logout」エンドポイントには特別な扱いが与えられています。任意のオリジンからの呼び出しが許可され、資格情報(Cookieなど)の送信が明示的に許可されます。許可されるヘッダーは、Angularがサンプルアプリで送信するヘッダーのみです。

Angularはクロスドメインリクエストで X-XSRF-TOKEN ヘッダーを送信しないため、CORS設定に加えて、ログアウトエンドポイントのCSRFを無効にする必要があります。authserverはこれまでCSRF設定を必要としませんでしたが、ログアウトエンドポイントの無視を簡単に追加できます。

@Override
protected void configure(HttpSecurity http) throws Exception {
  http
    .csrf()
      .ignoringAntMatchers("/logout/**")
    ...
}
CSRF保護をドロップすることは実際にはお勧めできませんが、この制限されたユースケースでは許容する準備ができているかもしれません。

UIアプリクライアントとauthserverの2つの簡単な変更により、UIアプリからログアウトすると、再度ログインすると、常にパスワードの入力が求められることがわかります。

もう1つの便利な変更点は、OAuth2クライアントを自動承認するように設定することです。これにより、ユーザーはトークンの付与を承認する必要がなくなります。これは、ユーザーが別のシステムとして認識しない内部認証サーバーでは一般的です。 AuthorizationServerConfigurerAdapter では、クライアントの初期化時にフラグが必要です。

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
  clients.inMemory().withClient("acme")
    ...
  .autoApprove(true);
}

認証サーバーのセッションを無効化

ログアウトエンドポイントでCSRF保護を放棄したくない場合は、他の簡単なアプローチを試すことができます。これは、トークンが許可されるとすぐに(実際は認証コードとなるとすぐに、認証サーバーのユーザーセッションを無効にする生成されます)。これも非常に簡単に実装できます。oauth2 サンプルから始めて、HandlerInterceptor をOAuth2エンドポイントに追加するだけです。

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints)
    throws Exception {
  ...
  endpoints.addInterceptor(new HandlerInterceptorAdapter() {
    @Override
    public void postHandle(HttpServletRequest request,
        HttpServletResponse response, Object handler,
        ModelAndView modelAndView) throws Exception {
      if (modelAndView != null
          && modelAndView.getView() instanceof RedirectView) {
        RedirectView redirect = (RedirectView) modelAndView.getView();
        String url = redirect.getUrl();
        if (url.contains("code=") || url.contains("error=")) {
          HttpSession session = request.getSession(false);
          if (session != null) {
            session.invalidate();
          }
        }
      }
    }
  });
}

このインターセプターは RedirectViewを探します。これは、ユーザーがクライアントアプリにリダイレクトされていることを示す信号であり、場所に認証コードまたはエラーが含まれているかどうかを確認します。暗黙的な許可も使用している場合は、「token =」を追加できます。

この簡単な変更により、認証するとすぐに、authserverのセッションはすでに停止しているため、クライアントからセッションを試行および管理する必要はありません。UIアプリからログアウトしてから再度ログインすると、認証サーバーはユーザーを認識せず、資格情報の入力を求めます。このパターンは、このチュートリアルのソースコード(GitHub) oauth2-logout サンプルによって実装されたものです。このアプローチの欠点は、本当のシングルサインオンがもはやないことです。システムの一部である他のアプリは、authserverセッションが停止していることを発見し、再度認証を要求する必要があります。複数のアプリがある場合の優れたユーザーエクスペリエンス。

結論

このセクションでは、OAuth2クライアントアプリケーションからログアウトするためのいくつかの異なるパターンを実装する方法を見てきました(開始点としてチュートリアルのセクション5のアプリケーションを使用)。他のパターンのオプションについても説明しました。これらのオプションはすべてを網羅しているわけではありませんが、トレードオフの適切なアイデアと、ユースケースに最適なソリューションを検討するためのいくつかのツールを提供する必要があります。このセクションにはJavaScriptの行が2、3行しかなく、Angularに固有のものではなかったため(XHRリクエストにフラグを追加します)、このガイドのサンプルアプリの狭い範囲を超えてすべてのレッスンとパターンを適用できます。繰り返し発生するテーマは、複数のUIアプリがあり、単一の認証サーバーに何らかの欠陥がある傾向があるシングルログアウト(SL)へのすべてのアプローチです:できることは、ユーザーの不快感を最小限に抑えるアプローチを選択することです。内部authserverと多くのコンポーネントで構成されるシステムがある場合、単一のシステムのようにユーザーに感じる唯一のアーキテクチャは、すべてのユーザーインタラクションのゲートウェイである可能性があります。

新しいガイドを作成したり、既存のガイドに貢献したいですか? 投稿ガイドラインを参照してください(GitHub)

すべてのガイドは、コード用のASLv2ライセンス、およびドキュメント用のAttribution、NoDerivativesクリエイティブコモンズライセンス(英語) でリリースされています。