NestJS项目实战:JWT认证和签名认证的实现方案
前言
在后端开发中,接口安全是一个重要话题。本文结合实际项目经验,详细介绍在 NestJS 框架中如何实现和使用 JWT认证 和 签名认证 这两种主流的认证方式。
一、认证方案分析
1.1 应用场景分析
在支付系统开发中,存在两种不同的认证需求:
-
后台管理系统认证
- 用户登录态管理
- 用户身份识别
- 权限信息携带
- 支持登出操作
-
开放API认证
- 第三方系统调用
- 支付回调通知
- 无需登录态
- 请求合法性验证
1.2 认证方案对比
特性 | JWT认证 | 签名认证 |
---|---|---|
使用场景 | 用户登录 | 开放API |
认证方式 | Token | 参数签名 |
存储需求 | 无需存储 | 需要密钥 |
可撤销性 | 不可撤销 | 可撤销 |
信息携带 | 可携带信息 | 不携带信息 |
性能开销 | 较小 | 较大 |
二、JWT认证实现
2.1 JWT基础知识
JWT(JSON Web Token)的组成部分:
- Header(头部)
- Payload(负载)
- Signature(签名)
标准格式:xxxxx.yyyyy.zzzzz
2.2 环境准备
bash
安装依赖包
npm install @nestjs/jwt @nestjs/passport passport passport-jwt
安装类型定义
npm install @types/passport-jwt -D
2.3 JWT模块配置
// src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { JwtStrategy } from './strategies/jwt.strategy';
@Module({
imports: [
PassportModule,
JwtModule.register({
secret: process.env.JWT_SECRET || 'your-secret-key',
signOptions: {
expiresIn: '24h',
algorithm: 'HS256'
},
}),
],
providers: [AuthService, JwtStrategy],
exports: [AuthService],
})
export class AuthModule {}
2.4 JWT策略实现
// src/auth/strategies/jwt.strategy.ts
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: process.env.JWT_SECRET || 'your-secret-key',
});
}
async validate(payload: any) {
const { userId, username } = payload;
if (!userId) {
throw new UnauthorizedException('无效的token');
}
return payload;
}
}
2.5 认证服务实现
// src/auth/auth.service.ts
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class AuthService {
constructor(private readonly jwtService: JwtService) {}
async generateToken(user: any) {
const payload = {
userId: user.userId,
username: user.username,
roles: user.roles
};
return {
access_token: this.jwtService.sign(payload),
expires_in: 24 60 60
};
}
async verifyToken(token: string) {
try {
return this.jwtService.verify(token);
} catch (error) {
return null;
}
}
}
三、签名认证实现
3.1 签名认证流程
- 请求参数排序
- 参数和密钥拼接
- MD5加密生成签名
- 服务端验证签名
3.2 签名工具类
// src/utils/sign.util.ts
import { createHash } from 'crypto';
export class SignUtil {
static generateSign(params: Record<string, any>, apiKey: string): string {
// 1. 过滤空值和签名字段
const filteredParams = Object.entries(params)
.filter(([key, value]) => {
return key !== 'sign' &&
value !== '' &&
value !== undefined &&
value !== null;
})
.reduce((acc, [key, value]) => {
acc[key] = value;
return acc;
}, {});
// 2. 参数排序
const sortedParams = Object.keys(filteredParams)
.sort()
.reduce((acc, key) => {
acc[key] = filteredParams[key];
return acc;
}, {});
// 3. 拼接参数
let signStr = Object.entries(sortedParams)
.map(([key, value]) => ${key}=${value})
.join('&');
signStr += &key=${apiKey};
// 4. MD5加密
return createHash('md5')
.update(signStr)
.digest('hex')
.toUpperCase();
}
static verifySign(params: Record<string, any>, sign: string, apiKey: string): boolean {
const calculatedSign = this.generateSign(params, apiKey);
return calculatedSign === sign;
}
}
3.3 签名认证守卫
// src/auth/guards/sign-auth.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { SignUtil } from '../../utils/sign.util';
import { ApiKeyService } from '../services/api-key.service';
@Injectable()
export class SignAuthGuard implements CanActivate {
constructor(
private readonly reflector: Reflector,
private readonly apiKeyService: ApiKeyService
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const params = { ...request.body, ...request.query };
const sign = params.sign;
const mchNo = params.mchNo;
if (!sign || !mchNo) {
return false;
}
try {
const apiKey = await this.apiKeyService.getApiKey(mchNo);
if (!apiKey) {
return false;
}
return SignUtil.verifySign(params, sign, apiKey);
} catch (error) {
console.error('签名验证失败:', error);
return false;
}
}
}```
## 四、实际应用示例
### 4.1 用户登录接口
```typescript
@Controller('sys')
export class SysController {
constructor(
private readonly authService: AuthService,
private readonly userService: UserService,
) {}
@Public()
@Post('login')
async login(@Body() loginDto: LoginDto) {
const user = await this.userService.validateUser(
loginDto.username,
loginDto.password
);
const token = await this.authService.generateToken(user);
return {
code: 0,
msg: '登录成功',
data: {
...token,
userInfo: {
userId: user.userId,
username: user.username,
roles: user.roles
}
}
};
}
}```
### 4.2 支付回调接口
```typescript
@Controller('pay')
export class PayController {
constructor(private readonly payService: PayService) {}
@SignAuth()
@Post('notify')
async payNotify(@Body() notifyDto: NotifyDto) {
try {
await this.payService.verifyNotifyData(notifyDto);
await this.payService.handlePayResult(notifyDto);
return 'success';
} catch (error) {
console.error('支付回调处理失败:', error);
return 'fail';
}
}
}
五、项目实践要点
5.1 安全性要点
-
JWT安全
- 定期轮换密钥
- 合理设置过期时间
- 关键操作二次验证
- 刷新token机制
-
签名安全
- 商户密钥隔离
- 时间戳校验
- 防重放处理
- 异常日志记录
5.2 性能优化
-
缓存优化
- 商户密钥缓存
- JWT黑名单缓存
- 用户信息缓存
-
计算优化
- 签名计算优化
- 参数排序优化
- 验证逻辑优化
5.3 开发规范
-
错误处理
- 统一错误拦截
- 错误日志记录
- 标准错误响应
-
代码组织
- 模块化设计
- 统一响应格式
- 规范代码注释
六、常见问题处理
6.1 JWT问题处理
-
Token过期处理
- 刷新token机制
- 过期前续期
- 降级处理方案
-
Token注销处理
- token黑名单
- Redis存储机制
- 定期清理策略
6.2 签名问题处理
-
验签失败排查
- 参数排序检查
- 密钥正确性验证
- 请求日志分析
-
重放攻击防范
- 时间戳验证
- 流水号机制
- 幂等性保证
七、总结
本文详细介绍了NestJS中JWT认证和签名认证的实现方案:
- JWT认证适用于用户登录场景
- 签名认证适用于开放API接口
- 两种认证方式可组合使用
- 通过装饰器实现认证控制
参考资料
本文首发于优快云,