跳到主要内容

为你的传统 Web 应用添加认证 (Authentication)

备注:

本指南假设你已经在管理控制台中创建了一个类型为“Traditional web”的应用程序。

你的应用可能运行在服务器端而不是浏览器中,使用像 Django、Laravel 等框架。这被称为传统 Web 应用。如果你在此页面找不到合适的 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:一个会话中间件,我们将使用会话来存储用户令牌。
  4. js-base64:另一个 Base64 转码器。
npm i @logto/js node-fetch@v2 express-session js-base64

使用会话​

当用户登录时,他们将获得一组令牌(访问令牌、ID 令牌、刷新令牌)和交互数据,会话是存储它们的绝佳位置。

我们在上一步中已经安装了 express-session,现在让我们简单地添加以下代码来设置它:

// app.js

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

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

实现用户认证 (Authentication) 功能​

提示:

我们假设应用程序运行在 http://localhost:3000,以下代码片段中会用到。

在这一步中,我们需要实现以下认证 (Authentication) 功能:

  1. getSignInUrl:构建并返回 Logto 授权 (Authorization) 服务器的完整 URL,用户将被重定向到该 URL。
  2. handleSignIn:解析认证 (Authentication) 过程完成后的回调 URL,获取代码查询参数,然后获取令牌(访问令牌、刷新令牌和 ID 令牌)以完成登录过程。
  3. refreshTokens:使用刷新令牌交换新的访问令牌。
提示:

你可以在管理控制台的应用详情页面找到并复制“应用密钥”:

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. 此认证 (Authentication) 过程遵循 OpenID Connect (OIDC) 协议,Logto 强制执行严格的安全措施以保护用户登录。
  2. 如果你有多个应用程序,可以使用相同的身份提供商 (IdP)(日志 (Logto))。一旦用户登录到一个应用程序,当用户访问另一个应用程序时,Logto 将自动完成登录过程。

要了解有关基于重定向的登录的原理和好处的更多信息,请参阅 Logto 登录体验解释。


配置重定向 URI​

让我们切换到 Logto Console 的应用详情页面。添加一个重定向 URI http://localhost:3000/callback 并点击“保存更改”。

Logto Console 中的重定向 URI

实现登录路由​

备注:

在调用 getSignInUrl() 之前,请确保你已在管理控制台中正确配置了重定向 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: 链接到用户参考中的“会话 & cookies”章节。

你可以清除会话中的令牌以使用户从此应用程序中登出。并查看此链接以了解更多关于“登出”的信息。

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}

进一步阅读​

终端用户流程:认证 (Authentication) 流程、账户流程和组织流程 配置连接器 (Connectors) 授权 (Authorization)

在 GPT 操作中认证 (Authentication) 用户:构建个人日程助手