使用 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 授权码模式流程
- 客户端重定向用户到授权端点:
GET /oauth2/authorize?
response_type=code&
client_id=client123&
redirect_uri=https://client.example.com/callback&
scope=read write&
state=xyz
- 用户登录并授权后,服务器重定向回客户端:
HTTP/1.1 302 Found
Location: https://client.example.com/callback?code=SplxlOBeZQQYbYS6WxSbIA&state=xyz
- 客户端使用授权码请求访问令牌:
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
- 服务器返回访问令牌:
{
"access_token": "2YotnFZFEjr1zCsicMWpAA",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
"scope": "read write"
}
8. 总结
本文详细介绍了如何使用 NestJS 实现一个完整的 OAuth2 授权服务器。我们实现了:
- 完整的 OAuth2 授权流程
- 用户认证系统
- 客户端应用管理
- 令牌管理