Skip to content

GraphQL - 客户端认证

身份验证(Authentication)是验证用户身份的过程。保护你的 GraphQL API 通常涉及确保只有经过身份验证(有时也经过授权)的用户才能执行特定操作或访问特定数据。

GraphQL 本身并不规定特定的身份验证机制。身份验证通常在 GraphQL 之外处理,通常使用标准的 Web 技术,如令牌(Tokens)(JWT、OAuth)、会话(sessions)或 API 密钥。然后,经过身份验证的用户身份通过上下文对象(context object)提供给你的 GraphQL 解析器(resolvers)。

一种流行的方法是使用 JSON Web Tokens (JWT)。流程通常如下:

  1. 登录: 用户向传统的 REST 端点(例如,/login)或专用的 GraphQL mutation 提供凭据(例如,电子邮件/密码)。
  2. 令牌发放: 如果凭据有效,服务器会生成一个包含用户信息(如用户 ID、角色)的 JWT,并使用一个密钥进行签名。
  3. 令牌存储: 服务器将 JWT 发送回客户端,客户端将其安全地存储(例如,在 localStorage、sessionStorage 或内存中)。
  4. 认证请求: 对于后续对 GraphQL 端点的请求,客户端在 HTTP 头部(通常是 Authorization: Bearer <token>)中包含 JWT。
  5. 令牌验证: 服务器上的一个中间件层拦截传入请求,从头部提取令牌,使用密钥验证其签名,并解码用户信息。
  6. 上下文填充: 经过验证的用户信息被添加到该请求的 GraphQL context 对象中。
  7. 解析器访问: 解析器可以从 context 访问用户信息,以执行身份验证检查或获取用户特定的数据。

示例:使用 Apollo Server & Express 进行 JWT 身份验证

Section titled “示例:使用 Apollo Server & Express 进行 JWT 身份验证”

我们将构建一个包含受保护查询和 /login 端点的服务器,然后展示客户端如何进行身份验证。

创建一个项目(auth-server-app),初始化 npm,并安装依赖项:

mkdir auth-server-app
cd auth-server-app
npm init -y
npm install @apollo/server graphql express cors body-parser jsonwebtoken
# Optional: npm install --save-dev nodemon 可选:安装 nodemon 作为开发依赖

我们添加了 jsonwebtoken 用于创建和验证 JWT。

创建一个简单的模拟用户存储(例如,db.js)。在实际应用中,这将是数据库。

// db.js (Mock Data) db.js (模拟数据)
const users = [
{
id: 'user-1',
email: 'test@example.com',
password: 'password123', // In real apps, store hashed passwords! 在实际应用中,存储哈希密码!
firstName: 'Test',
lastName: 'User'
}
];
export const findUserByEmail = (email) => users.find(user => user.email === email);
export const findUserById = (id) => users.find(user => user.id === id);
// Or use module.exports if not using ES Modules 如果不使用 ES 模块,则使用 module.exports

步骤 3:定义 Schema(schema.graphql 或在 server.js 中)

Section titled “步骤 3:定义 Schema(schema.graphql 或在 server.js 中)”

定义一个需要身份验证的查询:

#graphql
type User {
id: ID!
email: String!
firstName: String
}
type Query {
# This query requires authentication 这个查询需要身份验证
me: User
# Public query (optional) 公开查询(可选)
publicGreeting: String
}

步骤 4:创建 Resolvers(resolvers.js)

Section titled “步骤 4:创建 Resolvers(resolvers.js)”

me 解析器将检查上下文是否存在经过身份验证的用户。

// resolvers.js
import { GraphQLError } from 'graphql';
import { findUserById } from './db.js'; // Assuming db.js uses export 假设 db.js 使用 export
const resolvers = {
Query: {
// Resolver for the protected 'me' query 受保护的 'me' 查询的解析器
me: (parent, args, context, info) => {
// Check if user information exists in the context 检查上下文中是否存在用户信息
if (!context.user) {
// Throw an authentication error if not logged in 如果未登录,则抛出身份验证错误
throw new GraphQLError('User is not authenticated', {
extensions: { code: 'UNAUTHENTICATED' },
});
}
// Return the user data from context 从上下文中返回用户数据
return context.user;
},
publicGreeting: () => 'Hello from the public API!', // 来自公开 API 的问候!
},
};
export default resolvers; // Or module.exports 或者使用 module.exports

步骤 5:设置服务器、中间件和登录路由(server.js)

Section titled “步骤 5:设置服务器、中间件和登录路由(server.js)”

配置 Apollo Server,添加中间件来验证 JWT 并填充上下文,并创建 /login 端点。

import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import express from 'express';
import http from 'http';
import cors from 'cors';
import bodyParser from 'body-parser';
import jwt from 'jsonwebtoken';
import { readFileSync } from 'fs';
// Mock DB functions (assuming db.js uses export) 模拟数据库函数(假设 db.js 使用 export)
import { findUserByEmail, findUserById } from './db.js';
// import resolvers from './resolvers.js'; // If using export default 如果使用 export default
const resolvers = require('./resolvers'); // If using module.exports 如果使用 module.exports
const typeDefs = readFileSync('./schema.graphql', { encoding: 'utf-8' });
// Secret key for signing JWTs (store securely in env variables in real apps!) 用于签名 JWT 的密钥(在实际应用中安全地存储在环境变量中!)
const JWT_SECRET = 'your-very-secret-key-change-me';
const app = express();
const httpServer = http.createServer(app);
// Middleware to parse JSON and enable CORS 解析 JSON 和启用 CORS 的中间件
app.use(cors());
app.use(bodyParser.json());
// --- Authentication Middleware --- --- 身份验证中间件 ---
// This middleware runs before the GraphQL middleware 此中间件在 GraphQL 中间件之前运行
app.use((req, res, next) => {
const authHeader = req.headers.authorization || '';
const token = authHeader.startsWith('Bearer ') ? authHeader.substring(7) : null;
if (token) {
try {
// Verify the token 验证令牌
const decoded = jwt.verify(token, JWT_SECRET);
// Add decoded user ID to the request object (to be used in context) 将解码后的用户 ID 添加到请求对象(将在上下文中使用)
req.userId = decoded.sub; // 'sub' is standard JWT claim for subject (user ID) 'sub' 是 JWT 标准声明,代表主体(用户 ID)
} catch (err) {
console.warn('Invalid token:', err.message); // 无效令牌
// Don't throw error here, let GraphQL resolver handle unauthenticated state 不在这里抛出错误,让 GraphQL 解析器处理未认证状态
}
}
next(); // Proceed to the next middleware (GraphQL) 继续到下一个中间件(GraphQL)
});
// --- End Authentication Middleware --- --- 身份验证中间件结束 ---
// Apollo Server setup Apollo Server 设置
const server = new ApolloServer({
typeDefs,
resolvers,
});
await server.start();
// Apply GraphQL middleware, passing authenticated user to context 应用 GraphQL 中间件,将认证用户传递到上下文
app.use(
'/graphql',
expressMiddleware(server, {
context: async ({ req }) => {
// If middleware added userId, fetch user details 如果中间件添加了 userId,则获取用户详情
const userId = req.userId;
if (userId) {
// Fetch user from DB based on ID from token 根据令牌中的 ID 从数据库获取用户
const user = findUserById(userId);
return { user }; // Add user object to context 将用户对象添加到上下文
}
return {}; // Return empty context if no user ID 如果没有用户 ID,则返回空上下文
},
}),
);
// --- Login Endpoint --- --- 登录端点 ---
app.post('/login', (req, res) => {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).send({ error: 'Email and password required' }); // 需要电子邮件和密码
}
const user = findUserByEmail(email);
// IMPORTANT: In real apps, compare hashed passwords! 重要:在实际应用中,比较哈希密码!
if (!user || user.password !== password) {
return res.status(401).send({ error: 'Invalid credentials' }); // 无效凭据
}
// Credentials valid: Generate JWT 凭据有效:生成 JWT
const token = jwt.sign(
{ sub: user.id, email: user.email }, // Payload: standard 'sub' claim for user ID 载荷:用于用户 ID 的标准 'sub' 声明
JWT_SECRET,
{ expiresIn: '1h' } // Token expires in 1 hour 令牌在 1 小时后过期
);
// Send token back to client 将令牌发送回客户端
res.send({ token });
});
// --- End Login Endpoint --- --- 登录端点结束 ---
const PORT = 9000;
await new Promise((resolve) => httpServer.listen({ port: PORT }, resolve));
console.log(`🚀 Server ready at http://localhost:${PORT}`); // 服务器已就绪
console.log(`🔓 Login endpoint at POST http://localhost:${PORT}/login`); // 登录端点
console.log(`🧠 GraphQL endpoint at http://localhost:${PORT}/graphql`); // GraphQL 端点

关键点:

  • 定义了一个 JWT_SECRET(在生产环境中使用环境变量!)。
  • 一个 Express 中间件提取 Authorization: Bearer <token> 头部。
  • 它使用 jwt.verify 验证令牌,并将令牌载荷(payload)中的 userId(sub 声明)附加到 req 对象上。
  • expressMiddleware 的 context 函数检查 req.userId。如果存在,它会获取完整的用户详情(此处为模拟)并将 user 对象添加到 GraphQL 上下文。
  • /login 端点验证凭据(此处使用明文密码 - 在生产环境中切勿这样做,请使用哈希加密!),并使用 jwt.sign 签发包含用户 ID(sub)的 JWT。

在 package.json 中添加启动脚本,并运行 npm run dev 或 npm start。

  1. 尝试在没有令牌的情况下访问受保护查询: 打开 Apollo Sandbox(http://localhost:9000/graphql),运行 query { me { id email } }。你应该会收到“User is not authenticated”(用户未认证)错误。

  2. 登录: 使用 curl、Postman 或 Insomnia 等工具向 http://localhost:9000/login 发送一个 POST 请求,请求体为 JSON:

{
"email": "test@example.com",
"password": "password123"
}

你应该会收到类似 {"token": "eyJhbGciOi..."} 的响应。复制令牌值。

  1. 尝试使用令牌访问受保护查询: 返回 Apollo Sandbox。找到 ‘Headers’ 选项卡或区域。添加一个新的头部:
  • 头部名称:Authorization
  • 头部值:Bearer <paste_your_token_here>(将 <paste_your_token_here> 替换为实际的令牌)

现在,再次运行 query { me { id email } }。这次应该会成功并返回用户数据。

在一个简单的 HTML/JS 客户端中(例如在 jQuery 集成章节中的那个):

  • 登录: 添加电子邮件/密码的表单字段。提交时,使用 fetch 将凭据 POST 到 /login 端点。
  • 存储令牌: 成功登录后,存储收到的令牌(例如,在 localStorage 或一个变量中)。
  • 发送令牌: 使用 fetch 对 /graphql 发送 GraphQL 请求时,在 headers 选项中包含 Authorization: Bearer <token> 头部。
// Example: Fetching protected data after login 示例:登录后获取受保护的数据
const graphqlQuery = { query: '{ me { id firstName } }' };
const storedToken = localStorage.getItem('authToken'); // Get token from storage 从存储中获取令牌
fetch('http://localhost:9000/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// Add the Authorization header if token exists 如果令牌存在,则添加 Authorization 头部
...(storedToken && { 'Authorization': `Bearer ${storedToken}` })
},
body: JSON.stringify(graphqlQuery)
})
.then(response => response.json())
.then(data => {
if (data.errors) {
// Handle GraphQL errors (e.g., UNAUTHENTICATED) 处理 GraphQL 错误(例如,UNAUTHENTICATED)
console.error('GraphQL Error:', data.errors); // GraphQL 错误
if (data.errors.some(err => err.extensions?.code === 'UNAUTHENTICATED')){
alert('Please log in again.'); // 请重新登录。
// Redirect to login or clear token 重定向到登录页或清除令牌
localStorage.removeItem('authToken');
}
} else {
// Process successful data 处理成功数据
console.log('User data:', data.data.me); // 用户数据
// Update UI 更新 UI
}
})
.catch(error => {
// Handle network errors 处理网络错误
console.error('Network Error:', error); // 网络错误
});

2. 使用 Apollo Client 的 React 客户端

Section titled “2. 使用 Apollo Client 的 React 客户端”

Apollo Client 提供了使用 Apollo Link 中间件的更健壮的方法。

a) 安装 Link 库: npm install @apollo/client @apollo/link-context graphql(如果尚未安装)

b) 配置 Apollo Client 与 Auth Link:

// In your Apollo Client setup file (e.g., main.jsx or a dedicated apollo.js) 在你的 Apollo Client 设置文件(例如,main.jsx 或一个专门的 apollo.js 文件)中
import {
ApolloClient,
InMemoryCache,
ApolloProvider,
createHttpLink // Use createHttpLink instead of just uri 使用 createHttpLink 而非仅使用 uri
} from '@apollo/client';
import { setContext } from '@apollo/link-context'; // Import setContext 导入 setContext
// Standard HTTP link 标准 HTTP 链接
const httpLink = createHttpLink({
uri: 'http://localhost:9000/graphql',
});
// Middleware link to set the Authorization header 设置 Authorization 头部信息的中间件链接
const authLink = setContext((_, { headers }) => {
// Get the authentication token from local storage if it exists 如果存在,则从 local storage 获取身份验证令牌
const token = localStorage.getItem('authToken');
// Return the headers to the context so httpLink can read them 将头部信息返回到上下文,以便 httpLink 可以读取它们
return {
headers: {
...headers, // Spread existing headers 展开现有头部
authorization: token ? `Bearer ${token}` : "", // Add Authorization header 添加 Authorization 头部
}
}
});
// Create the Apollo Client instance using the link chain 使用链接链创建 Apollo Client 实例
const client = new ApolloClient({
link: authLink.concat(httpLink), // Chain authLink and httpLink 连接 authLink 和 httpLink
cache: new InMemoryCache(),
});
// ... rest of your setup (e.g., wrap App in ApolloProvider) ...其余设置(例如,用 ApolloProvider 包装 App)

c) 登录/登出逻辑:

  • 创建一个登录组件,使用 fetch(或者如果你创建了一个登录 mutation,则使用专用的 useMutation hook)调用 /login 端点。
  • 成功登录后,将令牌存储在 localStorage.setItem('authToken', receivedToken); 中。
  • 登录/登出后,使用 client.resetStore() 或 client.refetchQueries() 清除缓存并使用新的认证状态重新获取数据。
  • 登出时,移除令牌 localStorage.removeItem('authToken'); 并重置 store。

配置了 authLink 后,Apollo Client 将自动在所有后续 GraphQL 操作的头部中包含令牌。

  • HTTPS: 在生产环境中始终使用 HTTPS。
  • 密钥: 安全地存储 JWT 密钥(环境变量、密钥管理)。
  • 密码哈希: 切勿存储明文密码。使用强大的哈希算法(例如,bcrypt)。
  • 令牌过期: 为 JWT 设置合理的过期时间。
  • 令牌存储: 注意在客户端存储令牌的位置(localStorage 很常见但容易受到 XSS 攻击;对于 Web 应用,HttpOnly cookies 通常更安全,如果可行)。
  • 授权: 身份验证验证用户是谁;授权决定他们被允许做什么。在解析器中根据用户角色或权限(通常存储在令牌中或根据用户 ID 获取)实现授权检查。