JWT在GraphQL API中的集成
1. 理解 GraphQL 和 JWT
1.1 GraphQL 基础
GraphQL 是一种用于 API 的查询语言,同时也是一个满足你数据查询需求的运行时。与传统的 REST API 不同,GraphQL 允许客户端精确地指定它需要的数据,避免了过度获取(over - fetching)和获取不足(under - fetching)数据的问题。
例如,假设我们有一个博客 API,在 REST 架构下,获取一篇文章及其作者信息可能需要向 /articles/{id}
和 /users/{authorId}
分别发送请求。而在 GraphQL 中,我们可以通过一个查询一次性获取所需数据:
query {
article(id: 1) {
title
content
author {
name
email
}
}
}
GraphQL 服务端会解析这个查询,并返回符合查询结构的数据。
1.2 JWT 基础
JSON Web Token(JWT)是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间以 JSON 对象的形式安全地传输信息。JWT 通常由三部分组成:头部(Header)、载荷(Payload)和签名(Signature)。
- 头部:通常包含两部分信息,令牌的类型(即 JWT)和使用的哈希算法,例如
{ "alg": "HS256", "typ": "JWT" }
。然后将这个 JSON 对象进行 Base64Url 编码,形成 JWT 的第一部分。 - 载荷:这是存放实际需要传递数据的地方。载荷中可以包含一些标准字段,如
iss
(签发者)、exp
(过期时间)等,也可以包含自定义字段。例如{ "user_id": 1, "username": "john_doe", "exp": 1619314200 }
。同样,将这个 JSON 对象进行 Base64Url 编码,形成 JWT 的第二部分。 - 签名:为了创建签名部分,需要使用编码后的头部、编码后的载荷、一个密钥(secret)和头部中指定的签名算法。例如,如果使用 HMAC SHA256 算法,签名将按如下方式创建:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
。签名用于验证消息在传输过程中没有被更改,并且对于使用私钥签名的令牌,还可以验证 JWT 的发送者的身份。
一个完整的 JWT 看起来像这样:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImpvaG5fZG9lIiwiZXhwIjoxNjE5MzE0MjAwfQ.1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
2. 在 GraphQL API 中集成 JWT 的必要性
2.1 认证与授权
在 GraphQL API 中,认证(Authentication)用于确认请求的发起者是谁,而授权(Authorization)用于确定发起者是否有权限执行请求的操作。JWT 可以有效地解决这两个问题。通过在请求头中携带 JWT,服务端可以验证用户身份,并从 JWT 的载荷中获取用户的相关信息,如用户 ID、角色等,从而进行授权决策。
例如,一个 GraphQL API 可能有不同的查询和变更操作,某些操作可能只允许管理员角色的用户执行。通过验证 JWT 并检查其中的角色信息,服务端可以确保只有授权用户能够执行特定操作。
2.2 无状态性
GraphQL API 通常设计为无状态的,这意味着每个请求都是独立的,不依赖于之前的请求状态。JWT 完美地契合了这种无状态性。服务端在处理每个请求时,只需验证 JWT 的有效性,而无需在服务器端存储额外的会话状态。这不仅简化了服务器的实现,还使得系统更容易进行水平扩展。
2.3 跨域与分布式系统
在跨域或分布式系统环境下,JWT 可以在不同的服务之间轻松传递用户身份信息。例如,一个 GraphQL API 可能与多个微服务交互,通过传递 JWT,各个微服务可以共享用户认证和授权信息,而无需进行额外的复杂身份验证流程。
3. 集成步骤
3.1 生成 JWT
在后端应用中,首先需要有生成 JWT 的功能。以下以 Node.js 和 Express 为例,使用 jsonwebtoken
库来生成 JWT:
- 安装依赖:
npm install jsonwebtoken
- 生成 JWT 的代码示例:
const jwt = require('jsonwebtoken');
// 假设这是用户登录成功后获取到的用户信息
const user = {
id: 1,
username: 'john_doe',
role: 'user'
};
const secret = 'your - secret - key';
const token = jwt.sign(user, secret, { expiresIn: '1h' });
console.log(token);
在上述代码中,jwt.sign
方法接收三个参数:要编码到 JWT 中的数据(这里是 user
对象)、用于签名的密钥(secret
)以及一些选项,如 expiresIn
设置令牌的过期时间为 1 小时。
3.2 在 GraphQL 请求中传递 JWT
客户端在向 GraphQL API 发送请求时,需要将 JWT 包含在请求头中。通常,使用 Authorization
头来传递 JWT,格式为 Bearer <token>
。例如,在使用 fetch
进行 GraphQL 查询时:
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImpvaG5fZG9lIiwiZXhwIjoxNjE5MzE0MjAwfQ.1234567890abcdef1234567890abcdef1234567890abcdef';
const query = `
query {
article(id: 1) {
title
content
}
}
`;
fetch('/graphql', {
method: 'POST',
headers: {
'Content - Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ query })
})
.then(response => response.json())
.then(data => console.log(data));
3.3 在 GraphQL 服务端验证 JWT
在 GraphQL 服务端,需要对接收到的请求中的 JWT 进行验证。以下以 Node.js 和 Apollo Server 为例:
- 安装依赖:
npm install jsonwebtoken apollo - server - express express
- 配置 Apollo Server 并验证 JWT 的代码示例:
const express = require('express');
const { ApolloServer } = require('apollo - server - express');
const jwt = require('jsonwebtoken');
// 定义 GraphQL 类型和解析器
const typeDefs = `
type Query {
article(id: ID!): Article
}
type Article {
title: String
content: String
}
`;
const resolvers = {
Query: {
article: (parent, { id }) => {
// 这里假设从数据库获取文章,实际应用中需要连接数据库
return {
title: 'Sample Article',
content: 'This is a sample article content'
};
}
}
};
const app = express();
const secret = 'your - secret - key';
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (token) {
try {
const user = jwt.verify(token, secret);
return { user };
} catch (error) {
console.error('JWT verification error:', error);
}
}
return {};
}
});
server.applyMiddleware({ app });
const port = 4000;
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
在上述代码中,context
函数用于将经过验证的用户信息(如果 JWT 验证成功)传递给解析器。req.headers.authorization
获取请求头中的 Authorization
字段,然后提取出 JWT 并进行验证。如果验证成功,将用户信息放入 context
中,解析器可以通过 context.user
访问用户信息,从而进行授权决策。
3.4 基于 JWT 的授权
在解析器中,可以根据 JWT 中包含的用户信息进行授权。例如,假设只有管理员角色的用户可以创建文章,修改后的解析器代码如下:
const resolvers = {
Query: {
article: (parent, { id }) => {
// 这里假设从数据库获取文章,实际应用中需要连接数据库
return {
title: 'Sample Article',
content: 'This is a sample article content'
};
}
},
Mutation: {
createArticle: (parent, { title, content }, { user }) => {
if (!user || user.role!== 'admin') {
throw new Error('Unauthorized');
}
// 这里假设将文章保存到数据库,实际应用中需要连接数据库
return {
title,
content
};
}
}
};
在 createArticle
解析器中,首先检查 context
中的 user
是否存在以及用户角色是否为 admin
。如果不满足条件,则抛出 Unauthorized
错误,阻止未授权用户执行该操作。
4. 处理 JWT 过期和刷新
4.1 JWT 过期处理
当 JWT 过期时,服务端验证 JWT 会失败。在 GraphQL 服务端,可以统一处理这种情况,返回适当的错误信息给客户端。例如,在验证 JWT 的 context
函数中添加过期处理:
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (token) {
try {
const user = jwt.verify(token, secret);
return { user };
} catch (error) {
if (error.name === 'TokenExpiredError') {
throw new Error('Token has expired');
}
console.error('JWT verification error:', error);
}
}
return {};
}
});
这样,当 JWT 过期时,客户端会收到 Token has expired
的错误信息,提示用户需要重新登录或刷新令牌。
4.2 JWT 刷新
为了避免用户频繁重新登录,可以实现 JWT 刷新机制。通常,会使用一个刷新令牌(Refresh Token)。当 JWT 过期时,客户端使用刷新令牌向服务端请求一个新的 JWT。
- 生成刷新令牌:与生成 JWT 类似,不过刷新令牌通常有效期更长。
const refreshToken = jwt.sign({ userId: user.id }, secret, { expiresIn: '7d' });
- 存储刷新令牌:客户端需要安全地存储刷新令牌,例如在 HTTP 仅有的 Cookie 中。
- 刷新 JWT:在服务端,创建一个 GraphQL 变更操作来处理刷新 JWT 的请求。
const typeDefs = `
type Mutation {
refreshToken: Token
}
type Token {
accessToken: String
refreshToken: String
}
`;
const resolvers = {
Mutation: {
refreshToken: (parent, args, { req }) => {
const refreshToken = req.headers['x - refresh - token'];
if (!refreshToken) {
throw new Error('Refresh token is missing');
}
try {
const decoded = jwt.verify(refreshToken, secret);
const newAccessToken = jwt.sign({ userId: decoded.userId }, secret, { expiresIn: '1h' });
const newRefreshToken = jwt.sign({ userId: decoded.userId }, secret, { expiresIn: '7d' });
return {
accessToken: newAccessToken,
refreshToken: newRefreshToken
};
} catch (error) {
throw new Error('Invalid refresh token');
}
}
}
};
在上述代码中,refreshToken
变更操作接收客户端发送的刷新令牌(假设在 x - refresh - token
头中),验证刷新令牌的有效性。如果有效,生成新的访问令牌(JWT)和新的刷新令牌并返回给客户端。
5. 安全注意事项
5.1 密钥管理
用于签名 JWT 的密钥必须妥善保管。密钥泄露会导致恶意用户伪造 JWT,从而获得未授权的访问权限。密钥应该足够复杂,并且在生产环境中,建议使用环境变量来存储密钥,而不是硬编码在代码中。例如,在 Node.js 中可以使用 dotenv
库来加载环境变量:
npm install dotenv
require('dotenv').config();
const secret = process.env.JWT_SECRET;
5.2 防止 JWT 劫持
为了防止 JWT 被劫持,建议在传输过程中使用 HTTPS 协议,确保数据在网络传输中是加密的。此外,客户端存储 JWT 时应尽量采用安全的存储方式,如 HTTP 仅有的 Cookie,避免将 JWT 暴露在客户端脚本中,防止 XSS 攻击窃取 JWT。
5.3 验证 JWT 来源
服务端在验证 JWT 时,除了验证签名和过期时间,还应考虑验证 JWT 的来源。例如,可以检查 JWT 是否来自预期的客户端应用,防止恶意用户从其他非授权来源获取并使用 JWT。
5.4 限制 JWT 载荷数据
JWT 的载荷虽然方便携带用户信息,但应避免在载荷中包含敏感信息,如密码等。因为 JWT 的载荷是 Base64Url 编码的,很容易被解码查看。只在载荷中包含必要的用户标识和授权相关信息。
6. 集成 JWT 到不同语言和框架的案例
6.1 Python 和 Django
- 安装依赖:
pip install djangorestframework django - jwt
- 配置 Django 项目以使用 JWT 认证:
在
settings.py
中添加:
INSTALLED_APPS = [
...
'rest_framework',
'rest_framework_simplejwt'
]
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',
)
}
- 生成和验证 JWT: Django REST framework SimpleJWT 提供了生成和验证 JWT 的工具。例如,在视图中验证 JWT:
from rest_framework_simplejwt.authentication import JWTAuthentication
from rest_framework.views import APIView
from rest_framework.response import Response
class MyGraphQLView(APIView):
authentication_classes = [JWTAuthentication]
def post(self, request):
# 这里处理 GraphQL 请求,假设已经配置好 GraphQL 相关逻辑
return Response({'message': 'GraphQL request processed'})
6.2 Java 和 Spring Boot
- 引入依赖:
在
pom.xml
中添加:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt - api</artifactId>
<version>0.11.2</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt - impl</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt - javax</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
- 生成和验证 JWT:
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
import java.util.Date;
public class JwtUtil {
private static final Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
private static final long EXPIRATION_TIME = 3600000; // 1 hour
public static String generateToken(String subject) {
Date now = new Date();
Date expiration = new Date(now.getTime() + EXPIRATION_TIME);
Claims claims = Jwts.claims().setSubject(subject);
claims.put("issuedAt", now);
claims.put("expiration", expiration);
return Jwts.builder()
.setClaims(claims)
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}
public static boolean validateToken(String token) {
try {
Claims claims = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
Date expiration = claims.get("expiration", Date.class);
Date now = new Date();
return expiration.after(now);
} catch (Exception e) {
return false;
}
}
}
在 Spring Boot 的控制器中验证 JWT 并处理 GraphQL 请求:
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class GraphQLController {
@PostMapping("/graphql")
public ResponseEntity<String> handleGraphQLRequest(@RequestHeader("Authorization") String authorizationHeader) {
String token = authorizationHeader.replace("Bearer ", "");
if (JwtUtil.validateToken(token)) {
// 处理 GraphQL 请求
return new ResponseEntity<>("GraphQL request processed", HttpStatus.OK);
} else {
return new ResponseEntity<>("Unauthorized", HttpStatus.UNAUTHORIZED);
}
}
}
通过以上详细步骤和案例,你可以在不同的后端技术栈中成功地将 JWT 集成到 GraphQL API 中,实现安全可靠的认证和授权机制。在实际应用中,还需要根据具体业务需求进一步优化和完善相关功能。