跨域认证
当你的 Agent 部署后,为了保证安全,从客户端发送一个 token,然后在服务器端验证它。本指南涵盖与 agent 的 WebSocket 连接的认证模式。
WebSocket 认证
WebSocket 不是 HTTP,因此在跨域连接时握手是受限的。
你不能:
- 在升级期间发送自定义头
- 在连接时发送
Authorization: Bearer ...
你可以:
- 在连接 URL 中作为查询参数放置一个签名的、短期有效的 token
- 在你的服务器连接路径中验证 token
注意
切勿在 URL 中放置原始秘密。使用快速过期、限定到用户或房间的 JWT 或签名 token。
同源
如果客户端和服务器共享源,浏览器在 WebSocket 握手期间会发送 cookie。基于会话的认证可以在这里工作。优先使用 HTTP-only cookie。
跨源
Cookie 在跨源时不起作用。在 URL 查询中传递凭据,然后在服务器上验证。
使用示例
静态认证
JavaScript
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.
}
Explain Code
TypeScript
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.
}
Explain Code
异步认证
在连接前构建查询值。使用 Suspense 处理异步设置。
JavaScript
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>
);
}
Explain Code
TypeScript
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>
);
}
Explain Code
JWT 刷新模式
当连接因认证错误而失败时刷新 token。
JavaScript
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;
}
Explain Code
TypeScript
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;
}
Explain Code
跨域认证
在连接到另一个主机时在 URL 中传递凭据,然后在服务器上验证。
静态跨域认证
JavaScript
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.
}
Explain Code
TypeScript
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.
}
Explain Code
异步跨域认证
JavaScript
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.
}
Explain Code
TypeScript
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.
}
Explain Code
服务器端验证
在服务器端,在 onConnect handler 中验证 token:
JavaScript
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;
// ...
}
}
Explain Code
TypeScript
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;
// ...
}
}
Explain Code
最佳实践
- 使用短期 token - URL 中的 token 可能被记录。保持过期时间短(分钟,而不是小时)。
- 适当限定 token 范围 - 在 token 声明中包含 agent 名称或实例,以防止 token 在 agent 之间被重用。
- 每次连接都验证 - 始终在
onConnect中验证 token,而不仅仅是一次。 - 使用 HTTPS - 在生产中始终使用安全的 WebSocket 连接(
wss://)。 - 轮换 secret - 定期轮换你的 JWT 签名密钥或 token secret。
- 记录认证失败 - 跟踪失败的认证尝试以进行安全监控。
下一步
Routing 路由和认证 hook。
WebSockets 实时双向通信。
Agents API Agents SDK 的完整 API 参考。