GraphQL - 客户端认证
GraphQL - 身份验证
Section titled “GraphQL - 身份验证”身份验证(Authentication)是验证用户身份的过程。保护你的 GraphQL API 通常涉及确保只有经过身份验证(有时也经过授权)的用户才能执行特定操作或访问特定数据。
GraphQL 本身并不规定特定的身份验证机制。身份验证通常在 GraphQL 之外处理,通常使用标准的 Web 技术,如令牌(Tokens)(JWT、OAuth)、会话(sessions)或 API 密钥。然后,经过身份验证的用户身份通过上下文对象(context object)提供给你的 GraphQL 解析器(resolvers)。
常见方法:JWT 身份验证
Section titled “常见方法:JWT 身份验证”一种流行的方法是使用 JSON Web Tokens (JWT)。流程通常如下:
- 登录: 用户向传统的 REST 端点(例如,
/login)或专用的 GraphQL mutation 提供凭据(例如,电子邮件/密码)。 - 令牌发放: 如果凭据有效,服务器会生成一个包含用户信息(如用户 ID、角色)的 JWT,并使用一个密钥进行签名。
- 令牌存储: 服务器将 JWT 发送回客户端,客户端将其安全地存储(例如,在
localStorage、sessionStorage或内存中)。 - 认证请求: 对于后续对 GraphQL 端点的请求,客户端在 HTTP 头部(通常是
Authorization: Bearer <token>)中包含 JWT。 - 令牌验证: 服务器上的一个中间件层拦截传入请求,从头部提取令牌,使用密钥验证其签名,并解码用户信息。
- 上下文填充: 经过验证的用户信息被添加到该请求的 GraphQL
context对象中。 - 解析器访问: 解析器可以从
context访问用户信息,以执行身份验证检查或获取用户特定的数据。
示例:使用 Apollo Server & Express 进行 JWT 身份验证
Section titled “示例:使用 Apollo Server & Express 进行 JWT 身份验证”我们将构建一个包含受保护查询和 /login 端点的服务器,然后展示客户端如何进行身份验证。
步骤 1:项目设置与依赖项
Section titled “步骤 1:项目设置与依赖项”创建一个项目(auth-server-app),初始化 npm,并安装依赖项:
mkdir auth-server-appcd auth-server-appnpm init -ynpm install @apollo/server graphql express cors body-parser jsonwebtoken# Optional: npm install --save-dev nodemon 可选:安装 nodemon 作为开发依赖我们添加了 jsonwebtoken 用于创建和验证 JWT。
步骤 2:模拟数据(可选)
Section titled “步骤 2:模拟数据(可选)”创建一个简单的模拟用户存储(例如,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 中)”定义一个需要身份验证的查询:
#graphqltype 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.jsimport { 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 defaultconst 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。
步骤 6:运行服务器
Section titled “步骤 6:运行服务器”在 package.json 中添加启动脚本,并运行 npm run dev 或 npm start。
测试身份验证
Section titled “测试身份验证”-
尝试在没有令牌的情况下访问受保护查询: 打开 Apollo Sandbox(
http://localhost:9000/graphql),运行query { me { id email } }。你应该会收到“User is not authenticated”(用户未认证)错误。 -
登录: 使用
curl、Postman 或 Insomnia 等工具向http://localhost:9000/login发送一个 POST 请求,请求体为 JSON:
{ "email": "test@example.com", "password": "password123"}你应该会收到类似 {"token": "eyJhbGciOi..."} 的响应。复制令牌值。
- 尝试使用令牌访问受保护查询: 返回 Apollo Sandbox。找到 ‘Headers’ 选项卡或区域。添加一个新的头部:
- 头部名称:
Authorization - 头部值:
Bearer <paste_your_token_here>(将<paste_your_token_here>替换为实际的令牌)
现在,再次运行 query { me { id email } }。这次应该会成功并返回用户数据。
1. 基本 Fetch / jQuery 客户端
Section titled “1. 基本 Fetch / jQuery 客户端”在一个简单的 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,则使用专用的useMutationhook)调用/login端点。 - 成功登录后,将令牌存储在
localStorage.setItem('authToken', receivedToken);中。 - 登录/登出后,使用
client.resetStore()或client.refetchQueries()清除缓存并使用新的认证状态重新获取数据。 - 登出时,移除令牌
localStorage.removeItem('authToken');并重置 store。
配置了 authLink 后,Apollo Client 将自动在所有后续 GraphQL 操作的头部中包含令牌。
安全注意事项
Section titled “安全注意事项”- HTTPS: 在生产环境中始终使用 HTTPS。
- 密钥: 安全地存储 JWT 密钥(环境变量、密钥管理)。
- 密码哈希: 切勿存储明文密码。使用强大的哈希算法(例如,bcrypt)。
- 令牌过期: 为 JWT 设置合理的过期时间。
- 令牌存储: 注意在客户端存储令牌的位置(
localStorage很常见但容易受到 XSS 攻击;对于 Web 应用,HttpOnly cookies 通常更安全,如果可行)。 - 授权: 身份验证验证用户是谁;授权决定他们被允许做什么。在解析器中根据用户角色或权限(通常存储在令牌中或根据用户 ID 获取)实现授权检查。