JWT 的测试是确保认证系统安全可靠的重要环节。以下是完整的测试策略和实现方法:
1. 单元测试
测试 Token 生成
javascriptconst jwt = require('jsonwebtoken'); const { expect } = require('chai'); describe('JWT Token Generation', () => { const SECRET_KEY = 'test-secret-key'; const payload = { userId: '123', username: 'testuser' }; it('should generate a valid token', () => { const token = jwt.sign(payload, SECRET_KEY); expect(token).to.be.a('string'); expect(token.split('.')).to.have.lengthOf(3); }); it('should generate token with correct payload', () => { const token = jwt.sign(payload, SECRET_KEY); const decoded = jwt.decode(token); expect(decoded.userId).to.equal('123'); expect(decoded.username).to.equal('testuser'); }); it('should generate token with expiration time', () => { const token = jwt.sign(payload, SECRET_KEY, { expiresIn: '1h' }); const decoded = jwt.decode(token); expect(decoded.exp).to.be.a('number'); expect(decoded.exp).to.be.greaterThan(Math.floor(Date.now() / 1000)); }); it('should generate token with custom claims', () => { const customPayload = { ...payload, iss: 'test-issuer', aud: 'test-audience' }; const token = jwt.sign(customPayload, SECRET_KEY); const decoded = jwt.decode(token); expect(decoded.iss).to.equal('test-issuer'); expect(decoded.aud).to.equal('test-audience'); }); });
测试 Token 验证
javascriptdescribe('JWT Token Verification', () => { const SECRET_KEY = 'test-secret-key'; const payload = { userId: '123', username: 'testuser' }; it('should verify a valid token', () => { const token = jwt.sign(payload, SECRET_KEY); const decoded = jwt.verify(token, SECRET_KEY); expect(decoded.userId).to.equal('123'); expect(decoded.username).to.equal('testuser'); }); it('should throw error for invalid token', () => { const invalidToken = 'invalid.token.here'; expect(() => { jwt.verify(invalidToken, SECRET_KEY); }).to.throw('jwt malformed'); }); it('should throw error for expired token', () => { const expiredToken = jwt.sign(payload, SECRET_KEY, { expiresIn: '-1s' }); expect(() => { jwt.verify(expiredToken, SECRET_KEY); }).to.throw('jwt expired'); }); it('should throw error for wrong secret', () => { const token = jwt.sign(payload, SECRET_KEY); const wrongSecret = 'wrong-secret'; expect(() => { jwt.verify(token, wrongSecret); }).to.throw('invalid signature'); }); it('should verify token with algorithm check', () => { const token = jwt.sign(payload, SECRET_KEY, { algorithm: 'HS256' }); const decoded = jwt.verify(token, SECRET_KEY, { algorithms: ['HS256'] }); expect(decoded.userId).to.equal('123'); }); it('should reject token with wrong algorithm', () => { const token = jwt.sign(payload, SECRET_KEY, { algorithm: 'HS256' }); expect(() => { jwt.verify(token, SECRET_KEY, { algorithms: ['RS256'] }); }).to.throw('invalid algorithm'); }); });
2. 集成测试
测试认证流程
javascriptconst request = require('supertest'); const app = require('../app'); describe('Authentication Flow Integration Tests', () => { let authToken; it('should login and receive token', async () => { const response = await request(app) .post('/auth/login') .send({ username: 'testuser', password: 'password123' }) .expect(200); expect(response.body).to.have.property('token'); expect(response.body).to.have.property('expiresIn'); authToken = response.body.token; }); it('should access protected route with valid token', async () => { const response = await request(app) .get('/api/protected') .set('Authorization', `Bearer ${authToken}`) .expect(200); expect(response.body).to.have.property('data'); }); it('should reject request without token', async () => { await request(app) .get('/api/protected') .expect(401); }); it('should reject request with invalid token', async () => { await request(app) .get('/api/protected') .set('Authorization', 'Bearer invalid-token') .expect(401); }); it('should reject request with expired token', async () => { const expiredToken = jwt.sign( { userId: '123' }, process.env.JWT_SECRET, { expiresIn: '-1s' } ); await request(app) .get('/api/protected') .set('Authorization', `Bearer ${expiredToken}`) .expect(401); }); });
测试 Token 刷新
javascriptdescribe('Token Refresh Integration Tests', () => { let accessToken; let refreshToken; it('should login and receive both tokens', async () => { const response = await request(app) .post('/auth/login') .send({ username: 'testuser', password: 'password123' }) .expect(200); accessToken = response.body.accessToken; refreshToken = response.body.refreshToken; expect(accessToken).to.exist; expect(refreshToken).to.exist; }); it('should refresh access token', async () => { const response = await request(app) .post('/auth/refresh') .send({ refreshToken }) .expect(200); expect(response.body).to.have.property('accessToken'); expect(response.body.accessToken).to.not.equal(accessToken); accessToken = response.body.accessToken; }); it('should access protected route with new token', async () => { await request(app) .get('/api/protected') .set('Authorization', `Bearer ${accessToken}`) .expect(200); }); it('should reject refresh with invalid token', async () => { await request(app) .post('/auth/refresh') .send({ refreshToken: 'invalid-refresh-token' }) .expect(401); }); });
3. 安全测试
测试算法混淆攻击
javascriptdescribe('Security Tests - Algorithm Confusion', () => { it('should reject tokens with none algorithm', () => { const maliciousToken = jwt.sign( { userId: 'admin' }, '', { algorithm: 'none' } ); expect(() => { jwt.verify(maliciousToken, SECRET_KEY, { algorithms: ['HS256'] }); }).to.throw(); }); it('should reject tokens with unspecified algorithm', () => { const token = jwt.sign({ userId: '123' }, SECRET_KEY); // 尝试使用不同的算法验证 expect(() => { jwt.verify(token, SECRET_KEY, { algorithms: ['none'] }); }).to.throw('invalid algorithm'); }); });
测试 Token 篡改
javascriptdescribe('Security Tests - Token Tampering', () => { it('should reject tampered token', () => { const token = jwt.sign({ userId: '123' }, SECRET_KEY); // 篡改 token const parts = token.split('.'); parts[1] = Buffer.from(JSON.stringify({ userId: 'admin' })).toString('base64'); const tamperedToken = parts.join('.'); expect(() => { jwt.verify(tamperedToken, SECRET_KEY); }).to.throw('invalid signature'); }); });
4. 性能测试
测试 Token 生成性能
javascriptconst Benchmark = require('benchmark'); const suite = new Benchmark.Suite(); describe('Performance Tests', () => { it('should benchmark token generation', function(done) { this.timeout(10000); suite .add('HS256', () => { jwt.sign({ userId: '123' }, SECRET_KEY, { algorithm: 'HS256' }); }) .add('RS256', () => { jwt.sign({ userId: '123' }, privateKey, { algorithm: 'RS256' }); }) .add('ES256', () => { jwt.sign({ userId: '123' }, privateKey, { algorithm: 'ES256' }); }) .on('complete', function() { console.log('Fastest is ' + this.filter('fastest').map('name')); done(); }) .run({ async: true }); }); it('should handle concurrent token generation', async () => { const concurrency = 1000; const promises = []; for (let i = 0; i < concurrency; i++) { promises.push( new Promise((resolve) => { const start = Date.now(); jwt.sign({ userId: i.toString() }, SECRET_KEY); resolve(Date.now() - start); }) ); } const times = await Promise.all(promises); const avgTime = times.reduce((a, b) => a + b, 0) / times.length; expect(avgTime).to.be.below(10); // 平均时间应小于 10ms }); });
5. Mock 和 Stub
使用 Mock 测试
javascriptconst sinon = require('sinon'); describe('Authentication with Mocks', () => { let verifyStub; beforeEach(() => { verifyStub = sinon.stub(jwt, 'verify'); }); afterEach(() => { verifyStub.restore(); }); it('should handle valid token with mock', async () => { verifyStub.returns({ userId: '123', username: 'testuser' }); const response = await request(app) .get('/api/protected') .set('Authorization', 'Bearer mock-token') .expect(200); expect(verifyStub.calledOnce).to.be.true; }); it('should handle invalid token with mock', async () => { verifyStub.throws(new Error('Invalid token')); const response = await request(app) .get('/api/protected') .set('Authorization', 'Bearer invalid-token') .expect(401); expect(verifyStub.calledOnce).to.be.true; }); });
6. 测试覆盖率
使用 Istanbul/nyc
javascript// package.json { "scripts": { "test": "jest", "test:coverage": "jest --coverage", "test:watch": "jest --watch" }, "jest": { "collectCoverageFrom": [ "src/**/*.js", "!src/**/*.test.js" ], "coverageThreshold": { "global": { "branches": 80, "functions": 80, "lines": 80, "statements": 80 } } } }
7. 端到端测试
使用 Cypress
javascript// cypress/integration/auth.spec.js describe('Authentication E2E Tests', () => { it('should login successfully', () => { cy.visit('/login'); cy.get('input[name="username"]').type('testuser'); cy.get('input[name="password"]').type('password123'); cy.get('button[type="submit"]').click(); cy.url().should('include', '/dashboard'); cy.get('[data-testid="user-info"]').should('contain', 'testuser'); }); it('should redirect to login on token expiry', () => { cy.window().then((win) => { // 模拟 token 过期 win.localStorage.setItem('token', 'expired-token'); }); cy.visit('/dashboard'); cy.url().should('include', '/login'); }); it('should refresh token automatically', () => { cy.login('testuser', 'password123'); // 等待 token 刷新 cy.wait(15000); // 验证可以继续访问受保护的路由 cy.visit('/dashboard'); cy.url().should('include', '/dashboard'); }); });
8. 测试最佳实践
测试清单
- 测试 Token 生成
- 测试 Token 验证
- 测试过期处理
- 测试无效 Token
- 测试安全漏洞
- 测试性能
- 测试并发
- 测试边界情况
- 测试错误处理
- 达到测试覆盖率目标
测试工具推荐
- 单元测试: Jest, Mocha, Chai
- 集成测试: Supertest
- 端到端测试: Cypress, Playwright
- 性能测试: Benchmark.js, Artillery
- 安全测试: OWASP ZAP, Burp Suite
- 覆盖率: Istanbul/nyc, Jest Coverage
通过完整的测试策略,可以确保 JWT 认证系统的安全性、可靠性和性能。