跳至主要內容

為你的傳統網頁應用程式新增驗證 (Authentication)

備註:

本指南假設你已在管理控制台中創建了一個類型為 "Traditional web" 的應用程式。

你的應用程式可能運行在伺服器端而非瀏覽器上,使用像 Django、Laravel 等框架。這稱為傳統網頁應用程式。如果你在此頁面找不到合適的 SDK,可能需要手動整合。

本文將逐步指導你完成整合。我們以 Node.js 中的 Express 為例。

提示:

本文不僅適用於 Express,如果你使用其他框架甚至其他語言,可以將 @logto/js 替換為其他語言的核心 SDK,然後調整部分步驟。

獲取原始碼​

你可以前往 GitHub 獲取本指南的最終程式碼。

開始一個 Express 專案​

使用 express-generator,你可以快速開始一個 Express 專案。

mkdir express-logto
cd express-logto
npx express-generator

安裝相依套件​

範例應用程式需要 4 個相依套件:

  1. @logto/js:Logto 的 JavaScript 核心 SDK。
  2. node-fetch:在 Node.js 執行環境中提供 window.fetch 相容 API 的最小代碼。
  3. express-session:一個 session 中介軟體,我們將使用 session 來儲存使用者權杖。
  4. js-base64:另一個 Base64 編解碼器。
npm i @logto/js node-fetch@v2 express-session js-base64

使用 session​

當使用者登入時,他們將獲得一組權杖(存取權杖 (Access Token)、ID 權杖 (ID Token)、重新整理權杖 (Refresh Token))和互動數據,而 session 是儲存它們的絕佳位置。

我們在前一步已安裝 express-session,現在讓我們簡單地添加以下代碼來設置它:

// app.js

const session = require('express-session');

app.use(
session({
secret: 'keyboard cat', // 更改為你自己的密鑰
cookie: { maxAge: 86400 },
})
);

實作用戶驗證功能​

提示:

我們假設應用程式運行在 http://localhost:3000。

在此步驟中,我們需要實作以下驗證功能:

  1. getSignInUrl:構建並返回 Logto 授權伺服器的完整 URL,使用者將被重定向至該 URL。
  2. handleSignIn:解析驗證流程完成後的回調 URL,獲取 code 查詢參數,然後獲取權杖(存取權杖、重新整理權杖和 ID 權杖)以完成登入流程。
  3. refreshTokens:使用重新整理權杖交換新的存取權杖。
提示:

你可以在管理控制台的應用程式詳細資訊頁面找到並複製「App Secret」:

App Secret
// logto.js

const {
withReservedScopes,
fetchOidcConfig,
discoveryPath,
generateSignInUri,
verifyAndParseCodeFromCallbackUri,
fetchTokenByAuthorizationCode,
fetchTokenByRefreshToken,
} = require('@logto/js');
const fetch = require('node-fetch');
const { randomFillSync, createHash } = require('crypto');
const { fromUint8Array } = require('js-base64');

const config = {
endpoint: 'https://logto.dev',
appId: 'foo',
appSecret: '<your-app-secret-copied-from-console>',
redirectUri: 'http://localhost:3000/callback', // 你可能需要將其替換為應用程式的生產地址
scopes: withReservedScopes().split(' '),
};

const requester = (input, init) => {
const { appId, appSecret } = config;

return fetch(input, {
...init,
headers: {
...init?.headers,
authorization: `Basic ${Buffer.from(`${appId}:${appSecret}`, 'utf8').toString('base64')}`,
},
});
};

const generateRandomString = (length = 64) => {
return fromUint8Array(randomFillSync(new Uint8Array(length)), true);
};

const generateCodeChallenge = async (codeVerifier) => {
const encodedCodeVerifier = new TextEncoder().encode(codeVerifier);
const hash = createHash('sha256');
hash.update(encodedCodeVerifier);
const codeChallenge = hash.digest();
return fromUint8Array(codeChallenge, true);
};

const getOidcConfig = async () => {
return fetchOidcConfig(new URL(discoveryPath, config.endpoint).toString(), requester);
};

exports.getSignInUrl = async () => {
const { authorizationEndpoint } = await getOidcConfig();
const codeVerifier = generateRandomString();
const codeChallenge = await generateCodeChallenge(codeVerifier);
const state = generateRandomString();

const { redirectUri, scopes, appId: clientId } = config;

const signInUri = generateSignInUri({
authorizationEndpoint,
clientId,
redirectUri: redirectUri,
codeChallenge,
state,
scopes,
});

return { redirectUri, codeVerifier, state, signInUri };
};

exports.handleSignIn = async (signInSession, callbackUri) => {
const { redirectUri, state, codeVerifier } = signInSession;
const code = verifyAndParseCodeFromCallbackUri(callbackUri, redirectUri, state);

const { appId: clientId } = config;
const { tokenEndpoint } = await getOidcConfig();
const codeTokenResponse = await fetchTokenByAuthorizationCode(
{
clientId,
tokenEndpoint,
redirectUri,
codeVerifier,
code,
},
requester
);

return codeTokenResponse;
};

exports.refreshTokens = async (refreshToken) => {
const { appId: clientId, scopes } = config;
const { tokenEndpoint } = await getOidcConfig();
const tokenResponse = await fetchTokenByRefreshToken(
{
clientId,
tokenEndpoint,
refreshToken,
scopes,
},
requester
);

return tokenResponse;
};

登入​

在深入細節之前,以下是終端使用者體驗的快速概覽。登入流程可簡化如下:

  1. 你的應用程式呼叫登入方法。
  2. 使用者被重定向至 Logto 登入頁面。對於原生應用程式,系統瀏覽器會被開啟。
  3. 使用者登入後被重定向回你的應用程式(配置為重定向 URI)。

關於基於重導的登入​

  1. 此驗證流程遵循 OpenID Connect (OIDC) 協議,Logto 強制執行嚴格的安全措施以保護使用者登入。
  2. 如果你有多個應用程式,可以使用相同的身分提供者 (IdP, Identity provider)(Logto)。一旦使用者登入其中一個應用程式,Logto 將在使用者訪問另一個應用程式時自動完成登入流程。

欲了解更多關於基於重導登入的原理和優勢,請參閱 Logto 登入體驗解析。


配置重定向 URI​

讓我們切換到 Logto Console 的應用程式詳細資訊頁面。新增一個重定向 URI http://localhost:3000/callback,然後點擊「儲存變更」。

Logto Console 中的重定向 URI

實作登入路由​

備註:

在呼叫 getSignInUrl() 之前,請確保已在管理控制台中正確配置了 Redirect URI。

在 Express 中創建一個登入路由:

const { getSignInUrl } = require('./logto');

app.get('/sign-in', async (req, res) => {
const { redirectUri, codeVerifier, state, signInUri } = await getSignInUrl();
req.session.signIn = { codeVerifier, state, redirectUri };
res.redirect(signInUri);
});

以及一個處理回調的路由:

app.get('/callback', async (req, res) => {
if (!req.session.signIn) {
res.send('Bad request.');
return;
}

const response = await handleSignIn(
req.session.signIn,
`${req.protocol}://${req.get('host')}${req.originalUrl}`
);
req.session.tokens = {
...response,
expiresAt: response.expiresIn + Date.now(),
idToken: decodeIdToken(response.idToken),
};
req.session.signIn = null;

res.redirect('/');
});

登出​

TODO: 連結到用戶參考中的「session & cookies」章節。

你可以清除 session 中的權杖來讓使用者從此應用程式登出。並檢查此連結以了解更多關於「登出」的資訊。

app.get('/sign-out', (req, res) => {
req.session.tokens = null;
res.send('Sign out successful');
});

存取受保護的資源​

創建一個名為 withAuth 的中介軟體,將 auth 物件附加到 req,並驗證使用者是否已登入。

// auth.js

const { decodeIdToken } = require('@logto/js');
const { refreshTokens } = require('./logto');

const withAuth =
({ requireAuth } = { requireAuth: true }) =>
async (req, res, next) => {
if (requireAuth && !req.session.tokens) {
res.redirect('/sign-in');
return;
}

if (req.session.tokens) {
if (req.session.tokens.expiresAt >= Date.now()) {
// 存取權杖過期,刷新以獲取新權杖
try {
const response = await refreshTokens(req.session.tokens.refreshToken);
req.session.tokens = {
...response,
expiresAt: response.expiresIn + Date.now(),
idToken: decodeIdToken(response.idToken),
};
} catch {
// 交換失敗,重定向至登入
res.redirect('/sign-in');
return;
}
}

req.auth = req.session.tokens.idToken.sub;
}

next();
};

module.exports = withAuth;

創建 index 頁面,為訪客顯示登入連結,為已登入的使用者顯示前往個人資料的連結:

router.get('/', withAuth({ requireAuth: false }), function (req, res, next) {
res.render('index', { auth: Boolean(req.auth) });
});
extends layout

block content
h1 Hello logto
if auth
p: a(href="/user") Go to profile
else
p: a(href="/sign-in") Click here to sign in

創建 user 頁面,顯示 userId(主體 (subject)):

app.get('/user', withAuth(), (req, res, next) => {
res.render('user', { userId: req.auth });
});
extends layout

block content
h1 Hello logto
p userId: #{userId}

進一步閱讀​

終端使用者流程:驗證流程、帳號流程與組織流程 (End-user flows: authentication flows, account flows, and organization flows) 設定連接器 (Configure connectors) 授權 (Authorization) 在 GPT 操作中驗證使用者:構建個人議程助理