使用 NestJS 实现 OAuth2 授权服务

289 阅读4分钟

使用 NestJS 实现 OAuth2 授权服务

1. 项目概述

本文将详细介绍如何使用 NestJS 框架实现一个完整的 OAuth2 授权服务。我们将构建一个符合 OAuth2 规范的授权服务器,支持多种授权模式,并实现相关的安全特性。

1.1 技术栈

  • NestJS:主框架
  • TypeORM:数据库 ORM
  • PostgreSQL:数据库
  • JWT:令牌生成和验证
  • class-validator:数据验证
  • bcrypt:密码加密

1.2 功能特性

  • 用户认证系统
  • 客户端应用管理
  • OAuth2 授权流程
    • 授权码模式
    • 密码模式
    • 客户端模式
    • 简化模式
  • 令牌管理
    • 访问令牌
    • 刷新令牌
  • 安全特性
    • CSRF 防护
    • Rate Limiting
    • 密码加密

2. 项目初始化

2.1 创建 NestJS 项目

npm i -g @nestjs/cli
nest new oauth2-authorization-server
cd oauth2-authorization-server

2.2 安装依赖

npm install @nestjs/typeorm typeorm pg @nestjs/jwt @nestjs/passport passport passport-oauth2 bcrypt class-validator class-transformer @nestjs/config
npm install -D @types/passport-oauth2 @types/bcrypt

3. 数据库设计

3.1 用户表设计

// src/entities/user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';

@Entity()
export class User {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ unique: true })
  username: string;

  @Column()
  password: string;

  @Column({ nullable: true })
  email: string;

  @Column({ default: true })
  isActive: boolean;
}

3.2 客户端表设计

// src/entities/client.entity.ts
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';

@Entity()
export class Client {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column()
  clientId: string;

  @Column()
  clientSecret: string;

  @Column('simple-array')
  redirectUris: string[];

  @Column('simple-array')
  grants: string[];

  @Column({ default: true })
  isActive: boolean;
}

3.3 授权码表设计

// src/entities/authorization-code.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm';
import { User } from './user.entity';
import { Client } from './client.entity';

@Entity()
export class AuthorizationCode {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column()
  code: string;

  @Column()
  expiresAt: Date;

  @Column('simple-array')
  scope: string[];

  @ManyToOne(() => User)
  user: User;

  @ManyToOne(() => Client)
  client: Client;

  @Column()
  redirectUri: string;
}

4. 核心模块实现

4.1 用户模块

// src/modules/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from '../../entities/user.entity';
import * as bcrypt from 'bcrypt';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>,
  ) {}

  async create(username: string, password: string, email?: string): Promise<User> {
    const hashedPassword = await bcrypt.hash(password, 10);
    const user = this.usersRepository.create({
      username,
      password: hashedPassword,
      email,
    });
    return this.usersRepository.save(user);
  }

  async validateUser(username: string, password: string): Promise<User | null> {
    const user = await this.usersRepository.findOne({ where: { username } });
    if (user && await bcrypt.compare(password, user.password)) {
      return user;
    }
    return null;
  }
}

4.2 客户端模块

// src/modules/clients/clients.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Client } from '../../entities/client.entity';

@Injectable()
export class ClientsService {
  constructor(
    @InjectRepository(Client)
    private clientsRepository: Repository<Client>,
  ) {}

  async validateClient(clientId: string, clientSecret: string): Promise<Client | null> {
    const client = await this.clientsRepository.findOne({
      where: { clientId, clientSecret, isActive: true },
    });
    return client || null;
  }

  async create(clientData: Partial<Client>): Promise<Client> {
    const client = this.clientsRepository.create(clientData);
    return this.clientsRepository.save(client);
  }
}

4.3 OAuth2 模块

// src/modules/oauth2/oauth2.service.ts
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { AuthorizationCode } from '../../entities/authorization-code.entity';
import { UsersService } from '../users/users.service';
import { ClientsService } from '../clients/clients.service';

@Injectable()
export class OAuth2Service {
  constructor(
    private jwtService: JwtService,
    private usersService: UsersService,
    private clientsService: ClientsService,
    @InjectRepository(AuthorizationCode)
    private authCodeRepository: Repository<AuthorizationCode>,
  ) {}

  async generateAuthorizationCode(userId: string, clientId: string, scope: string[], redirectUri: string): Promise<string> {
    const code = this.generateRandomString(32);
    const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10 minutes

    const authCode = this.authCodeRepository.create({
      code,
      expiresAt,
      scope,
      user: { id: userId },
      client: { id: clientId },
      redirectUri,
    });

    await this.authCodeRepository.save(authCode);
    return code;
  }

  async generateTokens(userId: string, clientId: string, scope: string[]): Promise<{
    accessToken: string;
    refreshToken: string;
  }> {
    const payload = {
      sub: userId,
      clientId,
      scope,
    };

    const accessToken = this.jwtService.sign(payload, { expiresIn: '1h' });
    const refreshToken = this.jwtService.sign(payload, { expiresIn: '7d' });

    return {
      accessToken,
      refreshToken,
    };
  }

  private generateRandomString(length: number): string {
    const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    let result = '';
    for (let i = 0; i < length; i++) {
      result += charset[Math.floor(Math.random() * charset.length)];
    }
    return result;
  }
}

5. 授权端点实现

5.1 授权端点

// src/modules/oauth2/oauth2.controller.ts
import { Controller, Get, Post, Body, Query, Req, Res, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { OAuth2Service } from './oauth2.service';

@Controller('oauth2')
export class OAuth2Controller {
  constructor(private oauth2Service: OAuth2Service) {}

  @Get('authorize')
  @UseGuards(AuthGuard('oauth2'))
  async authorize(
    @Query('client_id') clientId: string,
    @Query('redirect_uri') redirectUri: string,
    @Query('scope') scope: string,
    @Query('response_type') responseType: string,
    @Query('state') state: string,
    @Req() req,
    @Res() res,
  ) {
    if (responseType !== 'code') {
      throw new Error('Unsupported response type');
    }

    const code = await this.oauth2Service.generateAuthorizationCode(
      req.user.id,
      clientId,
      scope.split(' '),
      redirectUri,
    );

    const redirectUrl = new URL(redirectUri);
    redirectUrl.searchParams.append('code', code);
    if (state) {
      redirectUrl.searchParams.append('state', state);
    }

    res.redirect(redirectUrl.toString());
  }

  @Post('token')
  async token(
    @Body('grant_type') grantType: string,
    @Body('code') code: string,
    @Body('client_id') clientId: string,
    @Body('client_secret') clientSecret: string,
    @Body('redirect_uri') redirectUri: string,
  ) {
    // 验证授权码
    const authCode = await this.oauth2Service.validateAuthorizationCode(
      code,
      clientId,
      redirectUri,
    );

    // 生成令牌
    const tokens = await this.oauth2Service.generateTokens(
      authCode.user.id,
      clientId,
      authCode.scope,
    );

    return {
      access_token: tokens.accessToken,
      token_type: 'Bearer',
      expires_in: 3600,
      refresh_token: tokens.refreshToken,
      scope: authCode.scope.join(' '),
    };
  }
}

6. 安全性实现

6.1 Rate Limiting

// src/modules/oauth2/oauth2.module.ts
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';

@Module({
  imports: [
    ThrottlerModule.forRoot({
      ttl: 60,
      limit: 10,
    }),
  ],
  providers: [
    {
      provide: APP_GUARD,
      useClass: ThrottlerGuard,
    },
  ],
})
export class OAuth2Module {}

6.2 CSRF 防护

// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as csurf from 'csurf';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.use(csurf());
  await app.listen(3000);
}
bootstrap();

7. 使用示例

7.1 授权码模式流程

  1. 客户端重定向用户到授权端点:
GET /oauth2/authorize?
  response_type=code&
  client_id=client123&
  redirect_uri=https://client.example.com/callback&
  scope=read write&
  state=xyz
  1. 用户登录并授权后,服务器重定向回客户端:
HTTP/1.1 302 Found
Location: https://client.example.com/callback?code=SplxlOBeZQQYbYS6WxSbIA&state=xyz
  1. 客户端使用授权码请求访问令牌:
POST /oauth2/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
code=SplxlOBeZQQYbYS6WxSbIA&
client_id=client123&
client_secret=secret123&
redirect_uri=https://client.example.com/callback
  1. 服务器返回访问令牌:
{
  "access_token": "2YotnFZFEjr1zCsicMWpAA",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
  "scope": "read write"
}

8. 总结

本文详细介绍了如何使用 NestJS 实现一个完整的 OAuth2 授权服务器。我们实现了:

  • 完整的 OAuth2 授权流程
  • 用户认证系统
  • 客户端应用管理
  • 令牌管理