Article

身份验证(客户端验证)JWT

更新于:2026-07-14

第一章: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 字段,无法主动失效可主动删除服务端 SessionJWT 需结合黑名单或短过期时间实现主动登出。
性能开销首次生成略高,验证快每次请求需查服务端存储JWT 验证无需查库,性能更优。

第二章:JWT 的结构解析

2.1 Header(头部)

方法/参数语法用途代码示例注意事项
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-SHA256HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret )使用密钥生成签名签名值为字节数组的 Base64Url 编码密钥必须保密,泄露将导致签名可伪造。
RSA-SHA256RSASHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), privateKey )使用私钥签名验证时使用公钥非对称更安全,适合分布式签发。
secret字符串或字节数组HMAC 签名密钥"my_secret_key"长度建议 ≥ 32 字符,避免弱密钥。
privateKeyPEM 格式私钥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第二部分
SignatureHMACSHA256(第一部分 + "." + 第二部分, "secret")SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c第三部分

最终 JWT:

eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJzdWIiOiAiMTIzNDU2Nzg5MCIsICJuYW1lIjogIkpvaG4gRG9lIiwgImlhdCI6IDE3MzA4NzAxNzd9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

注意事项:

  1. 三部分用点号(.)连接;
  2. 使用 Base64Url 编码(非 Base64);
  3. 签名确保完整性;
  4. 可在 jwt.io 等工具中解码查看内容。

第三章:JWT 的工作流程

3.1 JWT 的生成流程

步骤操作说明注意事项
1构造 Header定义算法(alg)和类型(typ)推荐使用 HS256 或 RS256,避免使用 “none” 算法。
2构造 Payload添加标准声明(如 exp、iss)和私有声明(如 user_id、role)避免存储敏感信息(如密码),控制载荷大小。
3Base64Url 编码 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 和 PayloadBase64Url 解码前两部分可查看内容,但未验证完整性。
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 标准声明字段详解

声明全称用途数据类型示例注意事项
issIssuer签发者标识string"https://auth.example.com"可选,用于验证令牌来源。
subSubject主题,代表用户或实体string"user123"可选,通常为用户唯一 ID。
audAudience接收方,指定哪些服务可接受此令牌string 或 array"https://api.example.com"必须验证,防止令牌被用于未授权服务。
expExpiration Time过期时间,UTC 时间的秒级时间戳number1730873777强烈建议设置,过期后令牌无效。
nbfNot Before生效时间,UTC 时间的秒级时间戳number1730870177可选,控制令牌提前生效。
iatIssued At签发时间,UTC 时间的秒级时间戳number1730870177可选,用于判断令牌年龄。
jtiJWT 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 获取用户数据无需查询数据库,实现无状态认证。

注意事项:

  1. JWT 应通过 Authorization: Bearer <token> 传递;
  2. 避免 localStorage 存储以防 XSS;
  3. 可结合 HttpOnly Cookie 提升安全性。

5.2 Token 刷新机制(Refresh Token)

方法/参数语法用途代码示例注意事项
Refresh Token长期有效的令牌(存储在服务端)用于获取新的 Access TokenrefreshToken: "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实现主动登出功能。

流程说明:

  1. 用户登录获取 Access Token 和 Refresh Token;
  2. Access Token 过期后,客户端用 Refresh Token 请求新令牌;
  3. 服务端验证 Refresh Token,签发新 Access Token;
  4. 用户登出时,将 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)生成 JWTjwt.sign({ id: 123 }, 'secret', { expiresIn: '15m' })options 可设置 expiresIn、algorithm 等。
jwt.verify()jwt.verify(token, secretOrPublicKey, callback)验证 JWT 签名和 expjwt.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)生成 JWTjwt.encode({"id": 123}, "secret", algorithm="HS256")返回字符串;若使用 RS256,key 为私钥字符串。
jwt.decode()jwt.decode(token, key, algorithms)验证并解码 JWTjwt.decode(token, "secret", algorithms=["HS256"])必须指定 algorithms,否则易受攻击。
jwt.get_unverified_header()jwt.get_unverified_header(token)获取未验证的 headeralg = jwt.get_unverified_header(token)['alg']用于动态选择密钥或算法。
异常处理jwt.ExpiredSignatureErrorexp 过期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()构建并生成 JWTJwts.builder().setExpiration(new Date(System.currentTimeMillis() + 86400000)).signWith(SignatureAlgorithm.HS256, "secret").compact()使用链式调用设置声明。
Jwts.parser()Jwts.parser().setSigningKey(key).parseClaimsJws(token)解析并验证 JWTJwts.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();