第 186 章:认证与授权(JWT / Passport)
学习目标
- 深入 JWT 实现原理
- 掌握 Passport 集成
- 实现 Refresh Token
- 学会 OAuth 与第三方登录
一、JWT 基础
1.1 结构
Header.Payload.Signature
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c1.2 安装
bash
pnpm add @nestjs/jwt @nestjs/passport passport passport-jwt bcrypt
pnpm add -D @types/passport-jwt @types/bcrypt二、JWT 模块
2.1 配置
typescript
// auth.module.ts
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
@Module({
imports: [
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
secret: config.get('JWT_SECRET'),
signOptions: {
expiresIn: config.get('JWT_EXPIRES_IN', '7d'),
},
}),
}),
],
providers: [JwtStrategy, AuthService],
controllers: [AuthController],
exports: [AuthService],
})
export class AuthModule {}2.2 签发 Token
typescript
@Injectable()
export class AuthService {
constructor(
private jwtService: JwtService,
private userService: UserService,
private config: ConfigService,
) {}
async login(dto: LoginDto) {
const user = await this.userService.findByEmail(dto.email);
if (!user) throw new UnauthorizedException('账号不存在');
const match = await bcrypt.compare(dto.password, user.password);
if (!match) throw new UnauthorizedException('密码错误');
// access_token (短期)
const payload = {
sub: user.id,
username: user.name,
role: user.role,
};
const accessToken = this.jwtService.sign(payload);
return {
access_token: accessToken,
user: { id: user.id, name: user.name, role: user.role },
};
}
}三、JWT 策略
3.1 实现
typescript
// jwt.strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(config: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: config.get('JWT_SECRET'),
});
}
// 验证通过后,返回值挂到 req.user
async validate(payload: any) {
return {
userId: payload.sub,
username: payload.username,
role: payload.role,
};
}
}3.2 全局 Guard
typescript
// jwt-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}3.3 全局注册
typescript
@Module({
providers: [
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
],
})
export class AppModule {}四、@CurrentUser 装饰器
typescript
// decorators/current-user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const CurrentUser = createParamDecorator(
(data: string | undefined, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const user = request.user;
return data ? user?.[data] : user;
},
);
// 使用
@Get('profile')
getProfile(@CurrentUser() user: User) {
return user;
}
@Get('id')
getId(@CurrentUser('userId') userId: number) {
return { userId };
}五、Refresh Token
5.1 原理
5.2 实现
typescript
// auth.service.ts
@Injectable()
export class AuthService {
constructor(
private jwtService: JwtService,
private config: ConfigService,
) {}
async login(dto: LoginDto) {
// ...校验用户
return this.generateTokens(user);
}
async refresh(refreshToken: string) {
try {
const payload = this.jwtService.verify(refreshToken, {
secret: this.config.get('JWT_REFRESH_SECRET'),
});
const user = await this.userService.findOne(payload.sub);
return this.generateTokens(user);
} catch {
throw new UnauthorizedException('Invalid refresh token');
}
}
private generateTokens(user: User) {
const payload = { sub: user.id, username: user.name, role: user.role };
return {
access_token: this.jwtService.sign(payload, {
secret: this.config.get('JWT_SECRET'),
expiresIn: '15m',
}),
refresh_token: this.jwtService.sign(payload, {
secret: this.config.get('JWT_REFRESH_SECRET'),
expiresIn: '7d',
}),
};
}
}5.3 Controller
typescript
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Public()
@Post('login')
login(@Body() dto: LoginDto) {
return this.authService.login(dto);
}
@Public()
@Post('refresh')
refresh(@Body('refresh_token') token: string) {
return this.authService.refresh(token);
}
}六、OAuth 2.0
6.1 GitHub 登录
bash
pnpm add @nestjs/passport passport-github2typescript
// github.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-github2';
@Injectable()
export class GithubStrategy extends PassportStrategy(Strategy, 'github') {
constructor() {
super({
clientID: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
callbackURL: 'http://localhost:3000/auth/github/callback',
scope: ['user:email'],
});
}
async validate(accessToken: string, refreshToken: string, profile: any) {
const { id, displayName, emails } = profile;
return {
providerId: id,
name: displayName,
email: emails?.[0]?.value,
provider: 'github',
};
}
}6.2 Controller
typescript
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Public()
@Get('github')
@UseGuards(AuthGuard('github'))
github() {} // 自动跳转
@Public()
@Get('github/callback')
@UseGuards(AuthGuard('github'))
async githubCallback(@Req() req: Request) {
const user = await this.authService.oauthLogin(req.user);
return this.authService.login(user);
}
}七、密码加密
typescript
import * as bcrypt from 'bcrypt';
const SALT_ROUNDS = 10;
// 注册:加密
const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);
// 登录:校验
const match = await bcrypt.compare(password, hashedPassword);八、当前用户类型
typescript
// types/auth.d.ts
import 'express';
declare module 'express' {
interface Request {
user: {
userId: number;
username: string;
role: string;
};
}
}九、Security 最佳实践
typescript
@Injectable()
export class SecurityInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler) {
const res = context.switchToHttp().getResponse();
// 安全头
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
return next.handle();
}
}JWT 安全
JWT_SECRET至少 32 字符,放.env- 使用 HTTPS
- 短期 access + 长期 refresh
- 不要把敏感信息放 payload
十、本章小结
| 概念 | 用途 |
|---|---|
| JwtModule | 签发/校验 Token |
| JwtStrategy | 解析 Token |
| JwtAuthGuard | 全局守卫 |
| @CurrentUser | 注入用户 |
| Refresh Token | 续签 |
| OAuth | 第三方登录 |
| bcrypt | 密码加密 |
动手练习
- 实现 register / login / profile 三个接口
- 加入 refresh token 机制
- 用 @CurrentUser 获取当前登录用户
- 集成 GitHub 第三方登录
推荐阅读
下一章:第 187 章:文件上传与静态资源 →