アプリケーションプログラミングインターフェイス(API)はいたるところにあります。これらは、ソフトウェアが他のソフトウェア(内部または外部)と一貫して通信できるようにします。これは、再利用性は言うまでもなく、スケーラビリティの重要な要素です。
今日、オンラインサービスが公開APIを使用することは非常に一般的です。これらにより、他の開発者はソーシャルメディアログイン、クレジットカード支払い、行動追跡などの機能を簡単に統合できます。ザ・ デファクト これに使用する標準は、REpresentational State Transfer(REST)と呼ばれます。
多数のプラットフォームとプログラミング言語をタスクに使用できますが、たとえば、 ASP.NETCore 、 Laravel(PHP) 、または ボトル(Python) —このチュートリアルでは、次のスタックを使用して、基本的で安全なRESTAPIバックエンドを構築します。
このチュートリアルに従う開発者は、ターミナル(またはコマンドプロンプト)にも慣れている必要があります。
注:ここではフロントエンドのコードベースについては説明しませんが、バックエンドがJavaScriptで記述されているため、スタック全体でコード(オブジェクトモデルなど)を共有すると便利です。
REST APIは、ステートレス操作の一般的なセットを使用してデータにアクセスして操作するために使用されます。これらの操作はHTTPプロトコルに不可欠であり、重要な作成、読み取り、更新、および削除(CRUD)機能を表しますが、クリーンな1対1の方法ではありません。
POST
(リソースを作成するか、通常はデータを提供します)GET
(リソースまたは個々のリソースのインデックスを取得します)PUT
(リソースの作成または置換)PATCH
(リソースの更新/変更)DELETE
(リソースを削除します)これらのHTTP操作とリソース名をアドレスとして使用して、操作ごとにエンドポイントを作成することでRESTAPIを構築できます。また、パターンを実装することで、安定したわかりやすい基盤が得られ、コードを迅速に進化させ、後で維持することができます。前述のように、サードパーティの機能を統合するために同じ基盤が使用され、そのほとんどは同様にREST APIを使用して、そのような統合をより高速にします。
とりあえず、Node.jsを使用して安全なRESTAPIの作成を始めましょう。
このチュートリアルでは、users
というリソース用の非常に一般的な(そして非常に実用的な)RESTAPIを作成します。
リソースの基本構造は次のとおりです。
id
(自動生成されたUUID)firstName
lastName
email
password
permissionLevel
(このユーザーは何をすることができますか?)そして、そのリソースに対して次の操作を作成します。
POST
エンドポイントで/users
(新しいユーザーを作成します)GET
エンドポイントで/users
(すべてのユーザーを一覧表示)GET
エンドポイントで/users/:userId
(特定のユーザーを取得します)PATCH
エンドポイントで/users/:userId
(特定のユーザーのデータを更新します)DELETE
エンドポイントで/users/:userId
(特定のユーザーを削除します)また、アクセストークンにはJSON Webトークン(JWT)を使用します。そのために、auth
という別のリソースを作成します。これは、ユーザーの電子メールとパスワードを期待し、その見返りとして、特定の操作での認証に使用されるトークンを生成します。 (DejanMilosevicのすばらしい記事 Javaでの安全なRESTアプリケーションのためのJWT これについてさらに詳しく説明します。原則は同じです。)
まず、最新のNode.jsバージョンがインストールされていることを確認してください。この記事では、バージョン14.9.0を使用します。古いバージョンでも動作する可能性があります。
次に、あなたが持っていることを確認してください MongoDB インストールされています。ここで使用するMongooseとMongoDBの詳細については説明しませんが、基本を実行するには、サーバーをサービスとしてではなく、インタラクティブモードで(つまり、コマンドラインからmongo
として)起動するだけです。これは、このチュートリアルのある時点で、Node.jsコードを介してではなく、MongoDBと直接対話する必要があるためです。
注:MongoDBでは、一部のRDBMSシナリオのように特定のデータベースを作成する必要はありません。 Node.jsコードからの最初の挿入呼び出しは、その作成を自動的にトリガーします。
このチュートリアルには、作業プロジェクトに必要なすべてのコードが含まれているわけではありません。代わりに、クローンを作成することを目的としています コンパニオンレポ 読み進めながらハイライトをたどるだけですが、必要に応じて、必要に応じてリポジトリから特定のファイルやスニペットをコピーすることもできます。
結果のrest-api-tutorial/
に移動しますターミナルのフォルダ。プロジェクトに3つのモジュールフォルダが含まれていることがわかります。
common
(すべての共有サービス、およびユーザーモジュール間で共有される情報の処理)users
(ユーザーに関するすべて)auth
(JWT生成とログインフローの処理)ここで、npm install
を実行します(または、お持ちの場合はyarn
)
おめでとうございます。これで、単純なRESTAPIバックエンドを実行するために必要なすべての依存関係とセットアップが完了しました。
使用します マングース 、MongoDBのオブジェクトデータモデリング(ODM)ライブラリで、ユーザースキーマ内にユーザーモデルを作成します。
まず、/users/models/users.model.js
でMongooseスキーマを作成する必要があります。
const userSchema = new Schema({ firstName: String, lastName: String, email: String, password: String, permissionLevel: Number });
スキーマを定義すると、スキーマをユーザーモデルに簡単にアタッチできます。
const userModel = mongoose.model('Users', userSchema);
その後、このモデルを使用して、Expressエンドポイント内で必要なすべてのCRUD操作を実装できます。
お金でクレジットカードを漏らした
users/routes.config.js
でルートを定義することにより、「ユーザーの作成」操作から始めましょう。
app.post('/users', [ UsersController.insert ]);
これは、メインのExpressアプリに取り込まれますindex.js
ファイル。 UsersController
オブジェクトはコントローラーからインポートされ、/users/controllers/users.controller.js
で定義されているパスワードを適切にハッシュします。
exports.insert = (req, res) => { let salt = crypto.randomBytes(16).toString('base64'); let hash = crypto.createHmac('sha512',salt) .update(req.body.password) .digest('base64'); req.body.password = salt + '$' + hash; req.body.permissionLevel = 1; UserModel.createUser(req.body) .then((result) => { res.status(201).send({id: result._id}); }); };
この時点で、サーバー(npm start
)を実行し、POST
を送信することで、Mongooseモデルをテストできます。 /users
へのリクエストいくつかのJSONデータを使用:
{ 'firstName' : 'Marcos', 'lastName' : 'Silva', 'email' : ' [email protected] ', 'password' : 's3cr3tp4sswo4rd' }
これに使用できるツールがいくつかあります。不眠症(以下で説明)とPostmanは人気のあるGUIツールであり、curl
一般的なCLIの選択です。たとえば、ブラウザの組み込みの開発ツールコンソールからJavaScriptを使用することもできます。
fetch('http://localhost:3600/users', { method: 'POST', headers: { 'Content-type': 'application/json' }, body: JSON.stringify({ 'firstName': 'Marcos', 'lastName': 'Silva', 'email': ' [email protected] ', 'password': 's3cr3tp4sswo4rd' }) }) .then(function(response) { return response.json(); }) .then(function(data) { console.log('Request succeeded with JSON response', data); }) .catch(function(error) { console.log('Request failed', error); });
この時点で、有効な投稿の結果は、作成されたユーザーからのIDのみになります:{ 'id': '5b02c5c84817bf28049e58a3' }
。 createUser
も追加する必要がありますusers/models/users.model.js
のモデルへのメソッド:
exports.createUser = (userData) => { const user = new User(userData); return user.save(); };
これで、ユーザーが存在するかどうかを確認する必要があります。そのために、次のエンドポイントに「IDでユーザーを取得」機能を実装します:users/:userId
。
まず、/users/routes/config.js
にルートを作成します。
app.get('/users/:userId', [ UsersController.getById ]);
次に、/users/controllers/users.controller.js
にコントローラーを作成します。
exports.getById = (req, res) => { UserModel.findById(req.params.userId).then((result) => { res.status(200).send(result); }); };
そして最後に、findById
を追加します/users/models/users.model.js
のモデルへのメソッド:
exports.findById = (id) => { return User.findById(id).then((result) => { result = result.toJSON(); delete result._id; delete result.__v; return result; }); };
応答は次のようになります。
{ 'firstName': 'Marcos', 'lastName': 'Silva', 'email': ' [email protected] ', 'password': 'Y+XZEaR7J8xAQCc37nf1rw==$p8b5ykUx6xpC6k8MryDaRmXDxncLumU9mEVabyLdpotO66Qjh0igVOVerdqAh+CUQ4n/E0z48mp8SDTpX2ivuQ==', 'permissionLevel': 1, 'id': '5b02c5c84817bf28049e58a3' }
ハッシュ化されたパスワードが表示されることに注意してください。このチュートリアルでは、パスワードを表示していますが、ハッシュ化されている場合でも、パスワードを公開しないことをお勧めします。もう1つ確認できるのは、permissionLevel
です。これは、後でユーザー権限を処理するために使用します。
上記のパターンを繰り返して、ユーザーを更新する機能を追加できるようになりました。 PATCH
を使用します変更したいフィールドのみを送信できるようになるためです。したがって、ルートはPATCH
になります。 /users/:userid
に送信すると、変更するフィールドが送信されます。変更は問題のユーザーまたは管理者に制限する必要があり、管理者のみがpermissionLevel
を変更できる必要があるため、追加の検証も実装する必要があります。とりあえずそれをスキップして、authモジュールを実装したらそれに戻ります。今のところ、コントローラーは次のようになります。
exports.patchById = (req, res) => { if (req.body.password){ let salt = crypto.randomBytes(16).toString('base64'); let hash = crypto.createHmac('sha512', salt).update(req.body.password).digest('base64'); req.body.password = salt + '$' + hash; } UserModel.patchUser(req.params.userId, req.body).then((result) => { res.status(204).send({}); }); };
デフォルトでは、リクエストが成功したことを示す応答本文のないHTTPコード204を送信します。
そして、patchUser
を追加する必要がありますモデルへの方法:
exports.patchUser = (id, userData) => { return User.findOneAndUpdate({ _id: id }, userData); };
ユーザーリストはGET
として実装されます/users/
で次のコントローラーによって:
exports.list = (req, res) => { let limit = req.query.limit && req.query.limit { res.status(200).send(result); }) };
対応するモデルメソッドは次のようになります。
exports.list = (perPage, page) => { return new Promise((resolve, reject) => { User.find() .limit(perPage) .skip(perPage * page) .exec(function (err, users) { if (err) { reject(err); } else { resolve(users); } }) }); };
結果のリスト応答は、次の構造になります。
[ { 'firstName': 'Marco', 'lastName': 'Silva', 'email': ' [email protected] ', 'password': 'z4tS/DtiH+0Gb4J6QN1K3w==$al6sGxKBKqxRQkDmhnhQpEB6+DQgDRH2qr47BZcqLm4/fphZ7+a9U+HhxsNaSnGB2l05Oem/BLIOkbtOuw1tXA==', 'permissionLevel': 1, 'id': '5b02c5c84817bf28049e58a3' }, { 'firstName': 'Paulo', 'lastName': 'Silva', 'email': ' [email protected] ', 'password': 'wTsqO1kHuVisfDIcgl5YmQ==$cw7RntNrNBNw3MO2qLbx959xDvvrDu4xjpYfYgYMxRVDcxUUEgulTlNSBJjiDtJ1C85YimkMlYruU59rx2zbCw==', 'permissionLevel': 1, 'id': '5b02d038b653603d1ca69729' } ]
そして、実装される最後の部分はDELETE
です。 /users/:userId
で。
削除するコントローラーは次のとおりです。
exports.removeById = (req, res) => { UserModel.removeById(req.params.userId) .then((result)=>{ res.status(204).send({}); }); };
以前と同じように、コントローラーは確認としてHTTPコード204を返し、コンテンツ本文は返しません。
対応するモデルメソッドは次のようになります。
exports.removeById = (userId) => { return new Promise((resolve, reject) => { User.deleteMany({_id: userId}, (err) => { if (err) { reject(err); } else { resolve(err); } }); }); };
これで、ユーザーリソースを操作するために必要なすべての操作が完了し、ユーザーコントローラーが完成しました。このコードの主なアイデアは、RESTパターンを使用するためのコアコンセプトを提供することです。いくつかの検証と権限を実装するには、このコードに戻る必要がありますが、最初に、セキュリティの構築を開始する必要があります。 authモジュールを作成しましょう。
users
を保護する前にパーミッションと検証ミドルウェアを実装することでモジュールを作成するには、現在のユーザーの有効なトークンを生成できる必要があります。ユーザーが有効な電子メールとパスワードを提供したことに応じて、JWTを生成します。 JWTは注目に値するJSONWebトークンであり、ユーザーが繰り返し検証することなく、複数のリクエストを安全に実行できるようにするために使用できます。通常、有効期限があり、通信を安全に保つために、新しいトークンが数分ごとに再作成されます。ただし、このチュートリアルでは、トークンの更新を省略し、ログインごとに1つのトークンを使用してトークンをシンプルに保ちます。
まず、POST
のエンドポイントを作成します/auth
へのリクエスト資源。リクエストの本文には、ユーザーのメールアドレスとパスワードが含まれます。
{ 'email' : ' [email protected] ', 'password' : 's3cr3tp4sswo4rd2' }
コントローラを使用する前に、/authorization/middlewares/verify.user.middleware.js
でユーザーを検証する必要があります。
exports.isPasswordAndUserMatch = (req, res, next) => { UserModel.findByEmail(req.body.email) .then((user)=>{ if(!user[0]){ res.status(404).send({}); }else{ let passwordFields = user[0].password.split('$'); let salt = passwordFields[0]; let hash = crypto.createHmac('sha512', salt).update(req.body.password).digest('base64'); if (hash === passwordFields[1]) { req.body = { userId: user[0]._id, email: user[0].email, permissionLevel: user[0].permissionLevel, provider: 'email', name: user[0].firstName + ' ' + user[0].lastName, }; return next(); } else { return res.status(400).send({errors: ['Invalid email or password']}); } } }); };
これが完了したら、コントローラーに移動してJWTを生成できます。
exports.login = (req, res) => { try { let refreshId = req.body.userId + jwtSecret; let salt = crypto.randomBytes(16).toString('base64'); let hash = crypto.createHmac('sha512', salt).update(refreshId).digest('base64'); req.body.refreshKey = salt; let token = jwt.sign(req.body, jwtSecret); let b = Buffer.from(hash); let refresh_token = b.toString('base64'); res.status(201).send({accessToken: token, refreshToken: refresh_token}); } catch (err) { res.status(500).send({errors: err}); } };
このチュートリアルではトークンを更新しませんが、コントローラーは、そのような生成を有効にして、後続の開発での実装を容易にするように設定されています。
ここで必要なのは、ルートを作成し、/authorization/routes.config.js
で適切なミドルウェアを呼び出すことだけです。
app.post('/auth', [ VerifyUserMiddleware.hasAuthValidFields, VerifyUserMiddleware.isPasswordAndUserMatch, AuthorizationController.login ]);
応答には、生成されたJWTがaccessTokenフィールドに含まれます。
{ 'accessToken': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI1YjAyYzVjODQ4MTdiZjI4MDQ5ZTU4YTMiLCJlbWFpbCI6Im1hcmNvcy5oZW5yaXF1ZUB0b3B0YWwuY29tIiwicGVybWlzc2lvbkxldmVsIjoxLCJwcm92aWRlciI6ImVtYWlsIiwibmFtZSI6Ik1hcmNvIFNpbHZhIiwicmVmcmVzaF9rZXkiOiJiclhZUHFsbUlBcE1PakZIRG1FeENRPT0iLCJpYXQiOjE1MjY5MjMzMDl9.mmNg-i44VQlUEWP3YIAYXVO-74803v1mu-y9QPUQ5VY', 'refreshToken': 'U3BDQXBWS3kyaHNDaGJNanlJTlFkSXhLMmFHMzA2NzRsUy9Sd2J0YVNDTmUva0pIQ0NwbTJqOU5YZHgxeE12NXVlOUhnMzBWMGNyWmdOTUhSaTdyOGc9PQ==' }
トークンを作成したら、Authorization
内で使用できます。 Bearer ACCESS_TOKEN
の形式を使用したヘッダー。
最初に定義する必要があるのは、誰がusers
を使用できるかです。資源。処理する必要があるシナリオは次のとおりです。
これらのシナリオを特定したら、最初に、ユーザーが有効なJWTを使用しているかどうかを常に検証するミドルウェアが必要になります。 /common/middlewares/auth.validation.middleware.js
のミドルウェア次のように単純にすることができます。
exports.validJWTNeeded = (req, res, next) => { if (req.headers['authorization']) { try { let authorization = req.headers['authorization'].split(' '); if (authorization[0] !== 'Bearer') { return res.status(401).send(); } else { req.jwt = jwt.verify(authorization[1], secret); return next(); } } catch (err) { return res.status(403).send(); } } else { return res.status(401).send(); } };
リクエストエラーの処理にはHTTPエラーコードを使用します。
ビットごとのAND演算子(ビットマスキング)を使用して、アクセス許可を制御できます。必要な各パーミッションを2の累乗として設定すると、32ビット整数の各ビットを1つのパーミッションとして扱うことができます。管理者は、権限値を2147483647に設定することにより、すべての権限を持つことができます。そのユーザーは、任意のルートにアクセスできます。別の例として、アクセス許可の値が7に設定されているユーザーは、値1、2、および4(2の0、1、および2の累乗)のビットでマークされたロールへのアクセス許可を持ちます。
そのためのミドルウェアは次のようになります。
exports.minimumPermissionLevelRequired = (required_permission_level) => { return (req, res, next) => { let user_permission_level = parseInt(req.jwt.permission_level); let user_id = req.jwt.user_id; if (user_permission_level & required_permission_level) { return next(); } else { return res.status(403).send(); } }; };
ミドルウェアは汎用です。ユーザーのアクセス許可レベルと必要なアクセス許可レベルが少なくとも1ビットで一致する場合、結果はゼロより大きくなり、アクションを続行できます。それ以外の場合は、HTTPコード403が返されます。
次に、認証ミドルウェアを/users/routes.config.js
のユーザーのモジュールルートに追加する必要があります。
app.post('/users', [ UsersController.insert ]); app.get('/users', [ ValidationMiddleware.validJWTNeeded, PermissionMiddleware.minimumPermissionLevelRequired(PAID), UsersController.list ]); app.get('/users/:userId', [ ValidationMiddleware.validJWTNeeded, PermissionMiddleware.minimumPermissionLevelRequired(FREE), PermissionMiddleware.onlySameUserOrAdminCanDoThisAction, UsersController.getById ]); app.patch('/users/:userId', [ ValidationMiddleware.validJWTNeeded, PermissionMiddleware.minimumPermissionLevelRequired(FREE), PermissionMiddleware.onlySameUserOrAdminCanDoThisAction, UsersController.patchById ]); app.delete('/users/:userId', [ ValidationMiddleware.validJWTNeeded, PermissionMiddleware.minimumPermissionLevelRequired(ADMIN), UsersController.removeById ]);
これで、RESTAPIの基本的な開発は終わりです。やらなければならないことは、それをすべてテストすることだけです。
不眠症 良い無料バージョンのまともなRESTクライアントです。もちろん、ベストプラクティスは、コードテストを含め、プロジェクトに適切なエラーレポートを実装することですが、サードパーティのRESTクライアントは、エラーレポートとサービスのデバッグが利用できない場合に、サードパーティのソリューションをテストおよび実装するのに最適です。ここでは、これを使用してアプリケーションの役割を果たし、APIで何が起こっているかについての洞察を得ます。
ユーザーを作成するには、POST
を実行する必要があります。必要なフィールドを適切なエンドポイントに送信し、生成されたIDを後で使用できるように保存します。
APIはユーザーIDで応答します:
これで、/auth/
を使用してJWTを生成できます。終点:
応答としてトークンを取得する必要があります。
近接のゲシュタルト法則の例
accessToken
を取得し、接頭辞Bearer
を付けます(スペースを覚えておいてください)、それをAuthorization
の下のリクエストヘッダーに追加します。
パーミッションミドルウェアを実装したのでこれを行わないと、登録以外のすべてのリクエストはHTTPコード401を返します。ただし、有効なトークンが設定されていると、/users/:userId
から次の応答が返されます。
また、前述のとおり、教育目的およびわかりやすくするために、すべてのフィールドを表示しています。パスワード(ハッシュまたはその他)は、応答に表示されないようにする必要があります。
ユーザーのリストを取得してみましょう。
驚き! 403応答を受け取ります。
ユーザーには、このエンドポイントにアクセスするための権限がありません。 permissionLevel
を変更する必要がありますユーザーの1から7まで(または、無料と有料のアクセス許可レベルがそれぞれ1と4として表されるため、5でもかまいません)。これは、MongoDBのインタラクティブなプロンプトで、次のように手動で行うことができます(IDを使用)ローカル結果に変更):
db.users.update({'_id' : ObjectId('5b02c5c84817bf28049e58a3')},{$set:{'permissionLevel':5}})
次に、新しいJWTを生成する必要があります。
それが行われた後、適切な応答が得られます。
次に、PATCH
を送信して更新機能をテストしましょう。 /users/:userId
にいくつかのフィールドを指定してリクエストします終点:
操作が成功したことの確認として204の応答が期待されますが、ユーザーにもう一度確認を要求することができます。
最後に、ユーザーを削除する必要があります。上記のように新しいユーザーを作成し(ユーザーIDを忘れないでください)、管理者ユーザーに適切なJWTがあることを確認する必要があります。新しいユーザーが削除操作を実行できるようにするには、権限を2053(2048— ADMIN
—および以前の5)に設定する必要があります。これが完了し、新しいJWTが生成されたら、Authorization
を更新する必要がありますリクエストヘッダー:
DELETE
を送信する/users/:userId
にリクエストすると、確認として204の応答が返されます。繰り返しになりますが、/users/
をリクエストして確認できます既存のすべてのユーザーを一覧表示します。
このチュートリアルで説明するツールとメソッドを使用すると、Node.jsでシンプルで安全なRESTAPIを作成できるようになります。プロセスに不可欠ではない多くのベストプラクティスがスキップされたため、次のことを忘れないでください。
common/config/env.config.js
から移動しますオフレポ、非環境ベース 秘密の配布メカニズム 読者にとっての最後の演習の1つは、コードベースをJavaScriptPromiseの使用から 非同期/待機 技術。
興味があるかもしれないあなたのために、今もあります TypeScriptバージョン 利用可能なプロジェクトの。
関連: REST仕様でこれまでに行ったことのない5つのこと