Playwright MCP身份认证集成:OAuth2与SAML单点登录实现全指南
Playwright MCP身份认证集成:OAuth2与SAML单点登录实现全指南
引言:身份认证的痛点与解决方案
在现代Web自动化测试中,身份认证(Authentication)是每个测试流程的关键环节。然而,传统认证方式面临诸多挑战:
- Cookie/Session依赖:测试状态与浏览器上下文强耦合,难以实现并行测试
- 凭证管理混乱:硬编码的用户名密码导致安全漏洞和维护成本上升
- 多系统认证复杂:企业级应用常采用OAuth2(开放授权)或SAML(安全断言标记语言)等标准协议,手动处理流程繁琐
Playwright MCP(Multi-Context Protocol)通过创新的跨上下文通信机制,提供了企业级身份认证的完整解决方案。本文将深入剖析如何基于Playwright MCP架构实现OAuth2与SAML单点登录(Single Sign-On, SSO),构建安全、高效、可扩展的自动化测试认证体系。
读完本文,您将掌握:
- Playwright MCP的认证架构与安全模型
- OAuth2授权码流程的自动化实现(含PKCE增强)
- SAML 2.0断言消费与会话管理
- 企业级认证测试的最佳实践与性能优化
- 跨浏览器上下文的认证状态共享方案
Playwright MCP认证架构解析
核心组件与通信流程
Playwright MCP的认证系统基于Chromium DevTools Protocol(CDP)构建,通过扩展程序(Extension)实现跨上下文通信。其核心架构包含三个关键组件:
跨上下文认证流程如下:
安全通信机制
RelayConnection类通过CDP事件转发实现安全通信:
// 扩展程序中CDP事件处理核心代码 (relayConnection.ts)
private _onDebuggerEvent(source: chrome.debugger.DebuggerSession, method: string, params: any): void {
if (source.tabId !== this._debuggee.tabId)
return;
debugLog('Forwarding CDP event:', method, params);
const sessionId = source.sessionId;
this._sendMessage({
method: 'forwardCDPEvent',
params: {
sessionId,
method,
params,
},
});
}
TabShareExtension管理认证会话的生命周期,确保认证状态在上下文间安全传递:
// 会话管理核心实现 (background.ts)
private async _setConnectedTabId(tabId: number | null): Promise<void> {
const oldTabId = this._connectedTabId;
this._connectedTabId = tabId;
if (oldTabId && oldTabId !== tabId)
await this._updateBadge(oldTabId, { text: '' });
if (tabId)
await this._updateBadge(tabId, {
text: '✓',
color: '#4CAF50',
title: 'Connected to MCP client'
});
}
OAuth2认证流程实现
OAuth2授权码流程详解
OAuth2授权码流程(Authorization Code Flow)是目前最安全的第三方认证方式,其流程如下:
针对公共客户端场景,需添加PKCE(Proof Key for Code Exchange)增强:
- 客户端生成随机
code_verifier - 计算
code_challenge = BASE64URL-ENCODE(SHA256(code_verifier)) - 授权请求中携带
code_challenge和code_challenge_method=S256 - 令牌请求中使用
code_verifier验证身份
基于Playwright MCP的实现
1. 认证配置模型
// OAuth2配置接口定义
interface OAuth2Config {
clientId: string;
clientSecret?: string; // 公共客户端可选
authorizationEndpoint: string;
tokenEndpoint: string;
redirectUri: string;
scope: string;
pkce?: boolean; // 是否启用PKCE
}
// 认证会话模型
interface AuthSession {
accessToken: string;
tokenType: string;
expiresIn: number;
refreshToken?: string;
scope: string;
expiresAt: number; // 时间戳
}
2. PKCE代码挑战生成
// PKCE工具函数
class PKCEUtil {
static generateCodeVerifier(): string {
const array = new Uint32Array(56);
crypto.getRandomValues(array);
return Array.from(array, dec => ('0' + dec.toString(16)).substr(-2)).join('');
}
static async generateCodeChallenge(verifier: string): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const digest = await crypto.subtle.digest('SHA-256', data);
return btoa(String.fromCharCode(...new Uint8Array(digest)))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
}
3. 授权码流程实现
async function authenticateViaOAuth2(config: OAuth2Config): Promise<AuthSession> {
// 1. 生成PKCE参数(如启用)
const codeVerifier = config.pkce ? PKCEUtil.generateCodeVerifier() : undefined;
const codeChallenge = codeVerifier ? await PKCEUtil.generateCodeChallenge(codeVerifier) : undefined;
// 2. 创建认证页面
const page = await context.newPage();
// 3. 构建授权URL
const authUrl = new URL(config.authorizationEndpoint);
authUrl.searchParams.set('client_id', config.clientId);
authUrl.searchParams.set('redirect_uri', config.redirectUri);
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('scope', config.scope);
if (config.pkce) {
authUrl.searchParams.set('code_challenge', codeChallenge!);
authUrl.searchParams.set('code_challenge_method', 'S256');
}
// 4. 导航到认证页面并等待重定向
const [response] = await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle' }),
page.goto(authUrl.toString())
]);
// 5. 提取授权码
const redirectUrl = new URL(response.url());
const code = redirectUrl.searchParams.get('code');
if (!code) throw new Error('授权码获取失败');
// 6. 交换访问令牌
const tokenResponse = await fetch(config.tokenEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: config.clientId,
client_secret: config.clientSecret || '',
redirect_uri: config.redirectUri,
code,
...(config.pkce && { code_verifier: codeVerifier })
})
});
// 7. 处理令牌响应
const tokenData = await tokenResponse.json();
return {
...tokenData,
expiresAt: Date.now() + (tokenData.expiresIn * 1000)
};
}
4. MCP集成与上下文共享
通过TabShareExtension的_connectTab方法实现认证状态共享:
// 扩展认证功能的TabShareExtension
class AuthTabShareExtension extends TabShareExtension {
async authenticateWithOAuth2(config: OAuth2Config): Promise<AuthSession> {
// 1. 创建专用认证标签页
const authTab = await chrome.tabs.create({
url: chrome.runtime.getURL('auth.html'),
active: true
});
// 2. 建立MCP连接
await this._connectToRelay(authTab.id!, config.mcpRelayUrl);
// 3. 执行OAuth2流程
const session = await authenticateViaOAuth2(config);
// 4. 共享认证状态
await this._connectTab(authTab.id!, config.targetTabId, config.windowId, config.mcpRelayUrl);
return session;
}
}
SAML 2.0单点登录实现
SAML认证原理
SAML 2.0通过XML格式的安全断言实现跨域认证,其核心流程包括:
SAML断言包含三种类型:
- 认证断言(Authentication Assertion):证明用户已认证
- 属性断言(Attribute Assertion):包含用户属性信息
- 授权决策断言(Authorization Decision Assertion):包含访问控制决策
Playwright MCP实现方案
1. SAML配置与断言处理
// SAML配置接口
interface SAMLConfig {
idpSsoUrl: string; // IdP单点登录URL
spEntityId: string; // 服务提供商实体ID
assertionConsumerServiceUrl: string; // 断言消费服务URL
nameIdFormat?: string; // 名称ID格式
signatureAlgorithm?: string; // 签名算法
}
// SAML响应解析结果
interface SAMLResponse {
nameId: string;
sessionIndex: string;
attributes: Record<string, string[]>;
validUntil: Date;
}
2. SAML请求生成
function generateSAMLRequest(config: SAMLConfig): string {
const requestId = `_${uuidv4()}`;
const issueInstant = new Date().toISOString();
// SAML请求XML
const samlRequestXml = `
<samlp:AuthnRequest
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
ID="${requestId}"
Version="2.0"
IssueInstant="${issueInstant}"
ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
AssertionConsumerServiceURL="${config.assertionConsumerServiceUrl}"
Destination="${config.idpSsoUrl}">
<saml:Issuer>${config.spEntityId}</saml:Issuer>
<samlp:NameIDPolicy
Format="${config.nameIdFormat || 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified'}"
AllowCreate="true"/>
</samlp:AuthnRequest>
`.trim();
// 压缩并编码SAML请求
const compressed = zlib.deflateSync(samlRequestXml, { level: 9 });
return btoa(String.fromCharCode(...new Uint8Array(compressed)));
}
3. SAML响应解析与验证
async function parseAndVerifySAMLResponse(samlResponseBase64: string, config: SAMLConfig): Promise<SAMLResponse> {
// 解码并解析SAML响应
const samlResponseXml = zlib.inflateSync(
Uint8Array.from(atob(samlResponseBase64), c => c.charCodeAt(0))
).toString();
// 验证签名(此处省略具体实现,需使用xml-crypto等库)
const isSignatureValid = await verifySAMLSignature(samlResponseXml, config);
if (!isSignatureValid) throw new Error('SAML响应签名验证失败');
// 解析XML获取断言信息
const doc = new DOMParser().parseFromString(samlResponseXml, 'text/xml');
const xpath = (path: string) => doc.evaluate(path, doc, null, XPathResult.STRING_TYPE, null).stringValue;
return {
nameId: xpath('//saml:NameID/text()'),
sessionIndex: xpath('//samlp:Response/samlp:AssertionConsumerServiceIndex/text()'),
attributes: extractSAMLAttributes(doc),
validUntil: new Date(xpath('//saml:Conditions/@NotOnOrAfter')),
};
}
// 提取SAML属性
function extractSAMLAttributes(doc: Document): Record<string, string[]> {
const attributes: Record<string, string[]> = {};
const attributeNodes = doc.evaluate(
'//saml:AttributeStatement/saml:Attribute',
doc,
null,
XPathResult.ORDERED_NODE_ITERATOR_TYPE,
null
);
let node: Node | null;
while ((node = attributeNodes.iterateNext())) {
const name = (node as Element).getAttribute('Name');
if (!name) continue;
const values: string[] = [];
const valueNodes = doc.evaluate(
'./saml:AttributeValue/text()',
node,
null,
XPathResult.ORDERED_NODE_ITERATOR_TYPE,
null
);
let valueNode: Node | null;
while ((valueNode = valueNodes.iterateNext())) {
values.push(valueNode.textContent || '');
}
attributes[name] = values;
}
return attributes;
}
4. MCP上下文中的SAML认证集成
async function authenticateWithSAML(config: SAMLConfig & { mcpRelayUrl: string }): Promise<SAMLResponse> {
// 1. 生成SAML请求
const samlRequest = generateSAMLRequest(config);
// 2. 创建SAML认证标签页
const authTab = await chrome.tabs.create({
url: `${config.idpSsoUrl}?SAMLRequest=${encodeURIComponent(samlRequest)}`,
active: true
});
// 3. 建立MCP连接
const tabShareExtension = new TabShareExtension();
await tabShareExtension._connectToRelay(authTab.id!, config.mcpRelayUrl);
// 4. 等待断言消费完成
const samlResponse = await new Promise<SAMLResponse>((resolve) => {
// 监听断言消费服务页面的消息
chrome.runtime.onMessage.addListener(function listener(message: any) {
if (message.type === 'samlResponseReceived') {
chrome.runtime.onMessage.removeListener(listener);
resolve(parseAndVerifySAMLResponse(message.samlResponse, config));
}
});
});
// 5. 共享认证状态到目标标签页
await tabShareExtension._connectTab(
authTab.id!,
config.targetTabId,
config.windowId,
config.mcpRelayUrl
);
return samlResponse;
}
企业级认证测试最佳实践
认证状态管理
会话持久化策略
class SessionManager {
private _storagePath: string;
constructor(storagePath: string = './auth-sessions') {
this._storagePath = storagePath;
fs.mkdirSync(this._storagePath, { recursive: true });
}
// 保存会话
async saveSession(key: string, session: AuthSession | SAMLResponse): Promise<void> {
const sessionPath = path.join(this._storagePath, `${key}.json`);
await fs.promises.writeFile(
sessionPath,
JSON.stringify({ ...session, savedAt: Date.now() }, null, 2)
);
}
// 加载会话(自动检查有效性)
async loadSession(key: string): Promise<AuthSession | SAMLResponse | null> {
const sessionPath = path.join(this._storagePath, `${key}.json`);
try {
const session = JSON.parse(await fs.promises.readFile(sessionPath, 'utf8'));
// 检查会话有效性
if ('expiresAt' in session && session.expiresAt < Date.now()) {
await this.deleteSession(key);
return null;
}
if ('validUntil' in session && new Date(session.validUntil) < new Date()) {
await this.deleteSession(key);
return null;
}
return session;
} catch (error) {
return null;
}
}
// 删除会话
async deleteSession(key: string): Promise<void> {
const sessionPath = path.join(this._storagePath, `${key}.json`);
await fs.promises.unlink(sessionPath).catch(() => {});
}
}
多环境认证配置管理
// 环境配置示例
const authConfigs = {
development: {
oauth2: {
clientId: 'dev-client-id',
authorizationEndpoint: 'https://dev-idp.example.com/oauth2/authorize',
tokenEndpoint: 'https://dev-idp.example.com/oauth2/token',
redirectUri: 'http://localhost:3000/auth/callback',
scope: 'openid profile email',
pkce: true
},
saml: {
idpSsoUrl: 'https://dev-idp.example.com/saml/sso',
spEntityId: 'https://dev-app.example.com/saml/metadata',
assertionConsumerServiceUrl: 'https://dev-app.example.com/saml/acs'
}
},
production: {
// 生产环境配置...
}
};
// 使用环境变量选择配置
const env = process.env.TEST_ENV || 'development';
const currentConfig = authConfigs[env as keyof typeof authConfigs];
安全性增强措施
1. 凭证加密存储
// 使用加密模块保护敏感凭证
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';
class CredentialEncryptor {
private readonly algorithm = 'aes-256-gcm';
private readonly key: Buffer;
constructor(encryptionKey: string) {
// 确保密钥长度为32字节(256位)
this.key = Buffer.from(encryptionKey.padEnd(32, 'x').slice(0, 32));
}
encrypt(text: string): string {
const iv = randomBytes(12);
const salt = randomBytes(64);
const cipher = createCipheriv(this.algorithm, this.key, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag().toString('hex');
return JSON.stringify({
iv: iv.toString('hex'),
salt: salt.toString('hex'),
encryptedData: encrypted,
authTag
});
}
decrypt(encryptedText: string): string {
const { iv, salt, encryptedData, authTag } = JSON.parse(encryptedText);
const decipher = createDecipheriv(
this.algorithm,
this.key,
Buffer.from(iv, 'hex')
);
decipher.setAuthTag(Buffer.from(authTag, 'hex'));
let decrypted = decipher.update(encryptedData, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
}
2. 认证流程超时控制
// 带超时控制的认证函数
async function withTimeout<T>(
promise: Promise<T>,
timeoutMs: number,
errorMessage: string = 'Operation timed out'
): Promise<T> {
return Promise.race([
promise,
new Promise<T>((_, reject) =>
setTimeout(() => reject(new Error(errorMessage)), timeoutMs)
)
]);
}
// 使用示例
try {
const session = await withTimeout(
authenticateViaOAuth2(config),
30000, // 30秒超时
'OAuth2认证流程超时,请检查网络连接或IdP可用性'
);
} catch (error) {
console.error('认证失败:', error.message);
}
性能优化策略
1. 认证缓存与预热
class AuthCache {
private cache = new Map<string, {
session: AuthSession | SAMLResponse;
timestamp: number;
}>();
private ttl: number; // 缓存过期时间(毫秒)
constructor(ttl: number = 3600000) { // 默认1小时
this.ttl = ttl;
}
get(key: string): AuthSession | SAMLResponse | null {
const entry = this.cache.get(key);
if (!entry) return null;
// 检查缓存是否过期
if (Date.now() - entry.timestamp > this.ttl) {
this.cache.delete(key);
return null;
}
return entry.session;
}
set(key: string, session: AuthSession | SAMLResponse): void {
this.cache.set(key, {
session,
timestamp: Date.now()
});
}
delete(key: string): void {
this.cache.delete(key);
}
clear(): void {
this.cache.clear();
}
}
// 预热认证缓存
async function warmupAuthCache(cache: AuthCache, configs: any): Promise<void> {
const authPromises = [
authenticateViaOAuth2(configs.oauth2).then(session =>
cache.set('oauth2-default', session)
),
authenticateWithSAML(configs.saml).then(response =>
cache.set('saml-default', response)
)
];
await Promise.allSettled(authPromises);
}
2. 并行认证会话管理
// 并行认证管理器
class ParallelAuthManager {
private readonly maxParallelSessions: number;
private activeSessions = 0;
private sessionQueue: (() => Promise<void>)[] = [];
constructor(maxParallelSessions: number = 5) {
this.maxParallelSessions = maxParallelSessions;
}
// 提交认证任务
submitAuthTask<T>(task: () => Promise<T>): Promise<T> {
return new Promise((resolve, reject) => {
this.sessionQueue.push(async () => {
try {
const result = await task();
resolve(result);
} catch (error) {
reject(error);
} finally {
this.activeSessions--;
this.processQueue();
}
});
this.processQueue();
});
}
// 处理任务队列
private processQueue(): void {
while (this.activeSessions < this.maxParallelSessions && this.sessionQueue.length > 0) {
const task = this.sessionQueue.shift();
if (task) {
this.activeSessions++;
task();
}
}
}
}
// 使用示例
const authManager = new ParallelAuthManager(3); // 最多3个并行会话
// 提交多个认证任务
const userIds = ['user1', 'user2', 'user3', 'user4', 'user5'];
const authTasks = userIds.map(userId =>
authManager.submitAuthTask(() => authenticateUser(userId))
);
const results = await Promise.allSettled(authTasks);
跨浏览器上下文认证共享
MCP连接池实现
class MCPConnectionPool {
private connections = new Map<number, RelayConnection>(); // tabId -> connection
private connectionTimeout: NodeJS.Timeout | null = null;
constructor(private readonly timeoutMs: number = 300000) { // 5分钟超时
this.startCleanupTimer();
}
// 获取或创建连接
async getConnection(tabId: number, relayUrl: string): Promise<RelayConnection> {
// 检查现有连接
const existingConnection = this.connections.get(tabId);
if (existingConnection) return existingConnection;
// 创建新连接
const socket = new WebSocket(relayUrl);
await new Promise<void>((resolve, reject) => {
socket.onopen = () => resolve();
socket.onerror = () => reject(new Error('WebSocket连接失败'));
setTimeout(() => reject(new Error('WebSocket连接超时')), 10000);
});
const connection = new RelayConnection(socket);
connection.onclose = () => this.connections.delete(tabId);
this.connections.set(tabId, connection);
return connection;
}
// 清理超时连接
private startCleanupTimer(): void {
this.connectionTimeout = setInterval(() => {
const now = Date.now();
for (const [tabId, connection] of this.connections) {
// 此处假设RelayConnection有lastActivity属性记录最后活动时间
if (now - (connection as any).lastActivity > this.timeoutMs) {
connection.close('连接超时自动关闭');
this.connections.delete(tabId);
}
}
}, 60000); // 每分钟检查一次
}
// 关闭所有连接
closeAllConnections(): void {
if (this.connectionTimeout) clearInterval(this.connectionTimeout);
for (const connection of this.connections.values()) {
connection.close('连接池关闭');
}
this.connections.clear();
}
}
测试场景集成示例
// 完整测试用例示例
import { test, expect } from '@playwright/test';
import { SessionManager } from './session-manager';
import { authenticateViaOAuth2, authenticateWithSAML } from './auth-providers';
const sessionManager = new SessionManager('./sessions');
const authConfig = require('../config/auth-config.json')[process.env.TEST_ENV || 'development'];
test.describe('企业应用认证测试', () => {
let oauthSession;
let samlResponse;
test.beforeAll(async () => {
// 尝试从缓存加载会话,避免重复认证
oauthSession = await sessionManager.loadSession('oauth2-user1');
samlResponse = await sessionManager.loadSession('saml-user1');
// 如果没有缓存或缓存过期,则执行认证
if (!oauthSession) {
oauthSession = await authenticateViaOAuth2({
...authConfig.oauth2,
mcpRelayUrl: 'ws://localhost:8080/mcp-relay'
});
await sessionManager.saveSession('oauth2-user1', oauthSession);
}
if (!samlResponse) {
samlResponse = await authenticateWithSAML({
...authConfig.saml,
mcpRelayUrl: 'ws://localhost:8080/mcp-relay'
});
await sessionManager.saveSession('saml-user1', samlResponse);
}
});
test('OAuth2认证用户访问受保护资源', async ({ context }) => {
// 使用MCP共享认证状态
const page = await context.newPage();
// 设置认证头
await page.route('**/*', route => {
route.continue({
headers: {
...route.request().headers(),
'Authorization': `${oauthSession.tokenType} ${oauthSession.accessToken}`
}
});
});
// 访问受保护资源
await page.goto('https://app.example.com/dashboard');
// 验证认证成功
await expect(page.locator('header .user-info')).toContainText(
oauthSession.userInfo?.email || '已认证用户'
);
});
test('SAML单点登录后访问用户资料', async ({ context }) => {
const page = await context.newPage();
// 通过MCP连接共享SAML会话
const connectionPool = new MCPConnectionPool();
const connection = await connectionPool.getConnection(
page.context().pages()[0].id(),
'ws://localhost:8080/mcp-relay'
);
// 验证SAML属性
expect(samlResponse.nameId).toBeTruthy();
expect(samlResponse.attributes.email).toContain('@example.com');
// 访问应用
await page.goto('https://app.example.com/profile');
await expect(page.locator('#fullname')).toContainText(
samlResponse.attributes.cn?.[0] || ''
);
});
test.afterAll(async () => {
// 清理测试会话(可选)
// await sessionManager.deleteSession('oauth2-user1');
// await sessionManager.deleteSession('saml-user1');
});
});
总结与展望
本文详细阐述了基于Playwright MCP实现企业级身份认证的完整方案,包括:
- 架构解析:深入分析了RelayConnection和TabShareExtension核心组件,揭示了MCP跨上下文通信的底层机制
- 协议实现:提供了OAuth2(含PKCE增强)和SAML 2.0两种主流认证协议的完整实现代码
- 最佳实践:从安全、性能、可维护性角度,提供了企业级认证测试的实用策略
- 测试集成:通过完整测试用例展示了如何在实际测试场景中应用这些认证方案
关键收获
- Playwright MCP的跨上下文通信能力为解决复杂认证问题提供了全新思路
- OAuth2的PKCE扩展是公共客户端场景的安全必备
- SAML断言解析需要严格的XML验证和签名校验
- 认证会话的缓存与管理对测试效率提升至关重要
- 并行认证和连接池技术可显著提高大规模测试的性能
未来发展方向
- 生物认证集成:探索WebAuthn/FIDO2协议的自动化测试方案
- 多因素认证(MFA):实现TOTP/HOTP等二次验证机制的自动化处理
- 认证分析工具:开发基于MCP的认证流程录制与回放工具
- 云原生认证:适配AWS Cognito、Azure AD等云身份服务的专用适配器
通过Playwright MCP的强大能力,企业级应用的身份认证测试不再是自动化流程中的绊脚石,而是可以通过系统化、工程化的方式得到完美解决。随着Web安全协议的不断演进,我们期待Playwright MCP能够支持更多新兴认证标准,为自动化测试领域带来更多创新解决方案。
附录:常见问题与解决方案
Q1: 如何处理OAuth2访问令牌过期?
A: 实现自动刷新机制:
async function refreshOAuthToken(config: OAuth2Config, refreshToken: string): Promise<AuthSession> {
const tokenResponse = await fetch(config.tokenEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'refresh_token',
client_id: config.clientId,
client_secret: config.clientSecret || '',
refresh_token: refreshToken
})
});
if (!tokenResponse.ok) throw new Error('令牌刷新失败');
const tokenData = await tokenResponse.json();
return {
...tokenData,
expiresAt: Date.now() + (tokenData.expiresIn * 1000)
};
}
// 使用拦截器自动刷新令牌
class TokenRefresher {
private readonly config: OAuth2Config;
private readonly sessionManager: SessionManager;
constructor(config: OAuth2Config, sessionManager: SessionManager) {
this.config = config;
this.sessionManager = sessionManager;
}
async intercept(request: Request): Promise<Request> {
const session = await this.sessionManager.loadSession('current-user');
if (!session) throw new Error('无有效认证会话');
// 检查令牌是否即将过期(30秒内)
if (session.expiresAt - Date.now() < 30000) {
try {
const newSession = await refreshOAuthToken(this.config, session.refreshToken!);
await this.sessionManager.saveSession('current-user', newSession);
return this.updateRequestAuthHeader(request, newSession);
} catch (error) {
// 刷新失败,需要重新认证
throw new Error('令牌刷新失败,请重新认证');
}
}
return this.updateRequestAuthHeader(request, session);
}
private updateRequestAuthHeader(request: Request, session: AuthSession): Request {
const headers = new Headers(request.headers);
headers.set('Authorization', `${session.tokenType} ${session.accessToken}`);
return new Request(request, { headers });
}
}
Q2: 如何处理SAML断言的签名验证?
A: 使用专业XML安全库:
import * as xmlCrypto from 'xml-crypto';
import * as xpath from 'xpath';
function verifySAMLSignature(samlResponseXml: string, config: SAMLConfig): boolean {
const doc = new DOMParser().parseFromString(samlResponseXml, 'text/xml');
// 查找签名节点
const signatureNode = xpath.select(
"//*[local-name(.)='Signature']",
doc
)[0] as Element;
if (!signatureNode) return false;
// 创建签名验证器
const sig = new xmlCrypto.SignedXml();
sig.loadSignature(signatureNode);
// 获取用于验证的公钥(通常从IdP元数据获取)
const publicKey = getPublicKeyFromIdPMetadata(config.idpMetadataUrl);
// 验证签名
return sig.checkSignature(publicKey);
}
// 从IdP元数据获取公钥
async function getPublicKeyFromIdPMetadata(metadataUrl: string): Promise<string> {
const response = await fetch(metadataUrl);
const metadataXml = await response.text();
const doc = new DOMParser().parseFromString(metadataXml, 'text/xml');
const xpathResult = xpath.select(
"//*[local-name(.)='KeyDescriptor' and @use='signing']/*[local-name(.)='KeyInfo']/*[local-name(.)='X509Data']/*[local-name(.)='X509Certificate']/text()",
doc
);
return xpathResult.length > 0 ? `-----BEGIN CERTIFICATE-----\n${xpathResult[0]}\n-----END CERTIFICATE-----` : '';
}
更多推荐



所有评论(0)