Skip to content
第 186 / 250 章Node⏱ 12 分钟阅读

第 186 章:认证与授权(JWT / Passport)

学习目标

  • 深入 JWT 实现原理
  • 掌握 Passport 集成
  • 实现 Refresh Token
  • 学会 OAuth 与第三方登录

一、JWT 基础

1.1 结构

Header.Payload.Signature
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

1.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-github2
typescript
// 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密码加密

动手练习

  1. 实现 register / login / profile 三个接口
  2. 加入 refresh token 机制
  3. 用 @CurrentUser 获取当前登录用户
  4. 集成 GitHub 第三方登录

推荐阅读


下一章:第 187 章:文件上传与静态资源

本站基于 VitePress 构建 · 由 Codebook 团队维护