跳到主要内容

如何在你的 API 服务或后端校验访问令牌 (Access token)

校验访问令牌 (Access token) 是在 Logto 中实施基于角色的访问控制 (RBAC)的关键步骤。本指南将带你完成在后端 / API 中验证 Logto 签发的 JWT,包括校验签名、发行者 (Issuer)、受众 (Audience)、过期时间、权限 (Scopes) 以及组织 (Organization) 上下文。

开始之前

步骤 1:初始化常量和工具方法

在你的代码中定义必要的常量和工具方法,用于处理令牌提取和校验。一个有效的请求必须包含 Authorization 头,格式为 Bearer <access_token>

auth-middleware.ts
import { IncomingHttpHeaders } from 'http';

const JWKS_URI = 'https://your-tenant.logto.app/oidc/jwks';
const ISSUER = 'https://your-tenant.logto.app/oidc';

export class AuthInfo {
constructor(
public sub: string,
public clientId?: string,
public organizationId?: string,
public scopes: string[] = [],
public audience: string[] = []
) {}
}

export class AuthorizationError extends Error {
name = 'AuthorizationError';
constructor(
message: string,
public status = 403
) {
super(message);
}
}

export function extractBearerTokenFromHeaders({ authorization }: IncomingHttpHeaders): string {
const bearerPrefix = 'Bearer ';

if (!authorization) {
throw new AuthorizationError('Authorization header is missing', 401);
}

if (!authorization.startsWith(bearerPrefix)) {
throw new AuthorizationError(`Authorization header must start with "${bearerPrefix}"`, 401);
}

return authorization.slice(bearerPrefix.length);
}

步骤 2:获取你的 Logto 租户信息

你需要以下信息来校验 Logto 签发的令牌:

  • JSON Web Key Set (JWKS) URI:Logto 公钥的 URL,用于验证 JWT 签名。
  • 发行者 (Issuer):期望的发行者值(Logto 的 OIDC URL)。

首先,找到你的 Logto 租户端点。你可以在以下位置找到:

  • 在 Logto 控制台,设置域名
  • 在你配置的任意应用设置中,设置端点与凭据

从 OpenID Connect 发现端点获取

这些值可以通过 Logto 的 OpenID Connect 发现端点获取:

https://<your-logto-endpoint>/oidc/.well-known/openid-configuration

以下是一个示例响应(为简洁省略了其他字段):

{
"jwks_uri": "https://your-tenant.logto.app/oidc/jwks",
"issuer": "https://your-tenant.logto.app/oidc"
}

由于 Logto 不允许自定义 JWKS URI 或发行者 (Issuer),你可以在代码中硬编码这些值。但不推荐在生产环境中这样做,因为如果将来有配置变更,会增加维护成本。

  • JWKS URI: https://<your-logto-endpoint>/oidc/jwks
  • 发行者 (Issuer): https://<your-logto-endpoint>/oidc

步骤 3:校验令牌和权限 (Permissions)

在提取令牌并获取 OIDC 配置后,校验以下内容:

  • 签名:JWT 必须有效且由 Logto(通过 JWKS)签名。
  • 发行者 (Issuer):必须与你的 Logto 租户发行者一致。
  • 受众 (Audience):必须与在 Logto 注册的 API 资源指示器 (Resource indicator) 匹配,或在适用时匹配组织上下文。
  • 过期时间:令牌必须未过期。
  • 权限 (Scopes):令牌必须包含你 API / 操作所需的权限 (Scopes)。Scopes 是 scope 声明中的以空格分隔的字符串。
  • 组织 (Organization) 上下文:如果保护的是组织级 API 资源,需要校验 organization_id 声明。

参见 JSON Web Token 了解更多关于 JWT 结构和声明 (Claims) 的信息。

不同权限模型下的校验要点

不同权限模型下,声明 (Claims) 和校验规则有所不同:

权限模型受众声明 (aud)组织声明 (organization_id)需校验的权限 (Scopes) (scope)
全局 API 资源API 资源指示器 (Resource indicator)API 资源权限
组织(非 API)权限urn:logto:organization:<id>(组织上下文在 aud 声明中)组织权限
组织级 API 资源API 资源指示器 (Resource indicator)组织 ID(需与请求匹配)API 资源权限

对于非 API 的组织权限,组织上下文通过 aud 声明表示 (如 urn:logto:organization:abc123)。organization_id 声明仅在组织级 API 资源令牌中存在。

提示:

对于多租户 API,务必同时校验权限 (Scopes) 和上下文(受众、组织)以确保安全。

添加校验逻辑

在本示例中,我们使用 jose 来验证 JWT。如果你还没有安装,请先安装:

npm install jose

或者使用你喜欢的包管理器(如 pnpmyarn)。

首先,添加这些用于处理 JWT 验证的通用工具:

jwt-validator.ts
import { createRemoteJWKSet, jwtVerify, JWTPayload } from 'jose';
import { AuthInfo, AuthorizationError } from './auth-middleware.js';

const jwks = createRemoteJWKSet(new URL(JWKS_URI));

export async function validateJwtToken(token: string): Promise<JWTPayload> {
const { payload } = await jwtVerify(token, jwks, {
issuer: ISSUER,
});

verifyPayload(payload);
return payload;
}

export function createAuthInfo(payload: JWTPayload): AuthInfo {
const scopes = (payload.scope as string)?.split(' ') ?? [];
const audience = Array.isArray(payload.aud) ? payload.aud : payload.aud ? [payload.aud] : [];

return new AuthInfo(
payload.sub!,
payload.client_id as string,
payload.organization_id as string,
scopes,
audience
);
}

function verifyPayload(payload: JWTPayload): void {
// 在这里根据权限模型实现你的验证逻辑
// 具体内容将在下方的权限模型部分展示
}

然后,实现用于验证访问令牌 (Access token) 的中间件:

auth-middleware.ts
import { Request, Response, NextFunction } from 'express';
import { extractBearerTokenFromHeaders, AuthorizationError } from './auth-middleware.js';
import { validateJwtToken, createAuthInfo } from './jwt-validator.js';

// 扩展 Express Request 接口以包含 auth
declare global {
namespace Express {
interface Request {
auth?: AuthInfo;
}
}
}

export async function verifyAccessToken(req: Request, res: Response, next: NextFunction) {
try {
const token = extractBearerTokenFromHeaders(req.headers);
const payload = await validateJwtToken(token);

// 将认证信息存储在 request 中以便通用使用
req.auth = createAuthInfo(payload);

next();
} catch (err: any) {
return res.status(err.status ?? 401).json({ error: err.message });
}
}

根据你的权限模型,在 jwt-validator.ts 中实现相应的验证逻辑:

jwt-validator.ts
function verifyPayload(payload: JWTPayload): void {
// 检查 audience 声明是否匹配你的 API 资源指示器 (Resource indicator)
const audiences = Array.isArray(payload.aud) ? payload.aud : payload.aud ? [payload.aud] : [];
if (!audiences.includes('https://your-api-resource-indicator')) {
throw new AuthorizationError('Invalid audience');
}

// 检查全局 API 资源所需的权限 (Scopes)
const requiredScopes = ['api:read', 'api:write']; // 替换为你实际需要的权限
const scopes = (payload.scope as string)?.split(' ') ?? [];
if (!requiredScopes.every((scope) => scopes.includes(scope))) {
throw new AuthorizationError('Insufficient scope');
}
}

步骤 4:为你的 API 应用中间件

将中间件应用到你需要保护的 API 路由。

app.ts
import express from 'express';
import { verifyAccessToken } from './auth-middleware.js';

const app = express();

app.get('/api/protected', verifyAccessToken, (req, res) => {
// 直接从 req.auth 获取认证 (Authentication) 信息
res.json({ auth: req.auth });
});

app.get('/api/protected/detailed', verifyAccessToken, (req, res) => {
// 你的受保护接口逻辑
res.json({
auth: req.auth,
message: '受保护数据访问成功',
});
});

app.listen(3000);

步骤 5:测试你的实现

使用有效和无效的令牌测试你的 API,确保:

  • 有效令牌可以通过校验并获得访问权限。
  • 对于无效 / 缺失令牌返回 401 Unauthorized,对于有效但缺少所需权限或上下文的令牌返回 403 Forbidden
自定义令牌声明 (Claims) JSON Web Token (JWT) OpenID Connect 发现

RFC 8707:资源指示器 (Resource indicator)