Angular6SPAでJWT認証を行う方法

公開: 2022-03-11

今日は、JSON Webトークン(JWT)認証をAngular 6(またはそれ以降)のシングルページアプリケーション(SPA)に統合するのがいかに簡単かを見ていきます。 少し背景から始めましょう。

JSON Web Tokenとは何ですか、なぜそれらを使用するのですか?

ここでの最も簡単で最も簡潔な答えは、それらが便利で、コンパクトで、安全であるということです。 それらの主張を詳細に見てみましょう:

  1. 便利:ログイン後にバックエンドへの認証にJWTを使用するには、1つのHTTPヘッダーを設定する必要があります。このタスクは、後で説明するように、関数またはサブクラス化によって簡単に自動化できます。
  2. コンパクト:トークンは、いくつかのヘッダーフィールドと、必要に応じてペイロードを含む、base64でエンコードされた文字列です。 署名されている場合でも、JWTの合計は通常200バイト未満です。
  3. 安全性:必須ではありませんが、JWTの優れたセキュリティ機能は、RSA公開/秘密鍵ペア暗号化または共有秘密を使用したHMAC暗号化のいずれかを使用してトークンに署名できることです。 これにより、トークンの出所と有効性が保証されます。

つまり、ユーザーを認証し、データ構造を解析したり独自の暗号化を実装したりすることなく、APIエンドポイントへの呼び出しを検証するための安全で効率的な方法があります。

アプリケーション理論

JWT認証の一般的なデータフローとフロントエンドシステムとバックエンドシステム間の使用

したがって、少し背景を説明して、これが実際のアプリケーションでどのように機能するかを詳しく見ていきましょう。 この例では、APIをホストするNode.jsサーバーがあり、Angular6を使用してSPAtodoリストを開発していると仮定します。このAPI構造も使用してみましょう。

  • /authPOST (JWTを認証して受信するためのユーザー名とパスワードを投稿します)
  • /todosGET (ユーザーのtodoリスト項目のリストを返します)
  • /todos/{id}GET (特定のtodoリストアイテムを返す)
  • /usersGET (ユーザーのリストを返します)

この単純なアプリケーションの作成については後ほど説明しますが、ここでは、理論上の相互作用に集中しましょう。 ユーザーがユーザー名とパスワードを入力できるシンプルなログインページがあります。 フォームが送信されると、その情報が/authエンドポイントに送信されます。 その後、ノードサーバーは適切な方法(データベースルックアップ、別のWebサービスへのクエリなど)でユーザーを認証できますが、最終的にエンドポイントはJWTを返す必要があります。

この例のJWTには、いくつかの予約済みクレームといくつかのプライベートクレームが含まれます。 予約済みのクレームは、認証に一般的に使用されるJWTが推奨するキーと値のペアですが、プライベートのクレームは、アプリにのみ適用されるキーと値のペアです。

予約済みクレーム

  • iss :このトークンの発行者。 通常はサーバーのFQDNですが、クライアントアプリケーションがそれを予期していることを知っている限り、何でもかまいません。
  • exp :このトークンの有効期限。 これは、1970年1月1日GMT(UNIX時間)の午前0時からの秒数です。
  • nbf :タイムスタンプの前は無効です。 頻繁には使用されませんが、有効期間の下限を示します。 expと同じ形式。

プライベートクレーム

  • uid :ログインしているユーザーのユーザーID。
  • role :ログインしたユーザーに割り当てられた役割。

私たちの情報はbase64でエンコードされ、共有キーtodo-app-super-shared-secretを使用してHMACを使用して署名されます。 以下は、JWTがどのように見えるかの例です。

 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ0b2RvYXBpIiwibmJmIjoxNDk4MTE3NjQyLCJleHAiOjE0OTgxMjEyNDIsInVpZCI6MSwicm9sZSI6ImFkbWluIn0.ZDz_1vcIlnZz64nSM28yA1s-4c_iw3Z2ZtP-SgcYRPQ

この文字列は、有効なログインがあることを確認し、接続されているユーザーを認識し、ユーザーがどの役割を持っているかを知るために必要なすべてです。

ほとんどのライブラリとアプリケーションは、簡単に取得できるようにこのJWTをlocalStorageまたはsessionStorageに保存しますが、これは単なる一般的な方法です。 トークンを使用して行うことは、将来のAPI呼び出しにトークンを提供できる限り、あなた次第です。

これで、SPAが保護されたAPIエンドポイントのいずれかを呼び出したい場合は常に、 Authorizationヘッダーのトークンを送信する必要があります。

 Authorization: Bearer {JWT Token}

:繰り返しになりますが、これは単に一般的な方法です。 JWTは、サーバーに自分自身を送信するための特定の方法を規定していません。 URLに追加したり、Cookieで送信したりすることもできます。

サーバーはJWTを受信すると、それをデコードし、HMAC共有シークレットを使用して整合性を確保し、 expフィールドとnbfフィールドを使用して有効期限を確認できます。 また、 issフィールドを使用して、このJWTの元の発行者であることを確認することもできます。

サーバーがトークンの有効性に満足すると、JWT内に保存されている情報を使用できます。 たとえば、含まれているuidは、リクエストを行っているユーザーのIDを提供します。 この特定の例では、 roleフィールドも含めました。これにより、ユーザーが特定のエンドポイントにアクセスできるかどうかを決定できます。 (この情報を信頼するか、データベースルックアップを実行するかは、必要なセキュリティのレベルによって異なります。)

 function getTodos(jwtString) { var token = JWTDecode(jwtstring); if( Date.now() < token.nbf*1000) { throw new Error('Token not yet valid'); } if( Date.now() > token.exp*1000) { throw new Error('Token has expired'); } if( token.iss != 'todoapi') { throw new Error('Token not issued here'); } var userID = token.uid; var todos = loadUserTodosFromDB(userID); return JSON.stringify(todos); }

シンプルなTodoアプリを作ろう

従うには、Node.jsの最新バージョン(6.x以降)、npm(3.x以降)、およびangular-cliがインストールされている必要があります。 npmを含むNode.jsをインストールする必要がある場合は、こちらの手順に従ってください。 その後angular-clinpm (またはインストールしている場合はyarn )を使用してインストールできます。

 # installation using npm npm install -g @angular/cli # installation using yarn yarn global add @angular/cli

ここで使用するAngular6ボイラープレートについては詳しく説明しませんが、次のステップでは、アプリにJWT認証を追加する簡単さを説明するために、小さなtodoアプリケーションを保持するGithubリポジトリを作成しました。 以下を使用してクローンを作成するだけです。

 git clone https://github.com/sschocke/angular-jwt-todo.git cd angular-jwt-todo git checkout pre-jwt

git checkout pre-jwtコマンドは、JWTが実装されていない名前付きリリースに切り替わります。

serverclientという名前のフォルダが2つあるはずです。 サーバーは、基本的なAPIをホストするノードAPIサーバーです。 クライアントはAngular6アプリです。

NodeAPIサーバー

開始するには、依存関係をインストールしてAPIサーバーを起動します。

 cd server # installation using npm npm install # or installation using yarn yarn node app.js

これらのリンクをたどって、データのJSON表現を取得できるはずです。 今のところ、認証が完了するまで、 userID=1のタスクを返すように/todosエンドポイントをハードコーディングしました。

  • http:// localhost:4000:ノードサーバーが実行されているかどうかを確認するためのテストページ
  • http:// localhost:4000 / api / users:システム上のユーザーのリストを返します
  • http:// localhost:4000 / api / todos: userID=1のタスクのリストを返します

Angularアプリ

クライアントアプリを使い始めるには、依存関係をインストールして開発サーバーを起動する必要もあります。

 cd client # using npm npm install npm start # using yarn yarn yarn start

:回線速度によっては、すべての依存関係をダウンロードするのに時間がかかる場合があります。

すべてが順調に進んでいる場合は、http:// localhost:4200に移動すると次のように表示されます。

JWTに対応していないバージョンのAngularTodoListアプリ。

JWTを介した認証の追加

JWT認証のサポートを追加するために、利用可能ないくつかの標準ライブラリを利用して、認証を簡素化します。 もちろん、これらの便利さを放棄してすべてを自分で実装することはできますが、それはここでの範囲を超えています。

まず、クライアント側にライブラリをインストールしましょう。 これは、クラウドベースの認証をWebサイトに追加できるライブラリであるAuth0によって開発および保守されています。 ライブラリ自体を利用するために、ライブラリのサービスを使用する必要はありません。

 cd client # installation using npm npm install @auth0/angular-jwt # installation using yarn yarn add @auth0/angular-jwt

コードについてはすぐに説明しますが、その間にサーバー側もセットアップしてみましょう。 body-parserjsonwebtokenexpress-jwtライブラリを使用して、NodeにJSONPOSTボディとJWTを認識させます。

 cd server # installation using npm npm install body-parser jsonwebtoken express-jwt # installation using yarn yarn add body-parser jsonwebtoken express-jwt

認証用のAPIエンドポイント

まず、トークンを与える前にユーザーを認証する方法が必要です。 簡単なデモでは、ハードコードされたユーザー名とパスワードを使用して固定認証エンドポイントを設定します。 これは、アプリケーションの必要に応じて単純または複雑にすることができます。 重要なのは、JWTを送り返すことです。

server/app.jsで、次のように他のrequire行の下にエントリを追加します。

 const bodyParser = require('body-parser'); const jwt = require('jsonwebtoken'); const expressJwt = require('express-jwt');

また、次のとおりです。

 app.use(bodyParser.json()); app.post('/api/auth', function(req, res) { const body = req.body; const user = USERS.find(user => user.username == body.username); if(!user || body.password != 'todo') return res.sendStatus(401); var token = jwt.sign({userID: user.id}, 'todo-app-super-shared-secret', {expiresIn: '2h'}); res.send({token}); });

これは主に基本的なJavaScriptコードです。 /authエンドポイントに渡されたJSON本文を取得し、そのユーザー名に一致するユーザーを見つけ、ユーザーとパスワードが一致することを確認し、一致しない場合は401 Unauthorizedエラーを返します。

重要な部分はトークンの生成であり、それを3つのパラメーターで分類します。 signの構文は次のとおりですjwt.sign(payload, secretOrPrivateKey, [options, callback]) 、ここで:

  • payloadは、トークン内でエンコードするキーと値のペアのオブジェクトリテラルです。 この情報は、復号化キーを持っている人なら誰でもトークンからデコードできます。 この例では、 user.idをエンコードして、認証のためにバックエンドでトークンを再度受信したときに、処理しているユーザーがわかるようにします。
  • secretOrPrivateKeyは、HMAC暗号化共有秘密鍵(簡単にするためにアプリで使用したもの)またはRSA/ECDSA暗号化秘密鍵のいずれかです。
  • optionsは、キーと値のペアの形式でエンコーダーに渡すことができるさまざまなオプションを表します。 通常、トークンが永久に有効にならないように、少なくともexpiresInexp予約済みクレームになる)とissueriss予約済みクレーム)を指定し、サーバーは実際にトークンを最初に発行したことを確認できます。
  • callbackは、トークンのエンコードを非同期で処理したい場合に、エンコードが完了した後に呼び出す関数です。

options詳細と、共有秘密鍵の代わりに公開鍵暗号を使用する方法についても読むことができます。)

Angular6JWT統合

Angular 6をJWTで機能させるには、 angular-jwtを使用すると非常に簡単です。 client/src/app/app.modules.tsに以下を追加するだけです:

 import { JwtModule } from '@auth0/angular-jwt'; // ... export function tokenGetter() { return localStorage.getItem('access_token'); } @NgModule({ // ... imports: [ BrowserModule, AppRoutingModule, HttpClientModule, FormsModule, // Add this import here JwtModule.forRoot({ config: { tokenGetter: tokenGetter, whitelistedDomains: ['localhost:4000'], blacklistedRoutes: ['localhost:4000/api/auth'] } }) ], // ... }

基本的に必要なのはそれだけです。 もちろん、初期認証を行うために追加するコードがいくつかありますが、 angular-jwtライブラリがすべてのHTTPリクエストと一緒にトークンを送信します。

  • tokenGetter()関数はそれが言うことを正確に実行しますが、それをどのように実装するかは完全にあなた次第です。 localStorageに保存したトークンを返すことを選択しました。 もちろん、 JSON Webトークンでエンコードされた文字列を返す限り、他​​のメソッドを自由に提供できます。
  • whiteListedDomainsオプションが存在するため、JWTが送信されるドメインを制限できるため、パブリックAPIはJWTも受信しません。
  • blackListedRoutesオプションを使用すると、ホワイトリストに登録されたドメインにある場合でもJWTを受信しない特定のルートを指定できます。 たとえば、認証エンドポイントは意味がないため、それを受信する必要はありません。トークンは通常、呼び出されたときにnullになります。

すべてを連携させる

この時点で、APIの/authエンドポイントを使用して特定のユーザーのJWTを生成する方法があり、Angularで配管を行ってすべてのHTTPリクエストでJWTを送信します。 すばらしいですが、ユーザーにとってまったく何も変わっていないことを指摘するかもしれません。 そして、あなたは正しいでしょう。 アプリ内のすべてのページに移動でき、JWTを送信しなくても任意のAPIエンドポイントを呼び出すことができます。 良くない!

誰がログインしているかを考慮するためにクライアントアプリを更新する必要があります。また、JWTを要求するようにAPIを更新する必要があります。 始めましょう。

ログインするには、新しいAngularコンポーネントが必要です。簡潔にするために、これはできるだけ単純にしておきます。 また、すべての認証要件を処理するサービスと、ログイン前にアクセスできないルートを保護するためのAngularGuardも必要です。クライアントアプリケーションのコンテキストで次のことを行います。

 cd client ng g component login --spec=false --inline-style ng g service auth --flat --spec=false ng g guard auth --flat --spec=false

これにより、 clientフォルダに4つの新しいファイルが生成されます。

 src/app/login/login.component.html src/app/login/login.component.ts src/app/auth.service.ts src/app/auth.guard.ts

次に、アプリの認証サービスとガードを提供する必要があります。 client/src/app/app.modules.ts更新します:

 import { AuthService } from './auth.service'; import { AuthGuard } from './auth.guard'; // ... providers: [ TodoService, UserService, AuthService, AuthGuard ],

次に、 client/src/app/app-routing.modules.tsのルーティングを更新して、認証ガードを利用し、ログインコンポーネントのルートを指定します。

 // ... import { LoginComponent } from './login/login.component'; import { AuthGuard } from './auth.guard'; const routes: Routes = [ { path: 'todos', component: TodoListComponent, canActivate: [AuthGuard] }, { path: 'users', component: UserListComponent, canActivate: [AuthGuard] }, { path: 'login', component: LoginComponent}, // ...

最後に、 client/src/app/auth.guard.tsを次の内容で更新します。

 import { Injectable } from '@angular/core'; import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; @Injectable() export class AuthGuard implements CanActivate { constructor(private router: Router) { } canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot) { if (localStorage.getItem('access_token')) { return true; } this.router.navigate(['login']); return false; } }

デモアプリケーションでは、ローカルストレージにJWTが存在するかどうかを確認するだけです。 実際のアプリケーションでは、トークンをデコードして、その有効性や有効期限などを確認します。たとえば、これにはJwtHelperServiceを使用できます。

この時点で、ログインする方法がないため、Angularアプリは常にログインページにリダイレクトしますclient/src/app/auth.service.tsの認証サービスから始めて、これを修正しましょう。

 import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; @Injectable() export class AuthService { constructor(private http: HttpClient) { } login(username: string, password: string): Observable<boolean> { return this.http.post<{token: string}>('/api/auth', {username: username, password: password}) .pipe( map(result => { localStorage.setItem('access_token', result.token); return true; }) ); } logout() { localStorage.removeItem('access_token'); } public get loggedIn(): boolean { return (localStorage.getItem('access_token') !== null); } }

私たちの認証サービスには、 loginlogoutの2つの機能しかありません。

  • login POSTは、提供されたusernamepasswordをバックエンドに送信し、 access_tokenを受信した場合はlocalStorageに設定します。 簡単にするために、ここではエラー処理はありません。
  • logoutは、 localStorageからaccess_tokenをクリアするだけであり、さらに何かに再度アクセスする前に、新しいトークンを取得する必要があります。
  • loggedInはブール型のプロパティであり、ユーザーがログインしているかどうかをすばやく判断するために使用できます。

そして最後に、ログインコンポーネント。 これらは実際にJWTを操作することとは関係がないため、コピーしてclient/src/app/login/login.components.htmlに貼り付けてください。

 <h4 *ngIf="error">{{error}}</h4> <form (ngSubmit)="submit()"> <div class="form-group col-3"> <label for="username">Username</label> <input type="text" name="username" class="form-control" [(ngModel)]="username" /> </div> <div class="form-group col-3"> <label for="password">Password</label> <input type="password" name="password" class="form-control" [(ngModel)]="password" /> </div> <div class="form-group col-3"> <button class="btn btn-primary" type="submit">Login</button> </div> </form>

そして、 client/src/app/login/login.components.tsには以下が必要です。

 import { Component, OnInit } from '@angular/core'; import { AuthService } from '../auth.service'; import { Router } from '@angular/router'; import { first } from 'rxjs/operators'; @Component({ selector: 'app-login', templateUrl: './login.component.html' }) export class LoginComponent { public username: string; public password: string; public error: string; constructor(private auth: AuthService, private router: Router) { } public submit() { this.auth.login(this.username, this.password) .pipe(first()) .subscribe( result => this.router.navigate(['todos']), err => this.error = 'Could not authenticate' ); } }

Voila、Angular 6のログイン例:

サンプルのAngularTodoListアプリのログイン画面。

この段階で、ログインしてjemmapaul 、またはsebastianとパスワードtodoを使用して) 、すべての画面を再度表示できるはずです。 しかし、私たちのアプリケーションは同じナビゲーションヘッダーを表示し、現在の状態に関係なくログアウトする方法はありません。 APIの修正に進む前に、それを修正しましょう。

client/src/app/app.component.tsで、ファイル全体を次のように置き換えます。

 import { Component } from '@angular/core'; import { Router } from '@angular/router'; import { AuthService } from './auth.service'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { constructor(private auth: AuthService, private router: Router) { } logout() { this.auth.logout(); this.router.navigate(['login']); } }

また、 client/src/app/app.component.htmlの場合、 <nav>セクションを次のように置き換えます。

 <nav class="nav nav-pills"> <a class="nav-link" routerLink="todos" routerLinkActive="active" *ngIf="auth.loggedIn">Todo List</a> <a class="nav-link" routerLink="users" routerLinkActive="active" *ngIf="auth.loggedIn">Users</a> <a class="nav-link" routerLink="login" routerLinkActive="active" *ngIf="!auth.loggedIn">Login</a> <a class="nav-link" (click)="logout()" href="#" *ngIf="auth.loggedIn">Logout</a> </nav>

ユーザーがログインしているかどうかに応じて、特定のアイテムのみを表示するようにナビゲーションコンテキストを認識しました。 もちろん、 auth.loggedInは、認証サービスをインポートできる場所であればどこでも使用できます。

APIの保護

あなたは考えているかもしれません、これは素晴らしいです…すべてが素晴らしく働いているように見えます。 しかし、3つの異なるユーザー名すべてでログインしてみると、何かに気付くでしょう。それらはすべて同じToDoリストを返します。 APIサーバーを見ると、実際には、各ユーザーが独自のアイテムのリストを持っていることがわかります。

さて、私たちが始めたとき、 userID=1のtodoリストを常に返すように/todosエンドポイントをコーディングしたことを思い出してください。 これは、現在ログインしているユーザーが誰であるかを知る方法がなかったためです。

これで、エンドポイントを保護し、JWTでエンコードされた情報を使用して必要なユーザーIDを提供することがいかに簡単であるかを見てみましょう。 最初に、最後のapp.use()呼び出しのすぐ下にあるserver/app.jsファイルに次の1行を追加します。

 app.use(expressJwt({secret: 'todo-app-super-shared-secret'}).unless({path: ['/api/auth']}));

express-jwtミドルウェアを使用し、共有シークレットとは何かを伝え、JWTを必要としないパスの配列を指定します。 以上です。 すべてのエンドポイントに触れたり、 ifステートメントを作成したりする必要はありません。

内部的には、ミドルウェアはいくつかの仮定を行っています。 たとえば、 AuthorizationヘッダーがBearer {token}の一般的なJWTパターンに従っていることを前提としています。 (ライブラリには、そうでない場合の動作をカスタマイズするためのオプションがたくさんあります。詳細については、express-jwtの使用法を参照してください。)

2番目の目的は、JWTでエンコードされた情報を使用して、誰が電話をかけているのかを確認することです。 もう一度express-jwtが助けになります。 トークンの読み取りと検証の一環として、署名プロセスで送信したエンコードされたペイロードをExpressの変数req.userに設定します。 その後、それを使用して、保存した変数にすぐにアクセスできます。 この例では、 userIDを認証されたユーザーのIDと同じに設定しているため、これをreq.user.userIDとして直接使用できます。

server/app.jsを再度更新し、 /todosエンドポイントを次のように変更します。

 res.send(getTodos(req.user.userID)); 

JWTを利用して、以前にハードコーディングしたものではなく、ログインしたユーザーのToDoリストを表示するAngularTodoListアプリ。

以上です。 APIが不正アクセスから保護されるようになり、認証されたユーザーがエンドポイントにいるユーザーを安全に特定できます。 クライアントアプリケーションにも単純な認証プロセスがあり、APIエンドポイントを呼び出すHTTPサービスには、認証トークンが自動的に付加されます。

Githubリポジトリのクローンを作成し、最終結果を実際に確認したい場合は、次を使用して最終的な形式でコードを確認できます。

 git checkout with-jwt

このウォークスルーが、独自のAngularアプリにJWT認証を追加するのに役立つことを願っています。 読んでくれてありがとう!

関連: JSON Web Tokenチュートリアル:LaravelとAngularJSの例