JavaScript 在網絡上無處不在。基本上每個網頁都或多或少包含一些 JavaScript 代碼,即使沒有,你的瀏覽器或許也裝了一些拓展附件,這些拓展附件能夠將一部分 JavaScript 代碼註入網頁中。2018 年依舊如此。

JavaScript 也可以在瀏覽器之外使用,從托管網絡服務器到控製 RC 汽車或者運行一個成熟的操作系統。有些時候你想要兩臺服務器互相通信,無論是本地網絡還是互聯網上。

今天,我會向你展示怎樣使用 node.js 創建一個 REST API,同時使用 OAuth2.0 維護它,以防止未經授權的請求。REST API 遍布整個網絡,但是如果沒有合適的工具那麽就需要大量的模板代碼。我會向你展示怎樣使用兩個神奇的工具,這兩個工具能使工作更輕松,包括用 OKta 實現客戶端憑證流(翻譯「Client Credentials Flow」這個詞組時自己認慫了,只好用百度翻譯了 😂。),它能不使用用戶上下文而將兩臺機器安全地連接在一起。

構建一個 REST 類型的 Node API 服務器

通過使用 Express JavaScript library 能夠讓創建 Node 網絡服務器更加簡單。創建一個文件夾用來包含你的服務器項目。

$ mkdir rest-api

Node 使用 package.json 文件來管理依賴項並且規範(感覺「define」翻譯成「規範」更合適。)你的項目。使用 npm init 命令創建一個,(執行這個命令後)會問你一些問題,這些問題是用來幫你初始化這個項目。截止到現在,你能夠使用標準的 JS 來強製規範你的編碼,並且將其用作測試。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
$ cd rest-api

$ npm init
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

See `npm help json` for definitive documentation on these fields
and exactly what they do.

Use `npm install <pkg>` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.
package name: (rest-api)
version: (1.0.0)
description: A parts catalog
entry point: (index.js)
test command: standard
git repository:
keywords:
author:
license: (ISC)
About to write to /Users/Braden/code/rest-api/package.json:

{
"name": "rest-api",
"version": "1.0.0",
"description": "A parts catalog",
"main": "index.js",
"scripts": {
"test": "standard"
},
"author": "",
"license": "ISC"
}


Is this OK? (yes)

默認的(文件)入口是 index.js,所以你應該用這個名稱創建一個新文件。下面的代碼給你提供了一個相當基礎的服務器(案例),它只是監聽了本機(127.0.0.1)3000 端口,但是沒有做任何其他事情。

index.js 文件內容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const express = require('express')
const bodyParser = require('body-parser')
const { promisify } = require('util')

const app = express()
app.use(bodyParser.json())

const startServer = async () => {
const port = process.env.SERVER_PORT || 3000
await promisify(app.listen).bind(app)(port)
console.log(`Listening on port ${port}`)
}

startServer()

(分號是自己添加的,絕大多數情況下沒有分號程序不會報錯,但有一些特殊情況沒了會報錯,還是養成添加分號的習慣吧。)

util 模塊中的 promisify 功能讓你傳入一個具有(傳統)回調功能的函數,而返回給你一個 Promise 式的回調函數,Promise 是異步處理的新標準。這也同樣讓我們使用最近比較新的 async/await 語法,能夠使代碼更優雅。

為了使其運行起來,你需要裝上你在文件開頭 require 的依賴包。通過使用 npm intall 安裝他們。依賴信息將會自動保存到你的 package.json 文件,並將依賴包安裝到本地 node_modules 文件夾中。

註意:你不應該將 node_modules(文件夾及其內容)提交到資源管理器中,因為它會很快變大(占據空間),(再者)package.json 文件(已經)保存了你使用的每一個依賴包的確切版本,如果你在另一臺計算機上安裝它,會得到相同的代碼。

$ npm install express@4.16.3 util@0.11.0

對於一些快速更新的依賴包(我猜是這個意思。),安裝 standard 作為 dev 依賴項,然後運行起來以確保正常。

1
2
3
4
5
$ npm install --save-dev standard@11.0.1
$ npm test

> rest-api@1.0.0 test /Users/bmk/code/okta/apps/rest-api
> standard

如果一切正常,你不應該看到在 > standard 這一行有任何輸出內容。如果報錯,可能是這個樣子:

1
2
3
4
5
6
7
8
9
10
11
12
$ npm test

> rest-api@1.0.0 test /Users/bmk/code/okta/apps/rest-api
> standard

standard: Use JavaScript Standard Style (https://standardjs.com)
standard: Run `standard --fix` to automatically fix some problems.
/Users/Braden/code/rest-api/index.js:3:7: Expected consistent spacing
/Users/Braden/code/rest-api/index.js:3:18: Unexpected trailing comma.
/Users/Braden/code/rest-api/index.js:3:18: A space is required after ','.
/Users/Braden/code/rest-api/index.js:3:38: Extra semicolon.
npm ERR! Test failed. See above for more details.

既然你的代碼已經準備好而且依賴包已經安裝好,你可以通過 node .(命令)運行你的服務。(. 指的是查看當前文件夾,檢查 package.json 文件找到此目錄中要使用的主文件是 index.js。):

$ node .

為了測試服務是否正在運行,你可以使用 curl 命令。(由於)沒有 ENDPOINTS(「ENDPOINTS」只可意會不可言傳,不知如何翻譯。),所以會返回一個錯誤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ curl localhost:3000 -i HTTP/1.1 404 Not Found X-Powered-By: Express
Content-Security-Policy: default-src 'self' X-Content-Type-Options: nosniff
Content-Type: text/html; charset=utf-8 Content-Length: 139 Date: Thu, 16 Aug
2018 01:34:53 GMT Connection: keep-alive

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Error</title>
</head>
<body>
<pre>Cannot GET /</pre>
</body>
</html>

盡管報了一個錯誤,但依舊是好消息。你還沒有設置任何 ENDPOINTS,所以 Express 返回一個 404 錯誤。如果你的服務根本沒有運行,則會得到如下錯誤:

1
2
$ curl localhost:3000 -i
curl: (7) Failed to connect to localhost port 3000: Connection refused

使用 Node、Express、Sequelize 和 Epilogue 構建 REST 類型的 API

既然你已經有了一個能夠工作的 Express 服務器,那麽就可以添加一個 REST API。事實上這比你想象的容易得多。使用 Sequelize 規範數據庫模式是我見過的最簡單的方法,而使用 Epilogue 創建 REST API ENDPOINTS 幾乎達到了零樣板。

你需要添加這些依賴包到你的項目中。Sequelize 需要知道怎樣與數據庫進行通信。現在,使用 SQLite,因為它能讓我們快速啟動和運行。

$ npm install sequelize@4.38.0 epilogue@0.7.1 sqlite3@4.0.2

用下面的代碼創建一個 database.js 文件。我會接下來更詳細地解釋每一部分。

database.js 文件內容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const Sequelize = require('sequelize')
const epilogue = require('epilogue')

const database = new Sequelize({
dialect: 'sqlite',
storage: './test.sqlite',
operatorsAliases: false,
})

const Part = database.define('parts', {
partNumber: Sequelize.STRING,
modelNumber: Sequelize.STRING,
name: Sequelize.STRING,
description: Sequelize.TEXT,
})

const initializeDatabase = async (app) => {
epilogue.initialize({ app, sequelize: database })

epilogue.resource({
model: Part,
endpoints: ['/parts', '/parts/:id'],
})

await database.sync()
}

module.exports = initializeDatabase

現在你只需要導入這個文件到你的主應用程序並運行初始化功能。在你的 index.js 文件中添加如下內容。

index.js 文件內容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@@ -2,10 +2,14 @@ const express = require('express');
const bodyParser = require('body-parser');
const { promisify } = require('util');

+const initializeDatabase = require('./database');
+
const app = express();
app.use(bodyParser.json());

const startServer = async () => {
+ await initializeDatabase(app);
+
const port = process.env.SERVER_PORT || 3000;
await promisify(app.listen).bind(app)(port);
console.log(`Listening on port ${port}`);
...

你現在能測試語法錯誤,如果一切正常運行這個程序:

1
2
3
4
5
6
7
8
9
$ npm test && node .

> rest-api@1.0.0 test /Users/bmk/code/okta/apps/rest-api
> standard

Executing (default): CREATE TABLE IF NOT EXISTS `parts` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `partNumber` VARCHAR(255), `modelNu
mber` VARCHAR(255), `name` VARCHAR(255), `description` TEXT, `createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL);
Executing (default): PRAGMA INDEX_LIST(`parts`)
Listening on port 3000

在另一個終端中,你能測試這是否真實有效(我使用 json CLI 來格式化 JSON 響應,用 npm install –global json 進行全局安裝。):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
$ curl localhost:3000/parts
[]

$ curl localhost:3000/parts -X POST -d '{
"partNumber": "abc-123",
"modelNumber": "xyz-789",
"name": "Alphabet Soup",
"description": "Soup with letters and numbers in it"
}' -H 'content-type: application/json' -s0 | json
{
"id": 1,
"partNumber": "abc-123",
"modelNumber": "xyz-789",
"name": "Alphabet Soup",
"description": "Soup with letters and numbers in it",
"updatedAt": "2018-08-16T02:22:09.446Z",
"createdAt": "2018-08-16T02:22:09.446Z"
}

$ curl localhost:3000/parts -s0 | json
[
{
"id": 1,
"partNumber": "abc-123",
"modelNumber": "xyz-789",
"name": "Alphabet Soup",
"description": "Soup with letters and numbers in it",
"createdAt": "2018-08-16T02:22:09.446Z",
"updatedAt": "2018-08-16T02:22:09.446Z"
}
]

Node API 工作原理

如果你著急看後面的可以跳過本部分,但我保證你會有收獲。

Sequelize 函數會創建一個數據庫。這是你配置詳細信息的地方,例如你要用 SQL 的什麽規範。現在,使用 SQLite 快速啟動和運行。

1
2
3
4
5
const database = new Sequelize({
dialect: 'sqlite',
storage: './test.sqlite',
operatorsAliases: false,
})

一旦你創建了數據庫,就可以使用 database.define 文件為每一個表定義模式。創建一個名為 parts 的表,其中包含了一些有用的字段以跟蹤 PARTS。默認情況下,當你創建或者更新一個 row 時,Sequelize 會自動創建更新 idcreatedAtupdatedAt 字段。

1
2
3
4
5
6
const Part = database.define('parts', {
partNumber: Sequelize.STRING,
modelNumber: Sequelize.STRING,
name: Sequelize.STRING,
description: Sequelize.TEXT,
})

Epilogue 需要連接 Express 中的 app 變量才能添加 ENDPOINTS。然而,app 變量是在另外一個文件定義的。解決這個問題的一個方法是 export(導出)一個函數,這個函數包含 app 變量並對其進行處理。在另一個(使用 app 變量的)文件中,你可以 import(導入)這個腳本,像 initializeDatabase(app) 一樣運行它。

Epilogue 需要用 appdatabase 進行初始化。然後定義你想要使用的 REST ENDPOINTS。resource 函數會包括 GETPOSTPUTDELETE 功能的 ENDPOINTS,主要用來自動執行的。

為了準確創建數據庫,你需要運行 database.sync(),這將返回一個 Promise 對象。在啟動服務器之前,你需要等待它結束。

module.exports 命令表示 initializeDatabase 函數可以被其他文件導入。

1
2
3
4
5
6
7
8
9
10
11
12
const initializeDatabase = async (app) => {
epilogue.initialize({ app, sequelize: database })

epilogue.resource({
model: Part,
endpoints: ['/parts', '/parts/:id'],
})

await database.sync()
}

module.exports = initializeDatabase

使用 OAuth2.0 來保護你的 Node + Express REST API

現在你已經啟動了一個 REST API 並且運行起來,假設你想要一個特定的應用程序從遠程位置使用它。如果你把它按原樣放在互聯網上,那麽任何人都可以隨意添加、修改或者刪除部件。

為了避免這種情況,你可以使用 OAuth2.0 客戶端憑據流。這是一種讓兩個服務器相互通信而不需要用戶上下文的方法。兩臺服務器必須達成協議才能使用第三方授權服務器。假設有兩個服務器,A 和 B,以及一個授權服務器,服務器 A 托管 REST API,服務器 B 希望訪問這個 API。

  • 服務器 B 向授權服務器發送一個密鑰以驗證身份並且同時請求一個臨時令牌(temporary token)。
  • 然後服務器 B 像往常一樣使用(consume)REST API,但隨著請求一起發送令牌。
  • 服務器 A 向授權服務器請求一些可用於驗證(verify)令牌的元數據(metadata)。
  • 服務器 A 驗證服務器 B 的請求。
    • 如果有效(valid),將會發送成功的響應。
    • 如果令牌無效(invalid),則發送錯誤信息,並且不會泄露(leaked)敏感信息(sensitive information)。

創建一個授權服務器

這裏是開始使用 Okta 的地方。Okta 能夠充當一個授權服務器用來保護你的數據。你可能會問自己「為什麽用 Okta?」。那是因為,使用 Okta(不僅)構建一個 REST 應用程序相當炫酷,(而且)構建一個安全的 REST 應用程序更炫酷。要實現這些,你需要添加身份驗證(authentication ),以便用戶在查看/修改(viewing/modifying)之前必須登錄。Okta 的目標是使身份管理(identity management)比你往常使用的更簡單、更安全而且更具有擴展性(scalable)。Okta 是一種雲服務,允許開發者創建、編輯和安全存儲用戶賬戶和用戶賬戶數據,並將它們與一個或者多個應用程序連接。我們的 API 使你能夠:

如果你還沒有,請註冊一個永久免費的賬戶並且開始吧!

創建賬戶後,登陸開發人員控製臺,導航到 API,然後轉到 Authorization Servers 選項,點擊指向默認服務器的鏈接。

Settings 選項中,復製 Issuer 字段。你需要將此保存到你的 Node 應用程序可以讀取的地方。在項目中,創建一個名為 .env 的文件,如下所示:

.env 文件內容:ISSUER=https://{yourOktaDomain}/oauth2/default

ISSUER 的值應該是設置頁 Issuer URI 字段的值。

註意:通常情況下,你不應該把 .env 文件存儲在源代碼管理器(source control)中。這允許多個項目使用相同的源代碼(source code),而不需要單獨的分叉(fork)。它可以確保你的安全信息不會被公開(尤其是你將要把源代碼作為開源代碼發布時)。

接下來,導航到 Scopes 選項卡,點擊 Add Scope 按鈕並為你的 REST API 創建一個作用域(scope)。你需要給它起個名字(比如 parts_manager),如果你願意你可以給它一個描述(介紹)。

你還應該將作用域名稱添加到 .env 文件中,這樣代碼就可以訪問它。

.env 文件內容:

1
2
ISSUER=https://{yourOktaDomain}/oauth2/default
SCOPE=parts_manager

現在你需要創建一個客戶端。導航到 Applications,然後點擊 Add Application,選擇 Service,然後點擊 Next。輸入你的服務名稱(例如 Parts Manager),然後點擊 Done

這將帶你到一個具有你的客戶憑據(credentials)的頁面。這些是服務器 B(將使用 RESTAPI 的服務器)進行身份驗證所需的憑據。對於本例,客戶機和服務器代碼在同一個存儲庫中,因此繼續將此數據添加到 .env 文件中。確保用此頁中的值替換 {yourClientId}{yourClientSecret}

1
2
CLIENT_ID={yourClientId}
CLIENT_SECRET={yourClientSecret}

創建中間件驗證 Express 中的令牌

在 Express 中,你可以添加將在每個端點之前運行的中間件。然後,你可以添加元數據、設置頭、記錄一些信息,甚至提前取消請求並發送錯誤消息。在這種情況下,你需要創建一些中間件來驗證客戶機發送的令牌。如果令牌有效,它將繼續到 REST API 並返回適當的響應(appropriate response)。如果令牌無效,它將改為響應一條錯誤消息,以便只有授權的計算機可以訪問。

為了驗證令牌,可以使用 Okta 的中間件。你還需要一個名為 dotenv 的工具來加載環境變量:

1
npm install dotenv@6.0.0 @okta/jwt-verifier@0.0.12

現在創建一個名為 auth.js 的文件,該文件將導出中間件:

auth.js 文件內容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const OktaJwtVerifier = require('@okta/jwt-verifier')

const oktaJwtVerifier = new OktaJwtVerifier({ issuer: process.env.ISSUER })

module.exports = async (req, res, next) => {
try {
const { authorization } = req.headers
if (!authorization) throw new Error('You must send an Authorization header')

const [authType, token] = authorization.trim().split(' ')
if (authType !== 'Bearer') throw new Error('Expected a Bearer token')

const { claims } = await oktaJwtVerifier.verifyAccessToken(token)
if (!claims.scp.includes(process.env.SCOPE)) {
throw new Error('Could not verify the proper scope')
}
next()
} catch (error) {
next(error.message)
}
}

這個函數首先檢查 authorization 是否在請求中,否則拋出錯誤。如果它存在,它應該看起來像 Bearer {token},其中 {token}JWT 字符串。如果(請求)頭不是以 bearer 開頭將會引發另一個錯誤。然後我們把令牌發送給 Okta 的 JWT 驗證器 以驗證令牌。如果令牌無效,JWT 驗證器將拋出一個錯誤。否則,它將返回包含一些信息的對象。然後你可以驗證它是否包含你期望的作用域。

如果一切都成功了,它將調用(call)不帶任何參數(parameters)的 next() 函數。它告訴 Express 可以轉到鏈中的下一個函數(另一個中間件或最後一個 ENDPOINT)。如果你傳遞一個字符串給下一個函數,Express 會將其視為一個錯誤,該錯誤將傳遞回客戶機,並且不會在鏈中繼續。

你仍然需要導入這個函數並將其作為中間件添加到應用程序中。你也同樣需要在 index 文件頭部加載 dotenv,以保證來自 .env 的環境變量加載到你的應用程序中。對 index.js 做出以下更改。

index.js 文件內容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@@ -1,11 +1,14 @@
+require('dotenv').config();
const express = require('express');
const bodyParser = require('body-parser');
const { promisify } = require('util');

+const authMiddleware = require('./auth');
const initializeDatabase = require('./database');

const app = express();
app.use(bodyParser.json());
+app.use(authMiddleware);

const startServer = async () => {
await initializeDatabase(app);
...

要測試請求是否被正確阻止,請嘗試再次運行它…

$ npm test && node .

…然後在另一個終端中運行一些 curl 命令來測試:

需要授權頭

1
2
3
4
5
6
7
8
9
10
11
$ curl localhost:3000/parts
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Error</title>
</head>
<body>
<pre>You must send an Authorization header</pre>
</body>
</html>

授權頭中需要無記名令牌(Bearer token)

1
2
3
4
5
6
7
8
9
10
11
$ curl localhost:3000/parts -H 'Authorization: Basic asdf:1234'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Error</title>
</head>
<body>
<pre>Expected a Bearer token</pre>
</body>
</html>

令牌有效

1
2
3
4
5
6
7
8
9
10
11
$ curl localhost:3000/parts -H 'Authorization: Bearer asdf'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Error</title>
</head>
<body>
<pre>Jwt cannot be parsed</pre>
</body>
</html>

使用 Node 創建測試客戶端

你現在已經為沒有有效令牌的人禁用了對應用程序的訪問,但是你如何獲取令牌並使用它?我將向你展示如何用 Node 編寫一個簡單的客戶機,這也將幫助你測試有效的令牌是否有效。

$ npm install btoa@1.2.1 request-promise@4.2.2

client.js 文件內容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
require('dotenv').config()
const request = require('request-promise')
const btoa = require('btoa')

const { ISSUER, CLIENT_ID, CLIENT_SECRET, SCOPE } = process.env

const [, , uri, method, body] = process.argv
if (!uri) {
console.log('Usage: node client {url} [{method}] [{jsonData}]')
process.exit(1)
}

const sendAPIRequest = async () => {
const token = btoa(`${CLIENT_ID}:${CLIENT_SECRET}`)
try {
const auth = await request({
uri: `${ISSUER}/v1/token`,
json: true,
method: 'POST',
headers: {
authorization: `Basic ${token}`,
},
form: {
grant_type: 'client_credentials',
scope: SCOPE,
},
})

const response = await request({
uri,
method,
body,
headers: {
authorization: `${auth.token_type} ${auth.access_token}`,
},
})

console.log(response)
} catch (error) {
console.log(`Error: ${error.message}`)
}
}

sendAPIRequest()

這裏的代碼將來自 .env 文件中的變量加載到環境中,然後從 Node 中獲取變量。Node 將環境變量存儲在 process.env 文件中(process 是一個全局變量,包含一系列有用的變量和函數。)。

1
2
3
4
require('dotenv').config()
// ...
const { ISSUER, CLIENT_ID, CLIENT_SECRET, SCOPE } = process.env
// ...

接下來,由於這將從命令行運行,你可以再次使用 process 來獲取用 process.argv 傳入的參數。這將為你提供一個數組,其中包含傳入的所有參數。前兩個逗號(commas)前面沒有變量名,因為前兩個逗號在本例中並不重要;它們只是通向 node 的路徑和腳本的名稱(clientclient.js)。

URL 是必需的,它將包括 ENDPOINT,但方法和 JSON 數據是可選的(optional)。默認的方法是 GET,所以如果你只是提取數據,你可以忽略掉它。在這種情況下,你也不需要任何有效載荷(payload)。如果參數看起來不正確,那麽這將退出程序,並顯示錯誤消息和退出代碼** 1 **來表示錯誤。

1
2
3
4
5
const [, , uri, method, body] = process.argv
if (!uri) {
console.log('Usage: node client {url} [{method}] [{jsonData}]')
process.exit(1)
}

Node 當前不允許在主線程中(main thread)await,因此要使用簡潔的 async/await 語法,你不得不創建一個函數,然後調用它。

If an error occurs in any of the awaited functions(如果在任何 awaited 函數中發生錯誤), the try/catch they will be printed out to the screen.

1
2
3
4
5
6
7
8
9
const sendAPIRequest = async () => {
try {
// ...
} catch (error) {
console.error(`Error: ${error.message}`)
}
}

sendAPIRequest()

這是客戶端向授權服務器發送令牌請求的地方。為了使用授權服務器本身進行授權,你需要使用 Basic Auth。Basic Auth 與瀏覽器在獲得一個要求用戶名和密碼的內置彈出窗口時使用的相同。假設(say)你的用戶名是 AzureDiamond,密碼是 hunter2。你的瀏覽器將它們與冒號(英文冒號)連接在一起,然後用 base64(這是 btoa 函數所做的)對它們進行編碼(encode),以獲得 QXp1cmVEaWFtb25kOmh1bnRlcjI=。然後發送 Basic QXp1cmVEaWFtb25kOmh1bnRlcjI= 授權頭。然後,服務器可以使用 base64 解碼(decode)令牌以獲取用戶名和密碼。

傳統的授權(原文是「Basic authorization」。)本質上(inherently )不安全,因為它很容易解碼,這就是為什麽 https 對於防止中間人攻擊(a man-in-the-middle attack)很重要的原因。在這裏,客戶機 ID(client ID)和客戶機 Secret(client secret)分別是用戶名和密碼,這也是為什麽你的 CLIENT_IDCLIENT_SECRET 要保密的原因。

對於 OAuth2.0,你還需要指定(specify)授權類型(grant type),在本例中,它是 client_credentials,因為你計劃在兩臺計算機之間進行對話。你還需要指定作用域。這裏可以添加許多其他選項,但這是我們演示所需的全部內容。

1
2
3
4
5
6
7
8
9
10
11
12
13
const token = btoa(`${CLIENT_ID}:${CLIENT_SECRET}`)
const auth = await request({
uri: `${ISSUER}/v1/token`,
json: true,
method: 'POST',
headers: {
authorization: `Basic ${token}`,
},
form: {
grant_type: 'client_credentials',
scope: SCOPE,
},
})

一旦你通過了身份驗證(authenticated),將獲得一個訪問令牌,你可以將其發送到你的 REST API,它看起來應該類似於 Bearer eyJra…HboUg(實際令牌比這個長得多,可能大約有 800 個字符。)。令牌包含了你向 REST API 證明你是誰、令牌何時到期所需的所有信息,以及各種其他信息,如請求的作用域、頒發者(issuer)和用於請求令牌的客戶端 ID。

然後將來自 REST API 的響應打印到屏幕上。

1
2
3
4
5
6
7
8
9
10
const response = await request({
uri,
method,
body,
headers: {
authorization: `${auth.token_type} ${auth.access_token}`,
},
})

console.log(response)

現在去測試一下。再次使用 npm test && node . 啟動應用程序,然後嘗試以下命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
$ node client http://localhost:3000/parts | json
[
{
"id": 1,
"partNumber": "abc-123",
"modelNumber": "xyz-789",
"name": "Alphabet Soup",
"description": "Soup with letters and numbers in it",
"createdAt": "2018-08-16T02:22:09.446Z",
"updatedAt": "2018-08-16T02:22:09.446Z"
}
]

$ node client http://localhost:3000/parts post '{
"partNumber": "ban-bd",
"modelNumber": 1,
"name": "Banana Bread",
"description": "Bread made from bananas"
}' | json
{
"id": 2,
"partNumber": "ban-bd",
"modelNumber": "1",
"name": "Banana Bread",
"description": "Bread made from bananas",
"updatedAt": "2018-08-17T00:23:23.341Z",
"createdAt": "2018-08-17T00:23:23.341Z"
}

$ node client http://localhost:3000/parts | json
[
{
"id": 1,
"partNumber": "abc-123",
"modelNumber": "xyz-789",
"name": "Alphabet Soup",
"description": "Soup with letters and numbers in it",
"createdAt": "2018-08-16T02:22:09.446Z",
"updatedAt": "2018-08-16T02:22:09.446Z"
},
{
"id": 2,
"partNumber": "ban-bd",
"modelNumber": "1",
"name": "Banana Bread",
"description": "Bread made from bananas",
"createdAt": "2018-08-17T00:23:23.341Z",
"updatedAt": "2018-08-17T00:23:23.341Z"
}
]

$ node client http://localhost:3000/parts/1 delete | json
{}

$ node client http://localhost:3000/parts | json
[
{
"id": 2,
"partNumber": "ban-bd",
"modelNumber": "1",
"name": "Banana Bread",
"description": "Bread made from bananas",
"createdAt": "2018-08-17T00:23:23.341Z",
"updatedAt": "2018-08-17T00:23:23.341Z"
}
]

(到此翻譯完了,以上翻譯內容結合本人理解,如有問題請留言。)

原文鏈接:https://developer.okta.com/blog/2018/08/21/build-secure-rest-api-with-node?utm_source=com.alibaba.android.rimet&utm_medium=social&utm_oi=1037865074043592704