このチュートリアルでは、MEAN スタックでのユーザー認証の管理について見ていきます。 Node、Express、MongoDB で構築された REST API を使用して Angular シングルページ アプリを構築する、最も一般的な MEAN アーキテクチャを使用します。
ユーザー認証について考えるときは、次のことに取り組む必要があります。
- ユーザーに登録してもらう
- ユーザーデータは保存しますが、パスワードを直接保存しないでください
- 戻ってきたユーザーがログインできるようにする
- ページを訪問するまでの間、ログインしているユーザーのセッションを維持する
- ログインしたユーザーのみが閲覧できるページがいくつかある
- ログイン状態に応じて画面への出力を変更します (「ログイン」ボタンや「マイプロファイル」ボタンなど)。
コードの詳細に入る前に、MEAN スタックで認証がどのように機能するかについて概要を見てみましょう。
JavaScriptの詳細については、本を読んでください。 JavaScript:初心者から忍者、第2版.
MEAN スタック認証フロー
では、MEAN スタックでは認証はどのように見えるのでしょうか?
これを引き続き高レベルに保ち、フローのコンポーネントは次のとおりです。
- ユーザーデータはパスワードがハッシュ化された状態でMongoDBに保存されます。
- CRUD 関数は Express API に組み込まれています - 作成 (登録)、読み取り (ログイン、プロファイルの取得)、更新、削除
- Angular アプリケーションは API を呼び出し、応答を処理します。
- Express API は JSONWebトークン (JWT、「ジョット」と発音) 登録時またはログイン時にこれを Angular アプリケーションに渡します
- Angular アプリケーションは、ユーザーのセッションを維持するために JWT を保存します。
- Angular アプリケーションは、保護されたビューを表示するときに JWT の有効性をチェックします。
- Angular アプリケーションは、保護された API ルートを呼び出すときに JWT を Express に返します。
ブラウザーでセッション状態を維持するには、Cookie よりも JWT が優先されます。 Cookie は、サーバー側アプリケーションを使用するときに状態を維持するのに適しています。
アプリケーション例
このチュートリアルのコードは次の場所で入手できます。 GitHubの。 アプリケーションを実行するには、以下が必要です インストールされたNode.js、 に加えて MongoDBの。 (インストール方法については、こちらを参照してください) Mongo の公式ドキュメント — Windows、Linux、macOS).
Angular アプリ
このチュートリアルの例をシンプルにするために、XNUMX つのページを持つ Angular アプリから始めます。
- ホームページ
- 登録ページ
- ログインページ
- プロフィールページ
ページは非常に基本的なもので、最初は次のようになります。
プロフィール ページには、認証されたユーザーのみがアクセスできます。 Angular アプリのすべてのファイルは、Angular CLI アプリ内の次のフォルダーにあります。 /client
.
ローカル サーバーの構築と実行には Angular CLI を使用します。 Angular CLI に慣れていない場合は、「 Angular CLI を使用した Todo アプリの構築 始めるためのチュートリアル。
REST API
また、Node、Express、MongoDB を使用して構築された REST API のスケルトンから始めます。 マングース スキーマを管理します。 この API には、最初は XNUMX つのルートが必要です。
/api/register
(POST)、新規ユーザーの登録を処理します。/api/login
(POST)、戻ってきたユーザーのログインを処理します。/api/profile/USERID
(GET)、指定されたときにプロファイルの詳細を返します。USERID
それでは設定してみましょう。 使用できます エクスプレスジェネレーター 多くの定型文を作成するためのツールです。 初めての方には、 使い方のチュートリアルはこちら.
でインストール npm i -g express-generator
。 次に、新しい Express アプリを作成し、選択します。 パグ ビューエンジンとして:
express -v pug mean-authentication
ジェネレーターが実行されたら、プロジェクト ディレクトリに移動し、依存関係をインストールします。
cd mean-authentication
npm i
執筆時点では、これは Pug の古いバージョンを取り込みます。 それを修正しましょう:
npm i pug@latest
途中で Mongoose をインストールすることもできます。
npm i mongoose
次に、フォルダー構造を作成する必要があります。
- 削除する
public
フォルダ:rm -rf public
. - 作る
api
ディレクトリ:mkdir api
. - 作る
controllers
models
、とroutes
のディレクトリapi
ディレクトリ:mkdir -p api/{controllers,models,routes}
. - 作る
authenication.js
ファイルとprofile.js
内のファイルcontrollers
ディレクトリ:touch api/controllers/{authentication.js,profile.js}
. - 作る
db.js
ファイルとusers.js
内のファイルmodels
ディレクトリ:touch api/models/{db.js,users.js}
. - 作る
index.js
内のファイルroutes
ディレクトリ:touch api/routes/index.js
.
完了すると、次のようになります。
.
└── api ├── controllers │ ├── authentication.js │ └── profile.js ├── models │ ├── db.js │ └── users.js └── routes └── index.js
次に、API 機能を追加しましょう。 のコードを置き換えます app.js
次のように:
require('./api/models/db'); const cookieParser = require('cookie-parser');
const createError = require('http-errors');
const express = require('express');
const logger = require('morgan');
const path = require('path'); const routesApi = require('./api/routes/index'); const app = express(); // view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug'); app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public'))); app.use('/api', routesApi); // catch 404 and forward to error handler
app.use((req, res, next) => { next(createError(404));
}); // error handler
app.use((err, req, res, next) => { // set locals, only providing error in development res.locals.message = err.message; res.locals.error = req.app.get('env') === 'development' ? err : {}; // render the error page res.status(err.status || 500); res.render('error');
}); module.exports = app;
以下を追加 api/models/db.js
:
require('./users');
const mongoose = require('mongoose');
const dbURI = 'mongodb://localhost:27017/meanAuth'; mongoose.set('useCreateIndex', true);
mongoose.connect(dbURI, { useNewUrlParser: true, useUnifiedTopology: true
}); mongoose.connection.on('connected', () => { console.log(`Mongoose connected to ${dbURI}`);
});
mongoose.connection.on('error', (err) => { console.log(`Mongoose connection error: ${err}`);
});
mongoose.connection.on('disconnected', () => { console.log('Mongoose disconnected');
});
以下を追加 api/routes/index.js
:
const ctrlAuth = require('../controllers/authentication');
const ctrlProfile = require('../controllers/profile'); const express = require('express');
const router = express.Router(); // profile
router.get('/profile/:userid', ctrlProfile.profileRead); // authentication
router.post('/register', ctrlAuth.register);
router.post('/login', ctrlAuth.login); module.exports = router;
以下を追加 api/controllers/profile.js
:
module.exports.profileRead = (req, res) => { console.log(`Reading profile ID: ${req.params.userid}`); res.status(200); res.json({ message : `Profile read: ${req.params.userid}` });
};
以下を追加 api/controllers/authentication.js
:
module.exports.register = (req, res) => { console.log(`Registering user: ${req.body.email}`); res.status(200); res.json({ message : `User registered: ${req.body.email}` });
}; module.exports.login = (req, res) => { console.log(`Logging in user: ${req.body.email}`); res.status(200); res.json({ message : `User logged in: ${req.body.email}` });
};
Mongo が実行されていることを確認し、最後に次のコマンドでサーバーを起動します。 npm run start
。 すべてが正しく設定されている場合は、Mongoose が接続されているというメッセージが端末に表示されるはずです。 mongodb://localhost:27017/meanAuth
これで、API にリクエストを送信し、API から応答を取得できるようになります。 次のようなツールを使用してこれをテストできます Postman.
Mongoose を使用した MongoDB データ スキーマの作成
次に、スキーマを追加しましょう api/models/users.js
。 これは、電子メール アドレス、名前、ハッシュ、ソルトの必要性を定義します。 ハッシュとソルトは、パスワードを保存する代わりに使用されます。 の email
ログイン認証情報に使用するため、一意に設定されます。 スキーマは次のとおりです。
const mongoose = require('mongoose'); const userSchema = new mongoose.Schema({ email: { type: String, unique: true, required: true }, name: { type: String, required: true }, hash: String, salt: String
}); mongoose.model('User', userSchema);
パスワードを保存せずに管理する
ユーザーのパスワードを保存することは、絶対にやってはいけないことです。 ハッカーがデータベースのコピーを入手した場合、それを使用してアカウントにログインできないようにする必要があります。 ここでハッシュとソルトが登場します。
ソルトは、各ユーザーに固有の文字列です。 ハッシュは、ユーザーが提供したパスワードとソルトを組み合わせて作成され、一方向暗号化が適用されます。 ハッシュは復号化できないため、ユーザーを認証する唯一の方法は、パスワードを取得し、ソルトと組み合わせて再度暗号化することです。 この出力がハッシュと一致する場合、パスワードは正しいはずです。
パスワードの設定とチェックを行うには、Mongoose スキーマ メソッドを使用できます。 これらは基本的に、スキーマに追加する関数です。 彼らは両方とも、 Node.js暗号化モジュール.
の上部にある users.js
モデル ファイルを使用するには、暗号化が必要です。
const crypto = require('crypto');
暗号はノードの一部として出荷されるため、何もインストールする必要はありません。 暗号自体にはいくつかの方法があります。 私たちは興味があります ランダムバイト ランダムな塩を作成し、 pbkdf2Sync ハッシュを作成します。
パスワードの設定
パスワードへの参照を保存するには、という新しいメソッドを作成します。 setPassword
userSchema
パスワードパラメータを受け入れるスキーマ。 このメソッドは次に使用します crypto.randomBytes
塩を設定し、 crypto.pbkdf2Sync
ハッシュを設定するには:
userSchema.methods.setPassword = function(password) { this.salt = crypto.randomBytes(16).toString('hex'); this.hash = crypto .pbkdf2Sync(password, this.salt, 1000, 64, 'sha512') .toString('hex');
};
ユーザーを作成するときにこのメソッドを使用します。 パスワードを保存する代わりに、 password
パスに渡すことができます。 setPassword
を設定する関数 salt
および hash
ユーザードキュメント内のパス。
パスワードを確認する
パスワードのチェックも同様のプロセスですが、Mongoose モデルからのソルトはすでにあります。 今回は、ソルトとパスワードを暗号化し、出力が保存されているハッシュと一致するかどうかを確認するだけです。
別の新しいメソッドを users.js
モデル ファイル、と呼ばれる validPassword
:
userSchema.methods.validPassword = function(password) { const hash = crypto .pbkdf2Sync(password, this.salt, 1000, 64, 'sha512') .toString('hex'); return this.hash === hash;
};
JSON Web トークン (JWT) の生成
Mongoose モデルが実行できる必要があるもう XNUMX つのことは、API がそれを応答として送信できるように JWT を生成することです。 Mongoose メソッドは、コードを XNUMX か所に保持し、必要なときにいつでも呼び出すことができるため、ここでも理想的です。 ユーザーの登録時とログイン時にこれを呼び出す必要があります。
JWT を作成するには、というパッケージを使用します。 jsonウェブトークンこれはアプリケーションにインストールする必要があるため、コマンドラインでこれを実行します。
npm i jsonwebtoken
次に、これを users.js
モデルファイル:
const jwt = require('jsonwebtoken');
このモジュールは、 sign
このメソッドを使用すると、JWT を作成することができます。これには、トークンに含めたいデータと、ハッシュ アルゴリズムが使用するシークレットを渡すだけです。 データは JavaScript オブジェクトとして送信し、有効期限を含める必要があります。 exp
プロパティ。
追加する generateJwt
メソッド userSchema
JWT を返すには次のようになります。
userSchema.methods.generateJwt = function() { const expiry = new Date(); expiry.setDate(expiry.getDate() + 7); return jwt.sign( { _id: this._id, email: this.email, name: this.name, exp: parseInt(expiry.getTime() / 1000) }, 'MY_SECRET' ); // DO NOT KEEP YOUR SECRET IN THE CODE!
};
注: シークレットを安全に保管することが重要です。シークレットが何であるかを知っているのは発信元のサーバーだけです。 特にコードがバージョン管理のどこかに保存されている場合は、シークレットを環境変数として設定し、ソース コードには含めないことをお勧めします。
データベースに対して行う必要があるのはこれだけです。
高速認証を処理するためにパスポートを設定する
パスポート Express で認証を処理するプロセスを簡素化するノード モジュールです。 Facebook、Twitter、Oauth でのログインなど、さまざまな認証「戦略」を操作するための共通のゲートウェイを提供します。 ここで使用する戦略は、ローカルに保存されているユーザー名とパスワードを使用するため、「ローカル」と呼ばれます。
パスポートを使用するには、まずパスポートと戦略をインストールし、それらを保存します。 package.json
:
npm i passport passport-local
パスポートの設定
内部 api
フォルダー、新しいフォルダーを作成します config
そこに次の名前のファイルを作成します passport.js
。 ここで戦略を定義します。
mkdir -p api/config
touch api/config/passport.js
戦略を定義する前に、このファイルにはパスポート、戦略、Mongoose、および User
モデル:
const mongoose = require('mongoose');
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const User = mongoose.model('User');
ローカル戦略の場合、基本的に必要なのは、 User
モデル。 このクエリは、指定された電子メール アドレスを持つユーザーを見つけて、 validPassword
ハッシュが一致するかどうかを確認するメソッド。
パスポートに関しては、対処しなければならない好奇心が XNUMX つだけあります。 内部的には、Passport のローカル戦略では、次の XNUMX つのデータが必要です。 username
および password
。 ただし、私たちが使用しているのは、 email
一意の識別子としてではなく、 username
。 これは、オプション オブジェクトで設定できます。 usernameField
戦略定義のプロパティ。 その後、Mongoose クエリに進みます。
つまり、戦略の定義は次のようになります。
passport.use( new LocalStrategy( { usernameField: 'email' }, function(username, password, done) { User.findOne({ email: username }, function(err, user) { if (err) { return done(err); } // Return if user not found in database if (!user) { return done(null, false, { message: 'User not found' }); } // Return if password is wrong if (!user.validPassword(password)) { return done(null, false, { message: 'Password is wrong' }); } // If credentials are correct, return the user object return done(null, user); }); } )
);
どのように validPassword
スキーマ メソッドは、 user
インスタンス。
あとはパスポートをアプリケーションに追加するだけです。 それで app.js
Passport モジュールを要求し、Passport 構成を要求し、Passport をミドルウェアとして初期化する必要があります。 これらすべてのアイテムを内部に配置すると、 app.js
特定の順序に適合させる必要があるため、これは非常に重要です。
Passport モジュールは、他の一般的なモジュールとともにファイルの先頭に必要です。 require
文:
const cookieParser = require('cookie-parser');
const createError = require('http-errors');
const express = require('express');
const logger = require('morgan');
const passport = require('passport');
const path = require('path');
設定が必要なはずです After 構成はモデルを参照するため、モデルは必須です。
require('./api/models/db');
require('./api/config/passport');
最後に、API ルートが追加される直前に、Passport を Express ミドルウェアとして初期化する必要があります。これは、これらのルートが Passport を初めて使用するためです。
app.use(passport.initialize());
app.use("/api", routesApi);
これでスキーマとパスポートが設定されました。 次に、これらを API のルートとコントローラーで使用します。
APIエンドポイントの構成
API を使用して行うことは XNUMX つあります。
- コントローラーを機能させる
- を確保する
/api/profile
認証されたユーザーのみがアクセスできるようにルートする
登録およびログイン API コントローラーのコーディング
サンプルアプリでは、登録コントローラーとログインコントローラーは次のとおりです。 /api/controllers/authentication.js。 コントローラーが動作するには、ファイルに Passport、Mongoose、およびユーザー モデルが必要です。
const mongoose = require('mongoose');
const passport = require('passport');
const User = mongoose.model('User');
レジスタ API コントローラー
レジスタ コントローラーは次のことを行う必要があります。
- 送信されたフォームからデータを取得し、新しい Mongoose モデル インスタンスを作成します。
- を呼び出す
setPassword
ソルトとハッシュをインスタンスに追加するために以前に作成したメソッド - インスタンスをレコードとしてデータベースに保存します
- JWTを生成する
- JSON 応答内で JWT を送信します
コードでは、すべては次のようになります。 これはダミーを置き換える必要があります register
先ほどコーディングした関数:
module.exports.register = (req, res) => { const user = new User(); user.name = req.body.name; user.email = req.body.email; user.setPassword(req.body.password); user.save(() => { const token = user.generateJwt(); res.status(200); res.json({ token: token }); });
};
これは、 setPassword
および generateJwt
Mongoose スキーマ定義で作成したメソッド。 このコードをスキーマに含めることで、このコントローラーがどのように読みやすく理解しやすくなるかを見てください。
実際には、このコードにはフォーム入力を検証し、エラーをキャッチする多数のエラー トラップがあることを忘れないでください。 save
関数。 コードの主な機能を強調するためにここでは省略していますが、復習が必要な場合は、「」を参照してください。Node.jsとExpressによるフォーム、ファイルのアップロード、セキュリティ"。
ログイン API コントローラー
ログイン コントローラーはほぼすべての制御を Passport に渡しますが、必要なフィールドが送信されたことを確認するために、事前に検証を追加することもできます (またそうする必要があります)。
Passport がその魔法を実行し、構成で定義された戦略を実行するには、 authenticate
以下に示すような方法です。 このメソッドは、XNUMX つの可能なパラメータを指定してコールバックを呼び出します。 err
, user
および info
。 場合 user
が定義されている場合は、ブラウザに返す JWT を生成するために使用できます。 これはダミーを置き換える必要があります login
前に定義したメソッド:
module.exports.login = (req, res) => { passport.authenticate('local', (err, user, info) => { // If Passport throws/catches an error if (err) { res.status(404).json(err); return; } // If a user is found if (user) { const token = user.generateJwt(); res.status(200); res.json({ token: token }); } else { // If user is not found res.status(401).json(info); } })(req, res);
};
APIルートの保護
バックエンドで最後に行うことは、認証されたユーザーのみがアクセスできるようにすることです。 /api/profile
ルート。 リクエストを検証する方法は、シークレットを再度使用して、リクエストとともに送信された JWT が本物であることを確認することです。 このため、コードに含めずに秘密にしておく必要があります。
ルート認証の構成
まず、と呼ばれるミドルウェアをインストールする必要があります。 エクスプレス-jwt:
npm i express-jwt
次に、それを要求し、ルートが定義されているファイル内で構成する必要があります。 サンプル アプリケーションでは、これは /api/routes/index.js。 構成とは、秘密を伝えることと、オプションで、上に作成するプロパティの名前を伝えることです。 req
JWT を保持するオブジェクト。 このプロパティは、ルートに関連付けられたコントローラー内で使用できるようになります。 プロパティのデフォルト名は次のとおりです。 user
、ただし、これはマングースのインスタンスの名前です。 User
モデルなので、次のように設定します payload
混乱を避けるために:
// api/routes/index.js const jwt = require('express-jwt'); const auth = jwt({ secret: 'MY_SECRET', userProperty: 'payload'
}); ...
繰り返しになりますが、 コードに秘密を残さないでください。
ルート認証の適用
このミドルウェアを適用するには、次のように、保護するルートの途中で関数を参照するだけです。
router.get('/profile', auth, ctrlProfile.profileRead);
私たちが変わったことに気づいてください /profile/:userid
〜へ /profile
, ID は JWT から取得されるためです。
誰かが有効な JWT なしでそのルートにアクセスしようとすると、ミドルウェアはエラーをスローします。 API が適切に動作することを確認するには、メインのエラー ハンドラー セクションに次のコードを追加して、このエラーをキャッチし、401 応答を返します。 app.js
ファイル:
// catch 404 and forward to error handler
app.use((req, res, next) => { ... }); // Catch unauthorised errors
app.use((err, req, res) => { if (err.name === 'UnauthorizedError') { res.status(401); res.json({ message: `${err.name}: ${err.message}` }); }
});
この時点で、GET を試みることができます。 /api/profile
Postman などのツールを使用したエンドポイント、または あなたのブラウザで401 応答が表示されるはずです。
ルート認証の使用
この例では、ユーザーに自分のプロファイルのみを表示できるようにしたいため、JWT からユーザー ID を取得し、それを Mongoose クエリで使用します。
このルートのコントローラーは次のとおりです /api/controllers/profile.js。 このファイルの内容全体は次のようになります。
const mongoose = require('mongoose');
const User = mongoose.model('User'); module.exports.profileRead = (req, res) => { // If no user ID exists in the JWT return a 401 if (!req.payload._id) { res.status(401).json({ message: 'UnauthorizedError: private profile' }); } else { // Otherwise continue User.findById(req.payload._id).exec(function(err, user) { res.status(200).json(user); }); }
};
当然のことながら、これはさらにエラー トラップ (ユーザーが見つからない場合など) で具体化する必要がありますが、このスニペットはアプローチの重要なポイントを示すために簡潔にまとめられています。
バックエンドについては以上です。 データベースが構成され、登録とログインのための API エンドポイントがあり、JWT と保護されたルートを生成して返します。
フロントエンドへ!
Angular アプリを初期化する
このセクションでは Angluar CLI を使用するので、先に進む前に、それがグローバルにインストールされていることを確認してください。
npm install -g @angular/cli
次に、プロジェクトのルート ディレクトリで次のコマンドを実行します。
ng new client ? Would you like to add Angular routing? Yes
? Which stylesheet format would you like to use? CSS
...
✔ Packages installed successfully. Successfully initialized git.
これにより、新しい client
ディレクトリ AppModule
および AppRoutingModule
。 「Angular ルーティングを追加しますか?」に「はい」と答えると、 AppRoutingModule
自動的に作成され、インポートされます AppModule
私達のために。
Angular フォームと Angular の HTTP クライアントを利用するため、Angular のフォームをインポートする必要があります。 フォームモジュール および HttpClient モジュール。 の内容を変更します client/src/app/app.module.ts
そのようです:
import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core"; import { AppRoutingModule } from "./app-routing.module";
import { AppComponent } from "./app.component";
import { FormsModule } from "@angular/forms";
import { HttpClientModule } from "@angular/common/http"; @NgModule({ declarations: [ AppComponent ], imports: [BrowserModule, AppRoutingModule, FormsModule, HttpClientModule], providers: [], bootstrap: [AppComponent]
})
export class AppModule {}
Angular認証サービスを作成する
フロントエンドの作業のほとんどは Angular サービスに入れて、管理するメソッドを作成できます。
- JWT をローカル ストレージに保存する
- ローカルストレージからJWTを読み取る
- ローカルストレージからJWTを削除する
- 登録およびログイン API エンドポイントの呼び出し
- ユーザーが現在ログインしているかどうかを確認する
- JWTからログインユーザーの詳細を取得する
という新しいサービスを作成する必要があります。 AuthenticationService
。 CLI を使用すると、次のコマンドを実行してこれを行うことができます。
$ cd client
$ ng generate service authentication
CREATE src/app/authentication.service.spec.ts (397 bytes)
CREATE src/app/authentication.service.ts (143 bytes)
サンプルアプリでは、これはファイル内にあります /client/src/app/authentication.service.ts:
import { Injectable } from "@angular/core"; @Injectable({ providedIn: "root"
})
export class AuthenticationService { constructor() {}
}
ローカル ストレージ: JWT の保存、読み取り、削除
次の訪問の間にユーザーをログイン状態に保つために、次を使用します。 localStorage
ブラウザで JWT を保存します。 代わりに使用するのは、 sessionStorage
、現在のブラウザ セッション中にのみトークンが保持されます。
まず、データ型を処理するためのインターフェイスをいくつか作成します。 これはアプリケーションの型チェックに役立ちます。 プロファイルは次のようにフォーマットされたオブジェクトを返します。 UserDetails
、ログインおよび登録エンドポイントは、 TokenPayload
リクエスト中に、 TokenResponse
オブジェクト:
export interface UserDetails { _id: string; email: string; name: string; exp: number; iat: number;
} interface TokenResponse { token: string;
} export interface TokenPayload { email: string; password: string; name?: string;
}
このサービスでは、 HttpClient
Angular からのサービスを使用して、サーバー アプリケーション (すぐに使用します) に HTTP リクエストを送信します。 Router
プログラムでナビゲートするサービス。 それらをサービス コンストラクターに注入する必要があります。
constructor(private http: HttpClient, private router: Router) {}
次に、JWT トークンと対話する XNUMX つのメソッドを定義します。 実施します saveToken
トークンの保存を処理する localStorage
そしてに token
財産、 getToken
トークンを取得するメソッド localStorage
または token
財産、そして logout
JWT トークンを削除してホームページにリダイレクトする関数。
サーバー側レンダリングを使用している場合、このコードは実行されないことに注意することが重要です。 localStorage
および window.atob
は利用できません。 についての詳細があります Angular ドキュメントのサーバー側レンダリングに対処するソリューション.
これまでのところ、次のことがわかります。
import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Router } from "@angular/router";
import { Observable } from "rxjs";
import { map } from "rxjs/operators"; export interface UserDetails { _id: string; email: string; name: string; exp: number; iat: number;
} interface TokenResponse { token: string;
} export interface TokenPayload { email: string; password: string; name?: string;
} @Injectable({ providedIn: "root"
})
export class AuthenticationService { private token: string; constructor(private http: HttpClient, private router: Router) {} private saveToken(token: string): void { localStorage.setItem("mean-token", token); this.token = token; } private getToken(): string { if (!this.token) { this.token = localStorage.getItem("mean-token"); } return this.token; } public logout(): void { this.token = ""; window.localStorage.removeItem("mean-token"); this.router.navigateByUrl("/"); }
}
次に、このトークンとトークンの有効性をチェックして、訪問者がログインしているかどうかを確認するメソッドを追加しましょう。
JWT からデータを取得する
JWT のデータを設定するとき ( generateJwt
Mongoose メソッド) に有効期限を含めました。 exp
財産。 しかし、JWT を見ると、次の例のようにランダムな文字列であるように見えます。
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJfaWQiOiI1NWQ0MjNjMTUxMzcxMmNkMzE3YTRkYTciLCJlbWFpbCI6InNpbW9uQGZ1bGxzdGFja3RyYWluaW5nLmNvbSIsIm5hbWUiOiJTaW1vbiBIb2xtZXMiLCJleHAiOjE0NDA1NzA5NDUsImlhdCI6MTQzOTk2NjE0NX0.jS50GlmolxLoKrA_24LDKaW3vNaY94Y9EqYAFvsTiLg
では、JWT はどうやって読むのでしょうか?
JWT は実際には、ドット (.
)。 これら XNUMX つの部分は次のとおりです。
- ヘッダ: 使用されるタイプとハッシュ アルゴリズムを含むエンコードされた JSON オブジェクト
- ペイロード: データを含むエンコードされた JSON オブジェクト、トークンの実際の本体
- 署名: サーバー上に設定された「シークレット」を使用した、ヘッダーとペイロードの暗号化されたハッシュ。
ここで関心があるのは XNUMX 番目の部分、つまりペイロードです。 これは、 エンコード 暗号化されるのではなく、つまり、 デコード ボーマンは
という機能があります アトブ これは最新のブラウザにネイティブであり、次のように Base64 文字列をデコードします。
したがって、トークンの XNUMX 番目の部分を取得し、デコードして JSON として解析する必要があります。 その後、有効期限が過ぎていないことを確認できます。
その最後には、 getUserDetails
関数は次のオブジェクトを返す必要があります。 UserDetails
タイプまたは null
有効なトークンが見つかったかどうかに応じて。 まとめると次のようになります。
public getUserDetails(): UserDetails { const token = this.getToken(); let payload; if (token) { payload = token.split(".")[1]; payload = window.atob(payload); return JSON.parse(payload); } else { return null; }
}
提供されるユーザーの詳細には、ユーザーの名前、電子メール、トークンの有効期限に関する情報が含まれており、ユーザー セッションが有効かどうかを確認するために使用されます。
ユーザーがログインしているかどうかを確認する
という新しいメソッドを追加します。 isLoggedIn
サービスに。 それは、 getUserDetails
メソッドを使用して JWT トークンからトークンの詳細を取得し、有効期限がまだ過ぎていないかどうかを確認します。
public isLoggedIn(): boolean { const user = this.getUserDetails(); if (user) { return user.exp > Date.now() / 1000; } else { return false; }
}
トークンが存在する場合、メソッドはユーザーがログインしているかどうかをブール値として返します。 これで、認証用のトークンを使用して、データをロードするための HTTP リクエストを構築できるようになりました。
API 呼び出しの構造化
API 呼び出しを簡単に行うには、 request
メソッドを AuthenticationService
、特定のタイプのリクエストに応じて、監視可能な適切な HTTP リクエストを構築して返すことができます。 これはこのサービスによってのみ使用され、コードの重複を減らすためにのみ存在するため、プライベート メソッドです。 これはAngularを使用します HttpClient
サービス。 これを忘れずに AuthenticationService
まだ存在しない場合:
private request( method: "post" | "get", type: "login" | "register" | "profile", user?: TokenPayload
): Observable<any> { let base$; if (method === "post") { base$ = this.http.post(`/api/${type}`, user); } else { base$ = this.http.get(`/api/${type}`, { headers: { Authorization: `Bearer ${this.getToken()}` } }); } const request = base$.pipe( map((data: TokenResponse) => { if (data.token) { this.saveToken(data.token); } return data; }) ); return request;
}
それは必要です map
API ログインまたは登録呼び出しによってトークンが返された場合、トークンをインターセプトしてサービスに保存するために RxJS からオペレーターを呼び出します。 これで、API を呼び出すためのパブリック メソッドを実装できるようになりました。
登録およびログイン API エンドポイントの呼び出し
追加するメソッドは XNUMX つだけです。 Angular アプリと API の間に、 login
および register
エンドポイントを取得し、返されたトークンを保存するか、 profile
ユーザーの詳細を取得するためのエンドポイント:
public register(user: TokenPayload): Observable<any> { return this.request("post", "register", user);
} public login(user: TokenPayload): Observable<any> { return this.request("post", "login", user);
} public profile(): Observable<any> { return this.request("get", "profile");
}
各メソッドは、実行する必要がある API 呼び出しの XNUMX つに対する HTTP リクエストを処理するオブザーバブルを返します。 これでサービスが完了します。 今度は、Angular アプリですべてを結び付けます。
Angular アプリに認証を適用する
使用できます AuthenticationService
Angular アプリ内でさまざまな方法で、私たちが求めているエクスペリエンスを提供します。
- 登録フォームとサインインフォームを接続する
- ユーザーのステータスを反映するようにナビゲーションを更新します
- ログインしたユーザーのみにアクセスを許可します
/profile
route - 保護された人に電話をかける
/api/profile
APIルート
開始するには、まず Angular CLI を使用して必要なコンポーネントを生成します。
$ ng generate component register
CREATE src/app/register/register.component.css (0 bytes)
CREATE src/app/register/register.component.html (23 bytes)
CREATE src/app/register/register.component.spec.ts (642 bytes)
CREATE src/app/register/register.component.ts (283 bytes)
UPDATE src/app/app.module.ts (458 bytes) $ ng generate component profile
CREATE src/app/profile/profile.component.css (0 bytes)
CREATE src/app/profile/profile.component.html (22 bytes)
CREATE src/app/profile/profile.component.spec.ts (635 bytes)
CREATE src/app/profile/profile.component.ts (279 bytes)
UPDATE src/app/app.module.ts (540 bytes) $ ng generate component login
CREATE src/app/login/login.component.css (0 bytes)
CREATE src/app/login/login.component.html (20 bytes)
CREATE src/app/login/login.component.spec.ts (621 bytes)
CREATE src/app/login/login.component.ts (271 bytes)
UPDATE src/app/app.module.ts (614 bytes) $ ng generate component home
CREATE src/app/home/home.component.css (0 bytes)
CREATE src/app/home/home.component.html (19 bytes)
CREATE src/app/home/home.component.spec.ts (614 bytes)
CREATE src/app/home/home.component.ts (267 bytes)
UPDATE src/app/app.module.ts (684 bytes)
登録コントローラーとログインコントローラーを接続する
コンポーネントが作成されたので、登録フォームとログイン フォームを見てみましょう。
登録ページ
まずは登録フォームを作成しましょう。 それは持っています NgModel
フィールドにアタッチされたディレクティブはすべて、 credentials
コントローラーのプロパティ。 フォームには、 (submit)
送信を処理するイベント バインディング。 サンプル アプリケーションでは、次のとおりです。 /client/src/app/register/register.component.html 次のようになります:
<form (submit)="register()"> <div class="form-group"> <label for="name">Full name</label> <input type="text" class="form-control" name="name" placeholder="Enter your name" [(ngModel)]="credentials.name" /> </div> <div class="form-group"> <label for="email">Email address</label> <input type="email" class="form-control" name="email" placeholder="Enter email" [(ngModel)]="credentials.email" /> </div> <div class="form-group"> <label for="password">Password</label> <input type="password" class="form-control" name="password" placeholder="Password" [(ngModel)]="credentials.password" /> </div> <button type="submit" class="btn btn-default">Register!</button>
</form>
コントローラーの最初のタスクは、 AuthenticationService
と Router
これらは注入され、コンストラクターを通じて利用可能になります。 次に内部では、 register
フォーム送信のハンドラー、呼び出し auth.register
、フォームから資格情報を渡します。
register
メソッドはオブザーバブルを返します。リクエストをトリガーするには、これをサブスクライブする必要があります。 オブザーバブルは成功または失敗を出力し、誰かが登録に成功した場合は、そのユーザーをプロフィール ページにリダイレクトするか、コンソールにエラーを記録するようにアプリケーションを設定します。
サンプル アプリケーションでは、コントローラーは /client/src/app/register/register.component.ts 次のようになります:
import { Component } from "@angular/core";
import { AuthenticationService, TokenPayload } from "../authentication.service";
import { Router } from "@angular/router"; @Component({ templateUrl: "./register.component.html", styleUrls: ["./register.component.css"]
})
export class RegisterComponent { credentials: TokenPayload = { email: "", name: "", password: "" }; constructor(private auth: AuthenticationService, private router: Router) {} register() { this.auth.register(this.credentials).subscribe( () => { this.router.navigateByUrl("/profile"); }, err => { console.error(err); } ); }
}
ログインページ
ログイン ページは登録ページと本質的によく似ていますが、このフォームでは名前は要求されず、電子メールとパスワードのみが要求されます。 サンプルアプリケーションでは、 /client/src/app/login/login.component.html 次のようになります:
<form (submit)="login()"> <div class="form-group"> <label for="email">Email address</label> <input type="email" class="form-control" name="email" placeholder="Enter email" [(ngModel)]="credentials.email" /> </div> <div class="form-group"> <label for="password">Password</label> <input type="password" class="form-control" name="password" placeholder="Password" [(ngModel)]="credentials.password" /> </div> <button type="submit" class="btn btn-default">Sign in!</button>
</form>
もう一度、フォーム送信ハンドラーを用意し、 NgModel
各入力の属性。 コントローラーでは、レジスターコントローラーと同じ機能が必要ですが、今回は login
の方法 AuthenticationService
.
サンプル アプリケーションでは、コントローラーは /client/src/app/login/login.component.ts 次のようになります:
import { Component } from "@angular/core";
import { AuthenticationService, TokenPayload } from "../authentication.service";
import { Router } from "@angular/router"; @Component({ templateUrl: "./login.component.html", styleUrls: ["./login.component.css"]
})
export class LoginComponent { credentials: TokenPayload = { email: "", password: "" }; constructor(private auth: AuthenticationService, private router: Router) {} login() { this.auth.login(this.credentials).subscribe( () => { this.router.navigateByUrl("/profile"); }, err => { console.error(err); } ); }
}
これで、ユーザーはアプリケーションに登録してサインインできるようになりました。 繰り返しになりますが、送信前にすべての必須フィールドが入力されていることを確認するために、フォームでさらに検証を行う必要があることに注意してください。 これらの例は、主な機能を強調するために最小限に留められています。
ユーザーのステータスに応じてコンテンツを変更する
ナビゲーションで表示したいのは、 サインイン ユーザーがログインしていない場合はリンクが表示され、ログインしている場合はユーザー名とプロフィール ページへのリンクが表示されます。ナビゲーションバーは次の場所にあります。 App
コンポーネント。
まず、見ていきます。 App
コンポーネントコントローラー。 を注入できます。 AuthenticationService
コンポーネントに追加し、テンプレート内で直接呼び出します。 サンプルアプリでは、ファイルは次の場所にあります /client/src/app/app.component.ts 次のようになります:
import { Component } from "@angular/core";
import { AuthenticationService } from "./authentication.service"; @Component({ selector: "app-root", templateUrl: "./app.component.html", styleUrls: ["./app.component.css"]
}) export class AppComponent { constructor(public auth: AuthenticationService) {}
}
これで、関連付けられたテンプレートで使用できるようになりました auth.isLoggedIn()
サインイン リンクとプロファイル リンクのどちらを表示するかを決定します。 ユーザーの名前をプロフィール リンクに追加するには、次の名前プロパティにアクセスします。 auth.getUserDetails()?.name
。 これは JWT からデータを取得していることに注意してください。 の ?.
演算子は、エラーをスローせずに、未定義の可能性があるオブジェクトのプロパティにアクセスする特別な方法です。
サンプルアプリでは、ファイルは次の場所にあります /client/src/app/app.component.html 更新された部分は次のようになります。
<ul class="nav navbar-nav navbar-right"> <li *ngIf="!auth.isLoggedIn()"><a routerLink="/login">Sign in</a></li> <li *ngIf="auth.isLoggedIn()"> <a routerLink="/profile">{{ auth.getUserDetails()?.name }}</a> </li> <li *ngIf="auth.isLoggedIn()"><a (click)="auth.logout()">Logout</a></li>
</ul> <router-outlet></router-outlet>
ログインユーザーのみのルートを保護する
このステップでは、ルートを保護することで、ログインしているユーザーのみがアクセスできるようにする方法を見ていきます。 /profile
パス。
Angular では、ルート ガードを定義できます。これにより、ルーティング ライフ サイクルのいくつかのポイントでチェックを実行して、ルートを読み込むことができるかどうかを判断できます。 を使用します。 CanActivate
ユーザーがログインしている場合にのみプロファイル ルートをロードするように Angular に指示するフック。
これを行うには、ルート ガードを作成する必要があります。
$ ng generate guard auth
? Which interfaces would you like to implement? CanActivate
CREATE src/app/auth.guard.spec.ts (331 bytes)
CREATE src/app/auth.guard.ts (456 bytes)
を実装する必要があります。 CanActivate
インターフェイスとそれに関連する canActivate
方法。 このメソッドは、 AuthenticationService.isLoggedIn
メソッド (基本的に、トークンが見つかったかどうか、まだ有効かどうかを確認します)、ユーザーが有効でない場合は、ユーザーをホームページにリダイレクトします。
In auth.guard.ts
:
import { Injectable } from "@angular/core";
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router
} from "@angular/router";
import { Observable } from "rxjs";
import { AuthenticationService } from "./authentication.service"; @Injectable({ providedIn: "root"
})
export class AuthGuard implements CanActivate { constructor(private auth: AuthenticationService, private router: Router) {} canActivate( next: ActivatedRouteSnapshot, state: RouterStateSnapshot ): | Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree { if (!this.auth.isLoggedIn()) { this.router.navigateByUrl("/"); return false; } return true; }
}
このガードを有効にするには、ルート設定でそれを宣言する必要があります。 というルートプロパティがあります canActivate
、ルートをアクティブ化する前に呼び出す必要があるサービスの配列を受け取ります。 ルートは次のように定義されています。 アプリルーティングモジュール、ここに示すようなルートが含まれています。
const routes: Routes = [ { path: "", component: HomeComponent }, { path: "login", component: LoginComponent }, { path: "register", component: RegisterComponent }, { path: "profile", component: ProfileComponent, canActivate: [AuthGuard] }
];
ファイル全体は次のようになります。
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { HomeComponent } from "./home/home.component";
import { LoginComponent } from "./login/login.component";
import { RegisterComponent } from "./register/register.component";
import { ProfileComponent } from "./profile/profile.component";
import { AuthGuard } from "./auth.guard"; const routes: Routes = [ { path: "", component: HomeComponent }, { path: "login", component: LoginComponent }, { path: "register", component: RegisterComponent }, { path: "profile", component: ProfileComponent, canActivate: [AuthGuard] }
]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule]
})
export class AppRoutingModule {}
このルート ガードを導入すると、認証されていないユーザーがプロフィール ページにアクセスしようとすると、Angular はルートの変更をキャンセルしてホームページにリダイレクトし、認証されていないユーザーからページを保護します。
保護された API ルートを呼び出す
/api/profile
ルートはリクエスト内の JWT をチェックするように設定されています。 それ以外の場合は、401 不正エラーが返されます。
トークンを API に渡すには、トークンをリクエストのヘッダーとして送信する必要があります。 Authorization
。 次のスニペットは、主要なデータ サービス関数と、トークンの送信に必要な形式を示しています。 の AuthenticationService
すでにこれを処理していますが、これは次の場所で見つけることができます /client/src/app/authentication.service.ts:
base$ = this.http.get(`/api/${type}`, { headers: { Authorization: `Bearer ${this.getToken()}` }
});
バックエンド コードは、リクエストが行われたときに、発行サーバーのみが知っているシークレットを使用して、トークンが本物であることを検証していることに注意してください。
これをプロフィール ページで使用するには、コントローラーを更新するだけです。 /client/src/app/profile/profile.component.ts サンプルアプリで。 これにより、 details
API が何らかのデータを返すときのプロパティ。 UserDetails
インタフェース:
import { Component, OnInit } from "@angular/core";
import { AuthenticationService, UserDetails } from "../authentication.service"; @Component({ templateUrl: "./profile.component.html", styleUrls: ["./profile.component.css"]
})
export class ProfileComponent implements OnInit { details: UserDetails; constructor(private auth: AuthenticationService) {} ngOnInit() { this.auth.profile().subscribe( user => { this.details = user; }, err => { console.error(err); } ); }
}
もちろん、これはビュー内のバインディングを更新するだけのケースです (/src/app/profile/profile.component.html)。 繰り返しになりますが、 ?.
これは、最初のレンダリング時に存在しないプロパティをバインドするための安全演算子です (データを最初に読み込む必要があるため)。
<div class="form-horizontal"> <div class="form-group"> <label class="col-sm-3 control-label">Full name</label> <p class="form-control-static">{{ details?.name }}</p> </div> <div class="form-group"> <label class="col-sm-3 control-label">Email</label> <p class="form-control-static">{{ details?.email }}</p> </div>
</div>
Angular アプリの実行
Angular アプリを実行するには、リクエストを次のようにルーティングする必要があります。 /api
で実行されている Express サーバーに http://localhost:3000/
。 これを行うには、 proxy.conf.json
内のファイル client
ディレクトリ:
touch proxy.conf.json
また、次のコンテンツを追加します。
{ "/api": { "target": "http://localhost:3000", "secure": false }
}
最後に、 start
スクリプトイン client/package.json
:
"start": "ng serve --proxy-config proxy.conf.json",
ここで、Mongo が実行されていることを確認し、次を使用してプロジェクトのルート内から Express アプリを起動します。 npm start
そして、内からAngularアプリを起動します。 client
同じコマンドを使用してディレクトリに移動します。
次に、 http://localhost:4200、(ほぼ)完成した製品を確認します。 でアカウントを登録してみます http://localhost:4200/register そしてログインして、すべてが正常に動作していることを確認します。
最後の仕上げ
お気づきかと思いますが、最終的なアプリにはスタイルがありません。 これは少し長いチュートリアルなので、ここには含めていません。 ただし、GitHub で完成したコードを見ると、そこからすべてを取得できます。 確認するファイルは次のとおりです。
これらのファイルから追加のマークアップをコピーすると、次のようになります。
これが、API ルートのセキュリティ保護やユーザー詳細の管理から、JWT の操作やルートの保護に至るまで、MEAN スタックで認証を管理する方法です。
出典: https://www.sitepoint.com/mean-stack-angular-angular-cli/?utm_source=rss