跳至主要內容

為你的傳統網頁應用程式新增驗證 (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}

進一步閱讀

終端使用者流程:驗證流程、帳戶流程與組織流程 配置連接器 保護你的 API 在 GPT 操作中驗證使用者:構建個人議程助理