JavaScript >> Javascript 文檔 >  >> Node.js

如何在 Node.js 中實現 OAuth2 工作流

如何通過設置與 Github API 的 OAuth 連接,在 JavaScript 和 Node.js 中實現 OAuth2 工作流。

開始使用

在本教程中,我們將使用 CheatCode 的全棧 JavaScript 框架 Joystick。 Joystick 將前端 UI 框架與用於構建應用的 Node.js 後端結合在一起。

首先,我們要通過 NPM 安裝 Joystick。確保在安裝之前使用 Node.js 16+ 以確保兼容性(如果您需要學習如何安裝 Node.js 或在計算機上運行多個版本,請先閱讀本教程):

終端

npm i -g @joystick.js/cli

這將在您的計算機上全局安裝操縱桿。安裝好之後,接下來我們新建一個項目:

終端

joystick create app

幾秒鐘後,您將看到一條消息已註銷到 cd 進入你的新項目並運行 joystick start .在你運行 joystick start 之前 ,我們需要添加一個依賴:node-fetch .

終端

cd app && npm i node-fetch

安裝後,繼續啟動您的應用程序:

終端

joystick start

在此之後,您的應用應該可以運行了,我們可以開始了。

公平警告

雖然 OAuth2 本身是實現身份驗證模式的標準,但 實現 該標準並不總是一致的。我們選擇 Github 作為我們的示例 API,因為他們的 OAuth 實現做得很好並且有據可查。 您選擇的 API 並非總是如此 .

重點是:將我們在此處介紹的步驟視為 OAuth2 實現應該的近似值 看起來像一個 API。有時你很幸運,有時你最終會收到警察的噪音投訴。需要注意的一些常見的不一致:

  1. 需要在 HTTP headers 中傳遞的未記錄或記錄不充分的參數 ,查詢params , 或 body .
  2. 需要在 HTTP headers 中傳遞的未記錄或記錄不充分的響應類型 .例如,某些 API 可能需要 Accept 標頭設置為 application/json 以獲取 JSON 格式的響應。
  3. 文檔中的錯誤示例代碼。
  4. 傳遞錯誤參數時的錯誤代碼(請參閱上面的各項)。

雖然這不是一切 你會遇到,這些通常會浪費你的時間和精力。如果您確定自己完全遵循了 API 文檔但仍然存在問題:查看上面的列表並嘗試使用您傳遞的內容(即使相關 API 沒有記錄它,這可能會令人沮喪) .

從 Github API 獲取憑據

首先,我們需要在 Github 上註冊我們的應用程序並獲取安全憑證。這是所有 OAuth2 實現的常見模式 .特別是,您將需要兩件事: client_id 和一個 client_secret .

client_idclient_secret 通過證明 client_id 指定的應用程序的所有權來授權連接 (這是公開的,所以技術上任何人都可以將它傳遞給 API,而 client_secret 是,顧名思義,秘密 )。

如果您還沒有 Github 帳戶,請訪問此鏈接並創建一個帳戶。

登錄後,在網站的右上角,單擊帶有頭像的圓圈圖標和旁邊的向下箭頭。從彈出的菜單中選擇“設置”。

接下來,在該頁面左側菜單的底部附近,找到並單擊“開發人員設置”選項。在下一頁的左側菜單中,找到並單擊“OAuth Apps”選項。

如果這是您第一次在 Github 上註冊 OAuth 應用程序,您應該會看到一個綠色按鈕,提示您“註冊新應用程序”。單擊該按鈕開始獲取您的 client_id 的過程 和 client_secret .

在此頁面上,您需要提供三件事:

  1. 您的 OAuth 應用程序的名稱。這是 Github 將在用戶確認您對其帳戶的訪問權限時向他們顯示的內容。
  2. 您的應用的主頁 URL(這可以只是一個用於測試的虛擬 URL)。
  3. 一個“授權回調 URL”,Github 將在其中發送一個特殊的 code 響應用戶批准授予我們的應用訪問其帳戶的權限。

對於#3,在本教程中,我們要輸入 http://localhost:2600/oauth/github (這與您在上面的屏幕截圖中看到的不同,但在意圖方面是等效的)。 http://localhost:2600 是我們使用 CheatCode 的 Joystick 框架創建的應用程序默認運行的地方。 /oauth/github 部分是我們接下來要連接的路徑/路由,我們希望 Github 向我們發送授權 code 我們可以換取一個 access_token 為用戶的帳戶。

填寫完成後,單擊“註冊應用程序”以創建您的 OAuth 應用程序。在下一個屏幕上,您需要找到“客戶端 ID”,然後單擊頁面中間附近的“生成新的客戶端密碼”按鈕。

注意 :當你生成你的 client_secret Github 將有意只在屏幕上顯示它一次 .建議您支持這個和您的 client_id 在密碼管理器或其他機密管理器中。如果您丟失了它,您將需要生成一個新的秘密並刪除舊的秘密以避免潛在的安全問題。

保留此頁面或複制 client_idclient_secret 以供下一步使用。

將我們的憑據添加到我們的設置文件中

在我們深入研究代碼之前,接下來,我們需要復制我們的 client_idclient_secret 進入我們應用程序的設置文件。在 Joystick 應用中,這是在我們運行 joystick create 時自動為我們創建的 .

打開settings-development.json 應用根目錄下的文件:

/settings-development.json

{
  "config": {
    "databases": [ ... ],
    "i18n": {
      "defaultLanguage": "en-US"
    },
    "middleware": {},
    "email": { ... }
  },
  "global": {},
  "public": {
    "github": {
      "client_id": "dc47b6a0a67b904c58c7"
    }
  },
  "private": {
    "github": {
      "client_id": "dc47b6a0a67b904c58c7",
      "client_secret": "<Client Secret Here>",
      "redirect_uri": "http://localhost:2600/oauth/github"
    }
  }
}

我們要關注兩個地方:publicprivate 文件中已經存在的對象。在兩者內部,我們要嵌套一個 github 將包含我們的憑據的對象。

注意這裡 :我們只想存儲 client_id public.github下 對象,而我們要存儲 client_idclient_secret private.github 下 目的。我們還想添加 redirect_uri 我們在 Github 上輸入(http://localhost:2600/oauth/github 一)。

完成這些設置後,我們就可以深入研究代碼了。

連接客戶端授權請求

首先,我們將在 UI 中添加一個簡單的頁面,我們可以在其中訪問“連接到 Github”按鈕,用戶可以單擊該按鈕來初始化 OAuth 請求。為了構建它,我們將重用 / 當我們使用 joystick create 生成應用程序時自動為我們定義的路由 .真快,如果我們打開 /index.server.js 在項目的根目錄下,我們可以看到 Joystick 是如何渲染的:

/index.server.js

import node from "@joystick.js/node";
import api from "./api";

node.app({
  api,
  routes: {
    "/": (req, res) => {
      res.render("ui/pages/index/index.js", {
        layout: "ui/layouts/app/index.js",
      });
    },
    "*": (req, res) => {
      res.render("ui/pages/error/index.js", {
        layout: "ui/layouts/app/index.js",
        props: {
          statusCode: 404,
        },
      });
    },
  },
});

在 Joystick 應用中,路由是通過 Express.js 實例定義的,該實例通過 node.app() 自動設置 從 @joystick.js/node 導入的函數 包裹。向該函數傳遞一個帶有 routes 的對象 選項設置為一個對象,其中定義了我們應用的所有路由。

這裡,/ 索引路由(或“根”路由)使用 res.render() 在 HTTP response 上由 Joystick 定義的函數 我們從 Express.js 獲得的對象。該函數旨在渲染使用 Joystick 的 UI 庫 @joystick.js/ui 創建的 Joystick 組件 .

在這裡,我們可以看到 ui/pages/index/index.js 正在通過的路徑。現在讓我們打開那個文件並修改它以顯示我們的“連接到 Github”按鈕。

/ui/pages/index/index.js

import ui from "@joystick.js/ui";

const Index = ui.component({
  events: {
    'click .login-with-github': (event) => {
      location.href = `https://github.com/login/oauth/authorize?client_id=${joystick.settings.public.github.client_id}&scope=repo user`;
    },
  },
  css: `
    div {
      padding: 40px;
    }

    .login-with-github {
      background: #333;
      padding: 15px 20px;
      border-radius: 3px;
      border: none;
      font-size: 15px;
      color: #fff;
    }

    .login-with-github {
      cursor: pointer;
    }

    .login-with-github:active {
      position: relative;
      top: 1px;
    }
  `,
  render: () => {
    return `
      <div>
        <button class="login-with-github">Connect to Github</button>
      </div>
    `;
  },
});

export default Index;

在這裡,我們覆蓋了 /ui/pages/index/index.js 的現有內容 包含將呈現我們的按鈕的組件的文件。在 Joystick 中,組件是通過調用 ui.component() 來定義的 從 @joystick.js/ui 導入的函數 封裝並傳遞一個選項對象來描述組件的行為和外觀。

在這裡,在 render 函數,我們返回一個 HTML 字符串,希望 Joystick 為我們在瀏覽器中呈現。在那個字符串中,我們有一個簡單的 <button></button> 類名 .login-with-github 的元素 .如果我們看上面的選項 render , css ,我們可以看到一些樣式被應用到我們的組件中,為頁面添加了一些填充,並為我們的按鈕設置樣式。

這裡的重要部分在 events 中 目的。在這裡,我們為 click 定義了一個事件監聽器 .login-with-github 類元素上的事件 .當在瀏覽器中檢測到該事件時,我們分配給 'click .login-with-github 的函數 這裡會被調用。

在內部,我們的目標是將用戶重定向到 Github 的 URL 以啟動 OAuth 授權請求。為此,我們設置全局 location.href 將瀏覽器中的值轉換為包含 URL 以及一些查詢參數的字符串:

  1. client_id 這裡賦值給joystick.settings.public.github.client_id的值 我們在 settings-development.json 中設置的 早點存檔。
  2. scope 設置等於兩個“範圍”,授予 access_token 特定權限 我們從 Github 為這個用戶獲取。在這裡,我們使用 repouser (根據 Github 文檔以空格分隔)範圍,使我們能夠訪問 Github 上的用戶存儲庫及其完整的用戶配置文件。此處提供了要請求的範圍的完整列表。

如果我們在應用程序運行時保存這些更改,操縱桿將在瀏覽器中自動刷新。假設我們的憑據是正確的,我們應該被重定向到 Github 並看到如下內容:

接下來,在我們點擊“授權”按鈕之前,我們需要連接 Github 將用戶重定向到的端點(我們設置為 http://localhost:2600/oauth/github 的“授權回調 URL” 早)。

處理代幣兌換

讓一切正常運行的最後一步是與 Github 進行代幣交換。為了批准我們的請求並完成我們的連接,Github 需要驗證連接到我們服務器的請求。為此,當用戶在我們剛剛在 Github 上看到的 UI 中單擊“授權”時,他們將向我們在設置應用時指定的“授權回調 URL”發送請求,並傳遞一個臨時 code 請求 URL 的查詢參數中的值,我們可以“交換”為永久 access_token 為我們的用戶。

首先,我們需要做的第一件事是將 URL/路由連接回我們的 index.server.js 文件:

/index.server.js

import node from "@joystick.js/node";
import api from "./api";
import github from "./api/oauth/github";

node.app({
  api,
  routes: {
    "/": (req, res) => {
      res.render("ui/pages/index/index.js", {
        layout: "ui/layouts/app/index.js",
      });
    },
    "/oauth/github": async (req, res) => {
      await github({ req });
      res.status(200).redirect('/');
    },
    "*": (req, res) => {
      res.render("ui/pages/error/index.js", {
        layout: "ui/layouts/app/index.js",
        props: {
          statusCode: 404,
        },
      });
    },
  },
});

對我們之前看到的內容進行了一些小的更改。在這裡,我們添加了我們的路線 /oauth/github 與我們了解 / 的方式完全相同 早些時候。在裡面,我們添加 async 加載我們的路由時將調用的函數的關鍵字,預期對函數 github() 的調用 這將返回一個 JavaScript Promise,我們可以 await 在響應路由請求之前。

一旦該函數完成,我們希望以 200 狀態響應來自 Github 的請求 並調用 .redirect() 將用戶重定向回我們應用程序中他們發起請求的頁面(我們的 / 索引路線)。

接下來,讓我們連接我們預期在 /api/oauth/github.js 上可用的功能 在我們的項目中:

/api/oauth/github.js

/* eslint-disable consistent-return */

import fetch from 'node-fetch';
import { URL, URLSearchParams } from 'url';

const getReposFromGithub = (username = '', access_token = '') => {
  return fetch(`https://api.github.com/user/repos`, {
    headers: {
      Accept: 'application/json',
      Authorization: `token ${access_token}`,
    },
  }).then(async (response) => {
    const data = await response.json();
    return data;
  }).catch((error) => {
    console.warn(error);
    throw new Error(error);
  });
};

const getUserFromGithub = (access_token = '') => {
  return fetch('https://api.github.com/user', {
    headers: {
      Accept: 'application/json',
      Authorization: `token ${access_token}`,
    },
  }).then(async (response) => {
    const data = await response.json();
    return data;
  }).catch((error) => {
    console.warn(error);
    throw new Error(error);
  });
};

const getAccessTokenFromGithub = (code = '') => {
  try {
    const url = new URL('https://github.com/login/oauth/access_token');
    const searchParams = new URLSearchParams({
      client_id: joystick.settings.private.github.client_id,
      client_secret: joystick.settings.private.github.client_secret,
      code,
      redirect_uri: joystick.settings.private.github.redirect_uri,
    });

    url.search = searchParams.toString();

    return fetch(url, {
      method: 'POST',
      headers: {
        Accept: 'application/json'
      },
    }).then(async (response) => {
      const data = await response.json();
      return data;
    }).catch((error) => {
      console.warn(error);
      throw new Error(error);
    });
  } catch (exception) {
    throw new Error(`[github.getAccessTokenFromGithub] ${exception.message}`);
  }
};

const validateOptions = (options) => {
  try {
    if (!options) throw new Error('options object is required.');
    if (!options.req) throw new Error('options.req is required.');
  } catch (exception) {
    throw new Error(`[github.validateOptions] ${exception.message}`);
  }
};

const github = async (options, { resolve, reject }) => {
  try {
    validateOptions(options);
    const { access_token } = await getAccessTokenFromGithub(options?.req?.query?.code);
    const user = await getUserFromGithub(access_token);
    const repos = await getReposFromGithub(user?.login, access_token);

    // NOTE: Set this information on a user in your database or store elsewhere for reuse.
    console.log({
      access_token,
      user,
      repos,
    });

    resolve();
  } catch (exception) {
    reject(`[github] ${exception.message}`);
  }
};

export default (options) =>
  new Promise((resolve, reject) => {
    github(options, { resolve, reject });
  });

為了讓一切更容易理解,在這裡,我們正在做一個完整的代碼轉儲,然後單步執行。在這個文件中,我們使用了一種稱為動作模式的模式(幾年前我提出的用於在應用中組織算法或多步代碼的模式)。

動作模式的基本構造是我們有一個主函數(這裡定義為 github ) 依次調用其他函數。該序列中的每個函數都執行單個任務,並在必要時返回一個值以移交給序列中的其他函數。

每個函數都定義為帶有 JavaScript try/catch 的箭頭函數 立即封鎖在它的身體內部。在 try 塊,我們運行該函數的代碼,並在 catch 我們調用 throw 傳遞帶有我們錯誤的標準化字符串。

這裡的想法是給我們的代碼一些結構,讓事情井井有條,同時讓錯誤更容易追踪(如果在函數中發生錯誤,[github.<functionName>] 部分告訴我們錯誤到底發生在哪裡)。

在這裡,因為這是一個“Promise”動作,所以我們包裝了主要的 github() 在我們文件的底部使用 JavaScript Promise 函數並導出 that 功能。回到我們的 /index.server.js 文件,這就是為什麼我們可以使用 async/await 模式。

對於我們的“行動”,我們分為三個步驟:

  1. 兌換code 我們從 Github 獲得永久的 access_token .
  2. 獲取與該 access_token 關聯的用戶 來自 Github API。
  3. 獲取與該 access_token 關聯的用戶的存儲庫 來自 Github API。

這裡的想法是展示獲取令牌然後執行 API 請求的過程with 那個令牌。所以很明顯,這是通用的,因此您可以將此模式/登錄應用到 any OAuth API。

/api/oauth/github.js

const getAccessTokenFromGithub = (code = '') => {
  try {
    const url = new URL('https://github.com/login/oauth/access_token');
    const searchParams = new URLSearchParams({
      client_id: joystick.settings.private.github.client_id,
      client_secret: joystick.settings.private.github.client_secret,
      code,
      redirect_uri: joystick.settings.private.github.redirect_uri,
    });

    url.search = searchParams.toString();

    return fetch(url, {
      method: 'POST',
      headers: {
        Accept: 'application/json'
      },
    }).then(async (response) => {
      const data = await response.json();
      return data;
    }).catch((error) => {
      console.warn(error);
      throw new Error(error);
    });
  } catch (exception) {
    throw new Error(`[github.getAccessTokenFromGithub] ${exception.message}`);
  }
};

專注於序列getAccessTokenFromGithub()中的第一步 ,這裡,我們需要執行一個返回到https://github.com/login/oauth/access_token的請求 Github API 中的端點以獲取永久的 access_token .

為此,我們要執行 HTTP POST 請求(根據 Github 文檔和 OAuth 實現的標準),傳遞請求所需的參數(同樣,根據 Github,但對於所有 OAuth2 請求都類似)。

為此,我們導入 URLURLSearchParams Node.js url 中的類 包(我們不必安裝這個包——它在 Node.js 應用程序中自動可用)。

首先,我們需要為 /login/oauth 創建一個新的 URL 對象 使用 new URL() 在 Github 上的端點 傳入該 URL。接下來,我們需要為我們的請求生成搜索參數 ?like=this 所以我們使用 new URLSearchParams() 類,傳入一個包含我們要添加到 URL 的所有查詢參數的對象。

在這裡,我們需要四個:client_id , client_secret , code , 和 redirect_uri .使用這四個參數,Github 將能夠驗證我們對 access_token 的請求 並返回一個我們可以使用的。

對於我們的 client_id , client_secret , 和 redirect_uri ,我們從 joystick.settings.private.github 中提取這些 我們在教程前面定義的對象。 code 是我們從 req?.query?.code 中檢索到的代碼 Github 傳遞給我們的值(在 Express.js 應用程序中,傳遞給我們服務器的任何查詢參數都設置為對象 query 在入站 req uest 對象)。

這樣,在我們執行我們的請求之前,我們通過設置 url.search 將我們的搜索參數添加到我們的 URL 值等於調用 .toString() 的結果 在我們的 searchParams 多變的。這將生成一個類似於 ?client_id=xxx&client_secret=xxx&code=xxx&redirect_uri=http://localhost:2600/oauth/github 的字符串 .

最後,有了這個,我們在頂部導入 fetch 來自 node-fetch 我們之前安裝的軟件包。我們調用它,傳遞我們的 url 我們剛剛生成的對象,後跟一個帶有 method 的選項對象 值設置為 POST (表示我們希望請求以 HTTP POST 的形式執行 請求)和一個 headers 目的。在那個 headers 對象,我們通過標準的 Accept 標頭告訴 Github API 我們將接受他們對我們請求的響應的 MIME 類型(在本例中為 application/json )。如果我們忽略這一點,Github 將使用默認的 url-form-encoded 返迴響應 MIME 類型。

一旦調用它,我們期望 fetch() 向我們返回一個帶有響應的 JavaScript Promise。要將響應作為 JSON 對象獲取,我們採用 response 傳遞給我們 .then() 的回調 方法,然後調用 response.json() 告訴fetch 將收到的響應正文格式化為 JSON 數據(我們使用 async/await 這裡告訴 JavaScript 等待來自 response.json() 的響應 函數)。

用那個 data 在手邊,我們從我們的函數中返回它。如果一切按計劃進行,我們應該從 Github 上取回一個看起來像這樣的對象:

{
  access_token: 'gho_abc123456',
  token_type: 'bearer',
  scope: 'repo,user'
}

接下來,如果我們查看我們的主要 github 函數,我們可以看到下一步是獲取我們從 getAccessTokenFromGithub() 得到的結果對象 函數並解構它,去掉 access_token 我們在上面的示例響應中看到的屬性。

有了這個,現在我們可以永久訪問該用戶在 Github 上的存儲庫和用戶帳戶(完成工作流的 OAuth 部分),直到他們撤銷訪問權限。

雖然我們在技術上完成 通過我們的 OAuth 實施,了解為什麼會很有幫助 在我們正在做的事情背後。現在,使用我們的 access_token 我們能夠代表執行對 Github API 的請求 我們的用戶。這意味著,就 Github 而言(並且在我們要求的範圍的限制內),我們 該用戶,直到用戶說我們不是並撤銷我們的訪問權限。

/api/oauth/github.js

const getUserFromGithub = (access_token = '') => {
  return fetch('https://api.github.com/user', {
    headers: {
      Accept: 'application/json',
      Authorization: `token ${access_token}`,
    },
  }).then(async (response) => {
    const data = await response.json();
    return data;
  }).catch((error) => {
    console.warn(error);
    throw new Error(error);
  });
};

專注於我們對 getUserFromGithub() 的調用 發出 API 請求的過程幾乎與我們的 access_token 相同 請求添加了一個新標頭 Authorization .這是另一個標準 HTTP 標頭,它允許我們將授權字符串傳遞給我們正在向其發出請求的服務器(在本例中為 Github 的 API 服務器)。

在該字符串中,遵循 Github API 的約定(這部分對於每個 API 會有所不同——有些需要 bearer <token> 模式,而其他人需要 <user>:<pass> 模式,而其他模式需要這兩種模式之一或另一種模式的 base64 編碼版本),我們傳遞關鍵字 token 後跟一個空格,然後是 access_token 我們從 getAccessTokenFromGithub() 收到的值 我們之前寫的函數。

為了處理響應,我們使用 response.json() 執行我們上面看到的完全相同的步驟 將響應格式化為 JSON 數據。

有了這個,我們應該期望得到一個描述我們用戶的大對象!

我們將在這裡結束。雖然我們 有另一個對 getReposFromGithub() 的函數調用 ,我們已經了解了執行此請求需要了解的內容。

回到我們的主 github() 函數,我們獲取所有三個調用的結果,並將它們組合到一個我們記錄到控制台的對像上。

而已!我們現在擁有對 Github 用戶帳戶的 OAuth2 訪問權限。

總結

在本教程中,我們學習瞭如何使用 Github API 實現 OAuth2 授權工作流。我們了解了不同 OAuth 實現之間的區別,並查看了在客戶端初始化請求然後在服務器上處理令牌交換的示例。最後,我們學習瞭如何獲取 access_token 我們從 OAuth 令牌交換中返回並使用它代表用戶執行 API 請求。


Tutorial JavaScript 教程
  1. 僅當元素可見時才執行單擊功​​能

  2. 在 JavaScript 項目中安裝和設置 Babel 7 的分步指南

  3. 如何比較兩個正則表達式?

  4. ES6 - 初學者指南 - 默認參數

  5. JavaScript 變得簡單:第 7 部分

  6. Web 組件 - 號召性用語 (CTA) 按鈕

  7. 在對像上創建方法

  1. 讓我們在 <=30 分鐘內構建 Twitter 克隆

  2. 懸停時更改圖像

  3. 如何通過對像數組映射以提取對象值?

  4. 你應該知道的 6 個受 jQuery 啟發的原生 DOM 操作方法

  5. JavaScript 中有問題的 Try-Catch

  6. JavaScript 內部寬度 |財產

  7. Next js 在加載頁面內容時在頂部顯示 0 一秒鐘

  1. 帶有星球大戰 API 的 Angular NGRX

  2. 直接提交到您的電子郵件地址的低代碼 HTML 表單! (免費無服務器表單)

  3. 如何在 Laravel 中創建多語言網站

  4. 將游戲控制器輸入添加到 React