跳转到内容
搜索文档

跨域身份验证

最后更新 查看 MarkdownAgent 设置

部署 Agent 后,为保持安全,客户端应发送 token,服务器端进行验证。本指南涵盖连接到 agent 的 WebSocket 身份验证模式。

WebSocket 身份验证

WebSocket 不是 HTTP,因此在跨域连接时握手能力有限。

你无法发送:

  • 升级过程中的自定义标头
  • 连接时的 Authorization: Bearer ...

你可以:

  • 在连接 URL 的查询参数中放入签名、短时效 token
  • 在服务器的连接路径中验证 token

同源

如果客户端与服务器共享源,浏览器会在 WebSocket 握手期间发送 cookie。基于 session 的身份验证在此场景下可用。请优先使用 HTTP-only cookie。

跨源

跨源时 cookie 无效。在 URL 查询中传递凭据,然后在服务器端验证。

用法示例

静态身份验证

import { useAgent } from "agents/react";

function ChatComponent() {
	const agent = useAgent({
		agent: "my-agent",
		query: {
			token: "demo-token-123",
			userId: "demo-user",
		},
	});

	// Use agent to make calls, access state, etc.
}
import { useAgent } from "agents/react";

function ChatComponent() {
	const agent = useAgent({
		agent: "my-agent",
		query: {
			token: "demo-token-123",
			userId: "demo-user",
		},
	});

	// Use agent to make calls, access state, etc.
}

异步身份验证

在连接前构建查询值。异步设置请使用 Suspense。

import { useAgent } from "agents/react";
import { Suspense, useCallback } from "react";

function ChatComponent() {
	const asyncQuery = useCallback(async () => {
		const [token, user] = await Promise.all([getAuthToken(), getCurrentUser()]);
		return {
			token,
			userId: user.id,
			timestamp: Date.now().toString(),
		};
	}, []);

	const agent = useAgent({
		agent: "my-agent",
		query: asyncQuery,
	});

	// Use agent to make calls, access state, etc.
}

function App() {
	return (
		<Suspense fallback={<div>Authenticating...</div>}>
			<ChatComponent />
		</Suspense>
	);
}
import { useAgent } from "agents/react";
import { Suspense, useCallback } from "react";

function ChatComponent() {
	const asyncQuery = useCallback(async () => {
		const [token, user] = await Promise.all([getAuthToken(), getCurrentUser()]);
		return {
			token,
			userId: user.id,
			timestamp: Date.now().toString(),
		};
	}, []);

	const agent = useAgent({
		agent: "my-agent",
		query: asyncQuery,
	});

	// Use agent to make calls, access state, etc.
}

function App() {
	return (
		<Suspense fallback={<div>Authenticating...</div>}>
			<ChatComponent />
		</Suspense>
	);
}

JWT 刷新模式

当连接因身份验证错误失败时刷新 token。

import { useAgent } from "agents/react";
import { useCallback } from "react";

const validateToken = async (token) => {
	// An example of how you might implement this
	const res = await fetch(`${API_HOST}/api/users/me`, {
		headers: {
			Authorization: `Bearer ${token}`,
		},
	});

	return res.ok;
};

const refreshToken = async () => {
	// Depends on implementation:
	// - You could use a longer-lived token to refresh the expired token
	// - De-auth the app and prompt the user to log in manually
	// - ...
};

function useJWTAgent(agentName) {
	const asyncQuery = useCallback(async () => {
		let token = localStorage.getItem("jwt");

		// If no token OR the token is no longer valid
		// request a fresh token
		if (!token || !(await validateToken(token))) {
			token = await refreshToken();
			localStorage.setItem("jwt", token);
		}

		return {
			token,
		};
	}, []);

	const agent = useAgent({
		agent: agentName,
		query: asyncQuery,
		queryDeps: [], // Run on mount
	});

	return agent;
}
import { useAgent } from "agents/react";
import { useCallback } from "react";

const validateToken = async (token: string) => {
	// An example of how you might implement this
	const res = await fetch(`${API_HOST}/api/users/me`, {
		headers: {
			Authorization: `Bearer ${token}`,
		},
	});

	return res.ok;
};

const refreshToken = async () => {
	// Depends on implementation:
	// - You could use a longer-lived token to refresh the expired token
	// - De-auth the app and prompt the user to log in manually
	// - ...
};

function useJWTAgent(agentName: string) {
	const asyncQuery = useCallback(async () => {
		let token = localStorage.getItem("jwt");

		// If no token OR the token is no longer valid
		// request a fresh token
		if (!token || !(await validateToken(token))) {
			token = await refreshToken();
			localStorage.setItem("jwt", token);
		}

		return {
			token,
		};
	}, []);

	const agent = useAgent({
		agent: agentName,
		query: asyncQuery,
		queryDeps: [], // Run on mount
	});

	return agent;
}

跨域身份验证

连接到其他主机时,在 URL 中传递凭据,然后在服务器端验证。

静态跨域身份验证

import { useAgent } from "agents/react";

function StaticCrossDomainAuth() {
	const agent = useAgent({
		agent: "my-agent",
		host: "https://my-agent.example.workers.dev",
		query: {
			token: "demo-token-123",
			userId: "demo-user",
		},
	});

	// Use agent to make calls, access state, etc.
}
import { useAgent } from "agents/react";

function StaticCrossDomainAuth() {
	const agent = useAgent({
		agent: "my-agent",
		host: "https://my-agent.example.workers.dev",
		query: {
			token: "demo-token-123",
			userId: "demo-user",
		},
	});

	// Use agent to make calls, access state, etc.
}

异步跨域身份验证

import { useAgent } from "agents/react";
import { useCallback } from "react";

function AsyncCrossDomainAuth() {
	const asyncQuery = useCallback(async () => {
		const [token, user] = await Promise.all([getAuthToken(), getCurrentUser()]);
		return {
			token,
			userId: user.id,
			timestamp: Date.now().toString(),
		};
	}, []);

	const agent = useAgent({
		agent: "my-agent",
		host: "https://my-agent.example.workers.dev",
		query: asyncQuery,
	});

	// Use agent to make calls, access state, etc.
}
import { useAgent } from "agents/react";
import { useCallback } from "react";

function AsyncCrossDomainAuth() {
	const asyncQuery = useCallback(async () => {
		const [token, user] = await Promise.all([getAuthToken(), getCurrentUser()]);
		return {
			token,
			userId: user.id,
			timestamp: Date.now().toString(),
		};
	}, []);

	const agent = useAgent({
		agent: "my-agent",
		host: "https://my-agent.example.workers.dev",
		query: asyncQuery,
	});

	// Use agent to make calls, access state, etc.
}

服务器端验证

在服务器端,在 onConnect 处理程序中验证 token:

import { Agent, Connection, ConnectionContext } from "agents";

export class SecureAgent extends Agent {
	async onConnect(connection, ctx) {
		const url = new URL(ctx.request.url);
		const token = url.searchParams.get("token");
		const userId = url.searchParams.get("userId");

		// Verify the token
		if (!token || !(await this.verifyToken(token, userId))) {
			connection.close(4001, "Unauthorized");
			return;
		}

		// Store user info on the connection state
		connection.setState({ userId, authenticated: true });
	}

	async verifyToken(token, userId) {
		// Implement your token verification logic
		// For example, verify a JWT signature, check expiration, etc.
		try {
			const payload = await verifyJWT(token, this.env.JWT_SECRET);
			return payload.sub === userId && payload.exp > Date.now() / 1000;
		} catch {
			return false;
		}
	}

	async onMessage(connection, message) {
		// Check if connection is authenticated
		if (!connection.state?.authenticated) {
			connection.send(JSON.stringify({ error: "Not authenticated" }));
			return;
		}

		// Process message for authenticated user
		const userId = connection.state.userId;
		// ...
	}
}
import { Agent, Connection, ConnectionContext } from "agents";

export class SecureAgent extends Agent {
	async onConnect(connection: Connection, ctx: ConnectionContext) {
		const url = new URL(ctx.request.url);
		const token = url.searchParams.get("token");
		const userId = url.searchParams.get("userId");

		// Verify the token
		if (!token || !(await this.verifyToken(token, userId))) {
			connection.close(4001, "Unauthorized");
			return;
		}

		// Store user info on the connection state
		connection.setState({ userId, authenticated: true });
	}

	private async verifyToken(token: string, userId: string): Promise<boolean> {
		// Implement your token verification logic
		// For example, verify a JWT signature, check expiration, etc.
		try {
			const payload = await verifyJWT(token, this.env.JWT_SECRET);
			return payload.sub === userId && payload.exp > Date.now() / 1000;
		} catch {
			return false;
		}
	}

	async onMessage(connection: Connection, message: string) {
		// Check if connection is authenticated
		if (!connection.state?.authenticated) {
			connection.send(JSON.stringify({ error: "Not authenticated" }));
			return;
		}

		// Process message for authenticated user
		const userId = connection.state.userId;
		// ...
	}
}

最佳实践

  1. 使用短时效 token — URL 中的 token 可能被记录。保持较短过期时间(分钟级,而非小时级)。

  2. 适当限定 token 范围 — 在 token 声明中包含 agent 名称或实例,防止 token 跨 agent 复用。

  3. 每次连接都验证 — 始终在 onConnect 中验证 token,而非仅验证一次。

  4. 使用 HTTPS — 生产环境始终使用安全 WebSocket 连接(wss://)。

  5. 轮换密钥 — 定期轮换 JWT 签名密钥或 token 密钥。

  6. 记录身份验证失败 — 跟踪失败的身份验证尝试以进行安全监控。

后续步骤

路由

路由与身份验证钩子。

GitHub OAuth agent 示例

使用 GitHub OAuth、HTTP-only cookie 和服务器拥有的 Durable Object 路由保护基于 Agents 构建的应用。

这篇文档对您有帮助吗?