第一章:JWT 概述
1.1 什么是 JWT
| 概念名称 | 说明 | 注意事项 |
|---|
| JWT (JSON Web Token) | 一种开放标准(RFC 7519),用于在网络应用环境间安全地传递声明(claims)。它将用户信息和元数据编码为一个紧凑的、自包含的字符串格式,通常用于身份验证和信息交换。 | JWT 本身不加密,仅签名确保完整性;如需保密,应使用 JWE(JSON Web Encryption)。 |
| 自包含性 | JWT 包含了所有必要的信息,如用户身份、权限、过期时间等,接收方无需查询数据库即可验证和使用。 | 载荷不宜过大,避免影响传输效率。 |
| 紧凑性 | 使用 Base64Url 编码,生成的令牌短小,适合在 URL、HTTP 头或 POST 参数中传输。 | 不宜在令牌中存储敏感信息(如密码),即使签名也无法防止信息泄露。 |
| 签名机制 | 通过签名确保令牌未被篡改,可使用 HMAC 或 RSA 等算法。 | 私钥必须严格保密,否则签名可被伪造。 |
1.2 JWT 的应用场景
| 场景名称 | 说明 | 注意事项 |
|---|
| 用户身份认证 | 用户登录后,服务器生成 JWT 返回客户端,后续请求携带该令牌进行身份识别。 | 应设置合理的过期时间,避免长期有效带来的安全风险。 |
| 单点登录(SSO) | 多个系统共享同一认证服务,通过 JWT 实现跨域身份传递。 | 需统一密钥管理或使用公私钥机制保证安全性。 |
| API 权限控制 | 在微服务架构中,JWT 可携带用户角色、权限等信息,供各服务进行授权判断。 | 权限变更后旧令牌仍有效,需结合黑名单或短过期时间处理。 |
| 信息交换 | 安全地在各方之间传输数据,签名可验证数据来源和完整性。 | 仅用于可信系统间通信,避免在公开渠道传输敏感数据。 |
1.3 JWT 与传统 Session 的对比
| 对比项 | JWT | 传统 Session | 注意事项 |
|---|
| 存储位置 | 客户端(如 localStorage、Cookie) | 服务端(内存、Redis 等) | JWT 减轻服务端存储压力,但客户端存储需防范 XSS。 |
| 可扩展性 | 高,无状态,适合分布式系统 | 低,需共享 Session 存储(如 Redis) | JWT 更适合微服务和跨域场景。 |
| 跨域支持 | 原生支持,无需额外配置 | 需配合 CORS、Session 共享等机制 | JWT 在跨域认证中更灵活。 |
| 过期控制 | 依赖令牌内 exp 字段,无法主动失效 | 可主动删除服务端 Session | JWT 需结合黑名单或短过期时间实现主动登出。 |
| 性能开销 | 首次生成略高,验证快 | 每次请求需查服务端存储 | JWT 验证无需查库,性能更优。 |
第二章:JWT 的结构解析
| 方法/参数 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| alg | "alg": "HS256" | 指定签名算法,如 HMAC SHA-256 | {"alg": "HS256", "typ": "JWT"} | 必须指定,常用 HS256(对称)或 RS256(非对称)。 |
| typ | "typ": "JWT" | 指定令牌类型 | {"alg": "HS256", "typ": "JWT"} | 固定为 “JWT”,大小写不敏感。 |
| enc | "enc": "A128GCM" | 加密算法(用于 JWE) | 不适用于标准 JWT | 标准 JWT 不使用此字段。 |
注:Header 经 Base64Url 编码后成为 JWT 第一部分。
2.2 Payload(载荷)
| 声明类型 | 参数名 | 用途 | 示例值 | 注意事项 |
|---|
| 公共声明 | iss (Issuer) | 签发者 | "https://example.com" | 可选,用于标识令牌来源。 |
| 公共声明 | sub (Subject) | 主题(用户标识) | "123456" | 可选,通常为用户 ID。 |
| 公共声明 | aud (Audience) | 接收方 | "https://api.example.com" | 可选,验证时需匹配。 |
| 公共声明 | exp (Expiration Time) | 过期时间(秒级时间戳) | 1730873777 | 强烈建议设置,防止长期有效。 |
| 公共声明 | nbf (Not Before) | 生效时间(秒级时间戳) | 1730870177 | 可选,控制令牌生效时间。 |
| 公共声明 | iat (Issued At) | 签发时间(秒级时间戳) | 1730870177 | 可选,用于判断令牌年龄。 |
| 公共声明 | jti (JWT ID) | 唯一标识 | "abc123" | 可选,用于防止重放攻击。 |
| 私有声明 | 自定义字段 | 业务数据 | {"role": "admin"} | 如 “role”, “email” 等,非标准但常用。 |
注:Payload 经 Base64Url 编码后成为 JWT 第二部分。
2.3 Signature(签名)
| 方法/参数 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| HMAC-SHA256 | HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret ) | 使用密钥生成签名 | 签名值为字节数组的 Base64Url 编码 | 密钥必须保密,泄露将导致签名可伪造。 |
| RSA-SHA256 | RSASHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), privateKey ) | 使用私钥签名 | 验证时使用公钥 | 非对称更安全,适合分布式签发。 |
| secret | 字符串或字节数组 | HMAC 签名密钥 | "my_secret_key" | 长度建议 ≥ 32 字符,避免弱密钥。 |
| privateKey | PEM 格式私钥 | RSA/ECDSA 签名私钥 | -----BEGIN PRIVATE KEY----- ... | 必须安全存储,禁止硬编码。 |
注:Signature 是对前两部分的签名,确保内容未被篡改。
2.4 JWT 完整格式示例
| 组成部分 | 原始内容 | Base64Url 编码后 | 说明 |
|---|
| Header | {"alg": "HS256", "typ": "JWT"} | eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9 | 第一部分 |
| Payload | {"sub": "1234567890", "name": "John Doe", "iat": 1730870177} | eyJzdWIiOiAiMTIzNDU2Nzg5MCIsICJuYW1lIjogIkpvaG4gRG9lIiwgImlhdCI6IDE3MzA4NzAxNzd9 | 第二部分 |
| Signature | HMACSHA256(第一部分 + "." + 第二部分, "secret") | SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c | 第三部分 |
最终 JWT:
eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJzdWIiOiAiMTIzNDU2Nzg5MCIsICJuYW1lIjogIkpvaG4gRG9lIiwgImlhdCI6IDE3MzA4NzAxNzd9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
注意事项:
- 三部分用点号(
.)连接;
- 使用 Base64Url 编码(非 Base64);
- 签名确保完整性;
- 可在 jwt.io 等工具中解码查看内容。
第三章:JWT 的工作流程
3.1 JWT 的生成流程
| 步骤 | 操作 | 说明 | 注意事项 |
|---|
| 1 | 构造 Header | 定义算法(alg)和类型(typ) | 推荐使用 HS256 或 RS256,避免使用 “none” 算法。 |
| 2 | 构造 Payload | 添加标准声明(如 exp、iss)和私有声明(如 user_id、role) | 避免存储敏感信息(如密码),控制载荷大小。 |
| 3 | Base64Url 编码 Header 和 Payload | 将 JSON 对象编码为字符串 | 使用 Base64Url 编码(无填充、+ 和 / 替换为 - 和 _)。 |
| 4 | 生成 Signature | 使用密钥对 encodedHeader.encodedPayload 进行签名 | HMAC 使用共享密钥,RSA 使用私钥签名。 |
| 5 | 拼接三部分 | 格式:encodedHeader.encodedPayload.encodedSignature | 最终令牌为字符串,可通过 HTTP 头(如 Authorization: Bearer <token>)传输。 |
3.2 JWT 的验证流程
| 步骤 | 操作 | 说明 | 注意事项 |
|---|
| 1 | 拆分 JWT | 按点号(.)拆分为三部分:header、payload、signature | 确保格式正确,三部分均存在。 |
| 2 | 解码 Header 和 Payload | Base64Url 解码前两部分 | 可查看内容,但未验证完整性。 |
| 3 | 验证签名算法 | 检查 header 中 alg 字段是否在允许列表中 | 防止算法混淆攻击(如强制使用 “none”)。 |
| 4 | 重新计算签名 | 使用相同算法和密钥对 header.payload 重新签名 | HMAC 使用共享密钥,RSA 使用公钥验证。 |
| 5 | 比对签名 | 将计算出的签名与 JWT 中的 signature 比较 | 必须恒定时间比较(constant-time comparison)防止时序攻击。 |
| 6 | 验证声明 | 检查 exp(未过期)、nbf(已生效)、iss、aud 等 | 服务器时间需同步,避免时钟漂移导致误判。 |
3.3 JWT 的安全性机制
| 机制 | 说明 | 注意事项 |
|---|
| 数字签名 | 确保令牌未被篡改,接收方可验证来源 | 推荐 RS256 等非对称算法提升安全性。 |
| 过期时间(exp) | 限制令牌有效期限 | 设置合理过期时间(如 15-30 分钟),减少泄露风险。 |
| 签发者(iss)和受众(aud) | 明确令牌的签发方和接收方 | 验证时必须比对,防止令牌被用于其他系统。 |
| HTTPS 传输 | 防止令牌在传输过程中被窃听 | 必须使用 HTTPS,避免在 HTTP 中暴露令牌。 |
| 防重放攻击 | 使用 jti 声明配合唯一标识 | 可结合短期黑名单或时间窗口机制检测重复使用。 |
| 密钥管理 | 安全存储和轮换密钥 | 定期更换密钥,避免长期使用同一密钥。 |
第四章:JWT 标准声明(Reserved Claims)
4.1 标准声明字段详解
| 声明 | 全称 | 用途 | 数据类型 | 示例 | 注意事项 |
|---|
| iss | Issuer | 签发者标识 | string | "https://auth.example.com" | 可选,用于验证令牌来源。 |
| sub | Subject | 主题,代表用户或实体 | string | "user123" | 可选,通常为用户唯一 ID。 |
| aud | Audience | 接收方,指定哪些服务可接受此令牌 | string 或 array | "https://api.example.com" | 必须验证,防止令牌被用于未授权服务。 |
| exp | Expiration Time | 过期时间,UTC 时间的秒级时间戳 | number | 1730873777 | 强烈建议设置,过期后令牌无效。 |
| nbf | Not Before | 生效时间,UTC 时间的秒级时间戳 | number | 1730870177 | 可选,控制令牌提前生效。 |
| iat | Issued At | 签发时间,UTC 时间的秒级时间戳 | number | 1730870177 | 可选,用于判断令牌年龄。 |
| jti | JWT ID | 唯一标识符,防止重放攻击 | string | "abc-123-def" | 可选,建议为每个令牌生成唯一 ID。 |
注:这些声明是预定义的,但非强制使用,建议在生产环境中合理使用以增强安全性。
第五章:JWT 的使用场景与最佳实践
5.1 用户身份认证流程
| 步骤 | 客户端操作 | 服务端操作 | 说明 |
|---|
| 1 | 提交用户名密码 | 验证凭证合法性 | 如通过数据库或 LDAP 验证。 |
| 2 | 接收登录请求 | 生成 JWT(含用户 ID、角色、exp 等) | 使用安全算法(如 RS256)签名。 |
| 3 | 存储 JWT | 返回 JWT 给客户端 | 通常通过响应体或 Set-Cookie 返回。 |
| 4 | 携带 JWT 请求 API | 验证 JWT 签名和声明 | 验证 exp、aud、iss 等字段。 |
| 5 | 获取用户信息 | 解析 Payload 获取用户数据 | 无需查询数据库,实现无状态认证。 |
注意事项:
- JWT 应通过
Authorization: Bearer <token> 传递;
- 避免 localStorage 存储以防 XSS;
- 可结合 HttpOnly Cookie 提升安全性。
5.2 Token 刷新机制(Refresh Token)
| 方法/参数 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| Refresh Token | 长期有效的令牌(存储在服务端) | 用于获取新的 Access Token | refreshToken: "xyz789..." | Refresh Token 应存储在服务端(如 Redis),支持主动失效。 |
| Access Token | 短期有效的 JWT | 用于 API 认证 | expiresIn: 900(15分钟) | 过期后客户端使用 Refresh Token 请求新令牌。 |
| /refresh 接口 | POST /refresh { "refreshToken": "..." } | 交换新 Access Token | 响应:{ "accessToken": "...", "expiresIn": 900 } | 需验证 Refresh Token 的有效性与未过期。 |
| blacklist | 令牌黑名单(如 Redis Set) | 标记已注销的 Refresh Token | 存储已失效的 Refresh Token ID | 实现主动登出功能。 |
流程说明:
- 用户登录获取 Access Token 和 Refresh Token;
- Access Token 过期后,客户端用 Refresh Token 请求新令牌;
- 服务端验证 Refresh Token,签发新 Access Token;
- 用户登出时,将 Refresh Token 加入黑名单。
5.3 安全使用建议
| 建议 | 说明 | 注意事项 |
|---|
| 使用 HTTPS | 所有 JWT 传输必须加密 | 防止中间人攻击窃取令牌。 |
| 设置短过期时间 | Access Token 有效期建议 15-30 分钟 | 减少令牌泄露后的危害窗口。 |
| 使用强密钥 | HMAC 密钥长度 ≥ 32 字符,RSA 密钥 ≥ 2048 位 | 避免弱密钥被暴力破解。 |
| 验证所有声明 | 必须验证 exp、aud、iss 等字段 | 防止令牌被滥用或伪造。 |
| 防止 XSS | 避免将 JWT 存储在 localStorage | 推荐使用 HttpOnly Cookie 存储。 |
| 防止 CSRF | 若使用 Cookie 存储,启用 SameSite 属性 | 设置 Cookie: SameSite=Strict 或 Lax。 |
| 定期轮换密钥 | 定期更换签名密钥 | 降低密钥泄露后的风险。 |
| 记录与监控 | 记录异常的令牌使用行为 | 如频繁过期令牌请求,可能为攻击迹象。 |
第六章:JWT 在主流语言中的实现
6.1 JavaScript(Node.js)实现
| 方法 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| jwt.sign() | jwt.sign(payload, secretOrPrivateKey, options) | 生成 JWT | jwt.sign({ id: 123 }, 'secret', { expiresIn: '15m' }) | options 可设置 expiresIn、algorithm 等。 |
| jwt.verify() | jwt.verify(token, secretOrPublicKey, callback) | 验证 JWT 签名和 exp | jwt.verify(token, 'secret', (err, decoded) => { ... }) | 同步调用:jwt.verify(token, secret);注意捕获错误。 |
| jwt.decode() | jwt.decode(token, options) | 仅解码不验证 | jwt.decode(token, { complete: true }) | 仅用于调试,不验证签名完整性。 |
| options.expiresIn | { expiresIn: '2h' } | 设置过期时间 | { expiresIn: '900s' } 或 '15m' | 支持数字(秒)或字符串(如 '2 days')。 |
| options.algorithm | { algorithm: 'RS256' } | 指定签名算法 | { algorithm: 'HS256' } | 默认 HS256,使用 RSA 时需提供密钥文件。 |
依赖库: npm install jsonwebtoken
6.2 Python 实现
| 方法 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| jwt.encode() | jwt.encode(payload, key, algorithm) | 生成 JWT | jwt.encode({"id": 123}, "secret", algorithm="HS256") | 返回字符串;若使用 RS256,key 为私钥字符串。 |
| jwt.decode() | jwt.decode(token, key, algorithms) | 验证并解码 JWT | jwt.decode(token, "secret", algorithms=["HS256"]) | 必须指定 algorithms,否则易受攻击。 |
| jwt.get_unverified_header() | jwt.get_unverified_header(token) | 获取未验证的 header | alg = jwt.get_unverified_header(token)['alg'] | 用于动态选择密钥或算法。 |
| 异常处理 | jwt.ExpiredSignatureError | exp 过期 | try: ... except jwt.ExpiredSignatureError: ... | 常见异常:ExpiredSignatureError, InvalidTokenError, DecodeError。 |
| PyJWT 库 | import jwt | 引入 JWT 模块 | pip install pyjwt | 推荐使用 PyJWT 包。 |
注意:jwt.encode() 在 PyJWT 2.0+ 默认不自动编码为 bytes,需注意字符串类型。
6.3 Java 实现
| 方法 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| Jwts.builder() | Jwts.builder().setSubject("123").signWith(key).compact() | 构建并生成 JWT | Jwts.builder().setExpiration(new Date(System.currentTimeMillis() + 86400000)).signWith(SignatureAlgorithm.HS256, "secret").compact() | 使用链式调用设置声明。 |
| Jwts.parser() | Jwts.parser().setSigningKey(key).parseClaimsJws(token) | 解析并验证 JWT | Jwts.parser().setSigningKey("secret").parseClaimsJws(token) | 验证失败抛出异常(如 ExpiredJwtException)。 |
| setExpiration() | .setExpiration(date) | 设置过期时间 | .setExpiration(new Date(System.currentTimeMillis() + 3600000)) | 需传入 java.util.Date 对象。 |
| setIssuer() | .setIssuer("auth.example.com") | 设置签发者 | .setIssuer(iss) | 可用于后续验证。 |
| parseClaimsJws() | .parseClaimsJws(token).getBody() | 获取 payload 内容 | Claims claims = parser.parseClaimsJws(token).getBody(); | 只有验证通过后才能获取 body。 |
依赖库(Maven):
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
第七章:常见问题与调试
7.1 常见错误码与含义
| 错误类型 | 含义 | 可能原因 | 解决方案 |
|---|
| TokenExpiredError | 令牌已过期 | exp 时间早于当前时间 | 刷新令牌或重新登录。 |
| JsonWebTokenError | 无效令牌格式 | 结构错误、编码错误、缺少部分 | 检查 JWT 字符串是否完整。 |
| NotBeforeError | 令牌尚未生效 | nbf 时间晚于当前时间 | 检查系统时间或等待生效。 |
| InvalidSignatureError | 签名无效 | 密钥不匹配、算法不一致 | 确认密钥和算法配置正确。 |
| AlgorithmNotSupportedError | 算法不支持 | 使用了未启用的 alg(如 none) | 配置允许的算法列表,拒绝 “none”。 |
| MalformedTokenError | 令牌格式错误 | Base64 解码失败、非三段式 | 检查传输过程中是否被截断或篡改。 |
7.2 调试工具推荐
| 工具名称 | 用途 | 使用方式 | 注意事项 |
|---|
| jwt.io | 在线解码和验证 JWT | 粘贴令牌,自动解析 header、payload,验证签名 | 可选择算法和密钥验证签名;切勿输入生产环境密钥。 |
| Postman | 发送带 JWT 的请求 | 在 Authorization 头设置 Bearer Token | 用于测试 API 认证流程。 |
| Chrome DevTools | 查看请求头和 Cookie | 检查 Authorization 头或 Cookie 中的 JWT | 确认客户端正确发送令牌。 |
| Wireshark | 抓包分析 | 分析 HTTPS 前的明文传输(仅测试环境) | 仅用于本地调试,生产环境必须加密。 |
| 自定义日志 | 记录 JWT 错误 | 捕获异常并记录 token、时间、IP | 有助于排查问题,但避免记录完整密钥。 |
7.3 安全漏洞防范
| 漏洞类型 | 描述 | 防范措施 |
|---|
| 算法混淆攻击 | 攻击者修改 alg 为 “none”,使服务端不验证签名 | 服务端强制指定算法,不信任 header 中的 alg。 |
| 密钥泄露 | HMAC 密钥或 RSA 私钥被暴露 | 使用环境变量或密钥管理服务(如 AWS KMS),禁止硬编码。 |
| 重放攻击 | 攻击者截获有效 JWT 并重复使用 | 设置短过期时间,使用 jti + 黑名单机制。 |
| XSS 攻击 | JS 脚本窃取 localStorage 中的 JWT | 使用 HttpOnly Cookie 存储,启用 CSP 策略。 |
| CSRF 攻击 | 跨站请求伪造,利用 Cookie 中的 JWT | 启用 SameSite=Strict/Lax,使用双重提交 Cookie 模式。 |
| 信息泄露 | Payload 中包含敏感信息 | 避免在 JWT 中存储密码、身份证号等敏感数据。 |
| 暴力破解 | 弱密钥被暴力破解 | 使用强密钥(长度 ≥ 32 字符),定期轮换。 |
| DoS 攻击 | 构造大量无效 JWT 耗尽服务器资源 | 限制请求频率,优化验证逻辑,使用缓存。 |
第八章:MVP 实践
8.1 JavaScript(Node.js)实现
| 文件 | 用途 | 注意 |
|---|
| .env | 配置环境变量 | 由 dotenv.config() 注入到环境变量中,然后使用 process.env 进行调用 |
| package.json | 配置三方依赖 | package-lock.json 是执行 npm install 之后自动生成 |
| server.js | 服务代码 | |
| test.js | 测试代码 | |
.env 配置环境变量:
PORT=3000
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
NODE_ENV=development
package.json 配置三方依赖:
{
"name": "jwt-minimal-backend",
"version": "1.0.0",
"description": "A minimal JWT-based backend application",
"main": "server.js",
"type": "module",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},
"dependencies": {
"express": "^4.18.2",
"jsonwebtoken": "^9.0.0",
"bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"dotenv": "^16.0.3"
},
"devDependencies": {
"nodemon": "^2.0.20"
}
}
server.js 服务代码:
import express from 'express';
import jwt from 'jsonwebtoken';
import bcrypt from 'bcryptjs';
import cors from 'cors';
import dotenv from 'dotenv';
dotenv.config();
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(express.json());
app.use(cors());
// Secret key for JWT (in production, use a strong secret from environment variables)
const JWT_SECRET = process.env.JWT_SECRET || 'your-super-secret-jwt-key';
// Mock database (in real applications, use a proper database)
let users = [
{
id: 1,
username: 'admin',
email: 'admin@example.com',
password: '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi' // password: password
}
];
// Utility function to generate JWT token
const generateToken = (user) => {
return jwt.sign(
{
id: user.id,
username: user.username,
email: user.email
},
JWT_SECRET,
{ expiresIn: '24h' }
);
};
// Public route - Health check
app.get('/', (req, res) => {
res.json({ message: 'JWT Minimal Backend API is running!' });
});
// Register endpoint
app.post('/api/auth/register', async (req, res) => {
try {
const { username, email, password } = req.body;
// Validate input
if (!username || !email || !password) {
return res.status(400).json({
success: false,
message: 'All fields are required'
});
}
// Check if user already exists
const existingUser = users.find(u => u.username === username || u.email === email);
if (existingUser) {
return res.status(400).json({
success: false,
message: 'User already exists'
});
}
// Hash password
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(password, saltRounds);
// Create new user
const newUser = {
id: users.length + 1,
username,
email,
password: hashedPassword
};
users.push(newUser);
// Generate token
const token = generateToken(newUser);
res.status(201).json({
success: true,
message: 'User registered successfully',
token,
user: {
id: newUser.id,
username: newUser.username,
email: newUser.email
}
});
} catch (error) {
console.error('Registration error:', error);
res.status(500).json({
success: false,
message: 'Internal server error'
});
}
});
// Login endpoint
app.post('/api/auth/login', async (req, res) => {
try {
const { username, password } = req.body;
// Validate input
if (!username || !password) {
return res.status(400).json({
success: false,
message: 'Username and password are required'
});
}
// Find user
const user = users.find(u => u.username === username);
if (!user) {
return res.status(401).json({
success: false,
message: 'Invalid credentials'
});
}
// Verify password
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
return res.status(401).json({
success: false,
message: 'Invalid credentials'
});
}
// Generate token
const token = generateToken(user);
res.json({
success: true,
message: 'Login successful',
token,
user: {
id: user.id,
username: user.username,
email: user.email
}
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({
success: false,
message: 'Internal server error'
});
}
});
// Middleware to authenticate token
const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({
success: false,
message: 'Access token required'
});
}
jwt.verify(token, JWT_SECRET, (err, decoded) => {
if (err) {
return res.status(403).json({
success: false,
message: 'Invalid or expired token'
});
}
req.user = decoded;
next();
});
};
// Protected route example
app.get('/api/profile', authenticateToken, (req, res) => {
res.json({
success: true,
message: 'Profile data retrieved successfully',
user: req.user
});
});
// Protected route example - Get all users
app.get('/api/users', authenticateToken, (req, res) => {
// Return all users except passwords
const usersWithoutPasswords = users.map(({ password, ...user }) => user);
res.json({
success: true,
users: usersWithoutPasswords
});
});
// Start server
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
console.log(`Visit http://localhost:${PORT} to test the API`);
});
test.js 测试代码:
// Simple API test script
import axios from 'axios';
const BASE_URL = 'http://localhost:3000';
let authToken = '';
async function testAPI() {
console.log('Testing JWT Minimal Backend API...\n');
try {
// Test health check
console.log('1. Testing health check endpoint...');
const healthResponse = await axios.get(`${BASE_URL}/`);
console.log('✓ Health check:', healthResponse.data.message);
// Test registration
console.log('\n2. Testing user registration...');
const registerResponse = await axios.post(`${BASE_URL}/api/auth/register`, {
username: 'testuser',
email: 'test@example.com',
password: 'password123'
});
console.log('✓ Registration successful:', registerResponse.data.message);
// Store the token for future requests
authToken = registerResponse.data.token;
console.log('✓ Token received:', authToken.substring(0, 20) + '...');
// Test login
console.log('\n3. Testing user login...');
const loginResponse = await axios.post(`${BASE_URL}/api/auth/login`, {
username: 'testuser',
password: 'password123'
});
console.log('✓ Login successful:', loginResponse.data.message);
// Test protected route - Profile
console.log('\n4. Testing protected profile route...');
const profileResponse = await axios.get(`${BASE_URL}/api/profile`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
console.log('✓ Profile access successful:', profileResponse.data.message);
console.log('✓ User info:', profileResponse.data.user);
// Test protected route - Users
console.log('\n5. Testing protected users route...');
const usersResponse = await axios.get(`${BASE_URL}/api/users`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
console.log('✓ Users access successful:', usersResponse.data.success);
console.log('✓ Users count:', usersResponse.data.users.length);
console.log('\n🎉 All tests passed! JWT authentication is working correctly.');
} catch (error) {
console.error('❌ Test failed:', error.response?.data || error.message);
}
}
// Run tests
testAPI();