第49集:认证系统实战
学习目标
- 了解认证系统的基本概念和组件
- 掌握JWT认证的原理和实现方法
- 学习如何在NestJS中实现用户注册和登录功能
- 掌握令牌管理和刷新令牌的实现
- 学习基于角色的授权实现
- 构建完整的认证系统实战案例
1. 认证系统概述
1.1 认证与授权
- 认证(Authentication):验证用户身份,确认用户是谁
- 授权(Authorization):验证用户权限,确认用户可以做什么
1.2 认证系统组件
- 用户存储:存储用户信息和密码哈希
- 认证机制:验证用户身份的方法(如用户名/密码、OAuth等)
- 会话管理:管理用户会话状态(如JWT令牌)
- 授权机制:控制用户访问权限(如基于角色的授权)
- 密码重置:处理用户密码重置请求
1.3 常见认证方法
- 基于会话的认证:使用cookie和session
- 基于令牌的认证:使用JWT、OAuth等
- 多因素认证:结合多种认证因素
- 生物识别认证:使用指纹、面部识别等
2. JWT认证原理
2.1 什么是JWT
JWT(JSON Web Token)是一种基于JSON的开放标准(RFC 7519),用于在各方之间安全地传输信息。JWT由三部分组成:
- Header:包含令牌类型和签名算法
- Payload:包含声明(claims),如用户ID、角色等
- Signature:使用密钥对前两部分进行签名,确保令牌完整性
2.2 JWT工作流程
- 用户登录,提供用户名和密码
- 服务器验证用户身份
- 服务器生成JWT令牌并返回给客户端
- 客户端存储JWT令牌
- 客户端在后续请求中携带JWT令牌
- 服务器验证JWT令牌的有效性
- 服务器处理请求并返回响应
2.3 JWT优缺点
优点:
- 无状态,服务器不需要存储会话状态
- 可扩展,支持分布式系统
- 自包含,令牌中包含所有必要信息
- 跨域支持,适用于前后端分离架构
缺点:
- 令牌无法撤销,除非设置过期时间
- 令牌大小较大,可能增加请求体积
- 签名验证需要计算资源
3. NestJS认证实现
3.1 核心依赖
npm install @nestjs/jwt @nestjs/passport passport passport-local passport-jwt bcrypt class-validator class-transformer3.2 认证模块结构
src/
├── auth/
│ ├── dto/
│ │ ├── login.dto.ts
│ │ ├── register.dto.ts
│ │ └── refresh-token.dto.ts
│ ├── guards/
│ │ ├── jwt-auth.guard.ts
│ │ └── roles.guard.ts
│ ├── strategies/
│ │ └── jwt.strategy.ts
│ ├── auth.controller.ts
│ ├── auth.service.ts
│ └── auth.module.ts
├── users/
│ ├── entities/
│ │ └── user.entity.ts
│ ├── users.service.ts
│ └── users.module.ts
└── app.module.ts3.3 用户实体
// src/users/entities/user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
export enum UserRole {
USER = 'user',
ADMIN = 'admin',
}
@Entity()
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
name: string;
@Column({ unique: true })
email: string;
@Column()
password: string;
@Column({ type: 'enum', enum: UserRole, default: UserRole.USER })
role: UserRole;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}3.4 认证DTOs
// src/auth/dto/register.dto.ts
import { IsString, IsEmail, MinLength } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class RegisterDto {
@ApiProperty({ description: 'User name', example: 'John Doe' })
@IsString()
name: string;
@ApiProperty({ description: 'User email', example: 'john@example.com' })
@IsEmail()
email: string;
@ApiProperty({ description: 'User password', example: 'password123', minLength: 6 })
@IsString()
@MinLength(6)
password: string;
}
// src/auth/dto/login.dto.ts
import { IsEmail, IsString } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class LoginDto {
@ApiProperty({ description: 'User email', example: 'john@example.com' })
@IsEmail()
email: string;
@ApiProperty({ description: 'User password', example: 'password123' })
@IsString()
password: string;
}
// src/auth/dto/refresh-token.dto.ts
import { IsString } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class RefreshTokenDto {
@ApiProperty({ description: 'Refresh token', example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' })
@IsString()
refreshToken: string;
}3.5 JWT策略
// src/auth/strategies/jwt.strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { UsersService } from '../../users/users.service';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
private usersService: UsersService,
private configService: ConfigService,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get('JWT_SECRET'),
});
}
async validate(payload: any) {
const user = await this.usersService.findOne(payload.sub);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}3.6 认证守卫
// src/auth/guards/jwt-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
// src/auth/guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { UserRole } from '../../users/entities/user.entity';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<UserRole[]>('roles', [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user;
const hasRole = requiredRoles.some((role) => user.role === role);
if (!hasRole) {
throw new ForbiddenException('Insufficient permissions');
}
return true;
}
}3.7 角色装饰器
// src/auth/decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
import { UserRole } from '../../users/entities/user.entity';
export const Roles = (...roles: UserRole[]) => SetMetadata('roles', roles);4. 认证服务实现
4.1 用户服务
// src/users/users.service.ts
import { Injectable, NotFoundException, ConflictException } 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(name: string, email: string, password: string) {
// Check if user already exists
const existingUser = await this.usersRepository.findOne({ where: { email } });
if (existingUser) {
throw new ConflictException('Email already in use');
}
// Hash password
const hashedPassword = await bcrypt.hash(password, 10);
const user = this.usersRepository.create({
name,
email,
password: hashedPassword,
});
return this.usersRepository.save(user);
}
async findOne(id: string) {
const user = await this.usersRepository.findOne({ where: { id } });
if (!user) {
throw new NotFoundException(`User with ID ${id} not found`);
}
return user;
}
async findByEmail(email: string) {
return this.usersRepository.findOne({ where: { email } });
}
async validatePassword(user: User, password: string): Promise<boolean> {
return bcrypt.compare(password, user.password);
}
}4.2 认证服务
// src/auth/auth.service.ts
import { Injectable, UnauthorizedException, ConflictException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../users/users.service';
import { RegisterDto, LoginDto } from './dto';
import { User } from '../users/entities/user.entity';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService,
private configService: ConfigService,
) {}
async register(registerDto: RegisterDto) {
const { name, email, password } = registerDto;
const user = await this.usersService.create(name, email, password);
const tokens = await this.generateTokens(user);
return { user, ...tokens };
}
async login(loginDto: LoginDto) {
const { email, password } = loginDto;
const user = await this.usersService.findByEmail(email);
if (!user) {
throw new UnauthorizedException('Invalid email or password');
}
const isValidPassword = await this.usersService.validatePassword(user, password);
if (!isValidPassword) {
throw new UnauthorizedException('Invalid email or password');
}
const tokens = await this.generateTokens(user);
return { user, ...tokens };
}
async refreshToken(refreshToken: string) {
try {
const payload = this.jwtService.verify(refreshToken, {
secret: this.configService.get('JWT_REFRESH_SECRET'),
});
const user = await this.usersService.findOne(payload.sub);
if (!user) {
throw new UnauthorizedException('Invalid refresh token');
}
const tokens = await this.generateTokens(user);
return { user, ...tokens };
} catch (error) {
throw new UnauthorizedException('Invalid refresh token');
}
}
private async generateTokens(user: User) {
const payload = { sub: user.id, email: user.email, role: user.role };
const accessToken = this.jwtService.sign(payload, {
secret: this.configService.get('JWT_SECRET'),
expiresIn: this.configService.get('JWT_ACCESS_EXPIRATION'),
});
const refreshToken = this.jwtService.sign(payload, {
secret: this.configService.get('JWT_REFRESH_SECRET'),
expiresIn: this.configService.get('JWT_REFRESH_EXPIRATION'),
});
return { accessToken, refreshToken };
}
}4.3 认证控制器
// src/auth/auth.controller.ts
import { Controller, Post, Body, UseGuards, Get, Request } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiBody } from '@nestjs/swagger';
import { AuthService } from './auth.service';
import { RegisterDto, LoginDto, RefreshTokenDto } from './dto';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { Roles } from './decorators/roles.decorator';
import { RolesGuard } from './guards/roles.guard';
import { UserRole } from '../users/entities/user.entity';
@ApiTags('auth')
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@ApiOperation({ summary: 'Register a new user' })
@ApiResponse({ status: 201, description: 'User registered successfully' })
@ApiResponse({ status: 400, description: 'Invalid input' })
@ApiResponse({ status: 409, description: 'Email already in use' })
@Post('register')
register(@Body() registerDto: RegisterDto) {
return this.authService.register(registerDto);
}
@ApiOperation({ summary: 'Login user' })
@ApiResponse({ status: 200, description: 'Login successful' })
@ApiResponse({ status: 401, description: 'Invalid email or password' })
@Post('login')
login(@Body() loginDto: LoginDto) {
return this.authService.login(loginDto);
}
@ApiOperation({ summary: 'Refresh access token' })
@ApiResponse({ status: 200, description: 'Token refreshed successfully' })
@ApiResponse({ status: 401, description: 'Invalid refresh token' })
@Post('refresh')
refreshToken(@Body() refreshTokenDto: RefreshTokenDto) {
return this.authService.refreshToken(refreshTokenDto.refreshToken);
}
@ApiOperation({ summary: 'Get current user' })
@ApiResponse({ status: 200, description: 'Return current user' })
@ApiResponse({ status: 401, description: 'Unauthorized' })
@UseGuards(JwtAuthGuard)
@Get('me')
getCurrentUser(@Request() req) {
return req.user;
}
@ApiOperation({ summary: 'Admin only route' })
@ApiResponse({ status: 200, description: 'Access granted' })
@ApiResponse({ status: 401, description: 'Unauthorized' })
@ApiResponse({ status: 403, description: 'Forbidden' })
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.ADMIN)
@Get('admin')
adminRoute() {
return { message: 'Admin access granted' };
}
}4.4 模块配置
// src/users/users.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
import { UsersService } from './users.service';
@Module({
imports: [TypeOrmModule.forFeature([User])],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
// src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { UsersModule } from '../users/users.module';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { JwtStrategy } from './strategies/jwt.strategy';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
UsersModule,
PassportModule,
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
secret: configService.get('JWT_SECRET'),
signOptions: {
expiresIn: configService.get('JWT_ACCESS_EXPIRATION'),
},
}),
inject: [ConfigService],
}),
],
providers: [AuthService, JwtStrategy],
controllers: [AuthController],
exports: [AuthService],
})
export class AuthModule {}
// src/app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config';
import { UsersModule } from './users/users.module';
import { AuthModule } from './auth/auth.module';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: '.env',
}),
TypeOrmModule.forRoot({
type: 'postgres',
host: process.env.DATABASE_HOST,
port: parseInt(process.env.DATABASE_PORT),
username: process.env.DATABASE_USERNAME,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME,
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: true,
}),
UsersModule,
AuthModule,
],
})
export class AppModule {}5. 环境配置
5.1 环境变量
# .env
# Database
DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_USERNAME=postgres
DATABASE_PASSWORD=postgres
DATABASE_NAME=nestjs-auth
# JWT
JWT_SECRET=your-secret-key
JWT_REFRESH_SECRET=your-refresh-secret-key
JWT_ACCESS_EXPIRATION=15m
JWT_REFRESH_EXPIRATION=7d
# Server
PORT=3000
NODE_ENV=development5.2 配置验证
// src/app.module.ts (updated)
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config';
import * as Joi from 'joi';
import { UsersModule } from './users/users.module';
import { AuthModule } from './auth/auth.module';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: '.env',
validationSchema: Joi.object({
DATABASE_HOST: Joi.string().required(),
DATABASE_PORT: Joi.number().required(),
DATABASE_USERNAME: Joi.string().required(),
DATABASE_PASSWORD: Joi.string().required(),
DATABASE_NAME: Joi.string().required(),
JWT_SECRET: Joi.string().required(),
JWT_REFRESH_SECRET: Joi.string().required(),
JWT_ACCESS_EXPIRATION: Joi.string().required(),
JWT_REFRESH_EXPIRATION: Joi.string().required(),
PORT: Joi.number().default(3000),
NODE_ENV: Joi.string().default('development'),
}),
}),
// ... other imports
],
})
export class AppModule {}6. 密码重置功能
6.1 密码重置DTOs
// src/auth/dto/forgot-password.dto.ts
import { IsEmail } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class ForgotPasswordDto {
@ApiProperty({ description: 'User email', example: 'john@example.com' })
@IsEmail()
email: string;
}
// src/auth/dto/reset-password.dto.ts
import { IsString, MinLength } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class ResetPasswordDto {
@ApiProperty({ description: 'Reset token', example: 'your-reset-token' })
@IsString()
token: string;
@ApiProperty({ description: 'New password', example: 'new-password123', minLength: 6 })
@IsString()
@MinLength(6)
password: string;
}6.2 密码重置服务
// src/auth/auth.service.ts (updated)
import { Injectable, UnauthorizedException, ConflictException, NotFoundException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../users/users.service';
import { RegisterDto, LoginDto, ForgotPasswordDto, ResetPasswordDto } from './dto';
import { User } from '../users/entities/user.entity';
import { ConfigService } from '@nestjs/config';
import * as bcrypt from 'bcrypt';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
// Add password reset entity
import { PasswordReset } from './entities/password-reset.entity';
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService,
private configService: ConfigService,
@InjectRepository(PasswordReset) private passwordResetRepository: Repository<PasswordReset>,
) {}
// Existing methods...
async forgotPassword(forgotPasswordDto: ForgotPasswordDto) {
const { email } = forgotPasswordDto;
const user = await this.usersService.findByEmail(email);
if (!user) {
throw new NotFoundException('User not found');
}
// Generate reset token
const resetToken = this.jwtService.sign(
{ sub: user.id },
{
secret: this.configService.get('JWT_SECRET'),
expiresIn: '1h',
},
);
// Save reset token to database
const passwordReset = this.passwordResetRepository.create({
user,
token: resetToken,
expiresAt: new Date(Date.now() + 60 * 60 * 1000), // 1 hour
});
await this.passwordResetRepository.save(passwordReset);
// In a real application, send an email with the reset link
// For example: `http://localhost:3000/auth/reset-password?token=${resetToken}`
return { message: 'Password reset link sent to your email' };
}
async resetPassword(resetPasswordDto: ResetPasswordDto) {
const { token, password } = resetPasswordDto;
// Find password reset record
const passwordReset = await this.passwordResetRepository.findOne({
where: { token },
relations: ['user'],
});
if (!passwordReset) {
throw new UnauthorizedException('Invalid or expired reset token');
}
if (passwordReset.expiresAt < new Date()) {
throw new UnauthorizedException('Reset token has expired');
}
// Hash new password
const hashedPassword = await bcrypt.hash(password, 10);
// Update user password
const user = passwordReset.user;
user.password = hashedPassword;
await this.usersService.update(user);
// Delete password reset record
await this.passwordResetRepository.delete(passwordReset.id);
return { message: 'Password reset successfully' };
}
}6.3 密码重置控制器
// src/auth/auth.controller.ts (updated)
import { Controller, Post, Body, UseGuards, Get, Request } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiBody } from '@nestjs/swagger';
import { AuthService } from './auth.service';
import { RegisterDto, LoginDto, RefreshTokenDto, ForgotPasswordDto, ResetPasswordDto } from './dto';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { Roles } from './decorators/roles.decorator';
import { RolesGuard } from './guards/roles.guard';
import { UserRole } from '../users/entities/user.entity';
@ApiTags('auth')
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
// Existing methods...
@ApiOperation({ summary: 'Request password reset' })
@ApiResponse({ status: 200, description: 'Password reset link sent' })
@ApiResponse({ status: 404, description: 'User not found' })
@Post('forgot-password')
forgotPassword(@Body() forgotPasswordDto: ForgotPasswordDto) {
return this.authService.forgotPassword(forgotPasswordDto);
}
@ApiOperation({ summary: 'Reset password' })
@ApiResponse({ status: 200, description: 'Password reset successfully' })
@ApiResponse({ status: 401, description: 'Invalid or expired reset token' })
@Post('reset-password')
resetPassword(@Body() resetPasswordDto: ResetPasswordDto) {
return this.authService.resetPassword(resetPasswordDto);
}
}6.4 密码重置实体
// src/auth/entities/password-reset.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, ManyToOne, CreateDateColumn } from 'typeorm';
import { User } from '../../users/entities/user.entity';
@Entity()
export class PasswordReset {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
token: string;
@Column()
expiresAt: Date;
@ManyToOne(() => User, (user) => user.id, { onDelete: 'CASCADE' })
user: User;
@CreateDateColumn()
createdAt: Date;
}6.5 更新用户服务
// src/users/users.service.ts (updated)
import { Injectable, NotFoundException, ConflictException } 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>,
) {}
// Existing methods...
async update(user: User) {
return this.usersRepository.save(user);
}
}7. 实战案例
7.1 完整项目结构
src/
├── auth/
│ ├── dto/
│ │ ├── register.dto.ts
│ │ ├── login.dto.ts
│ │ ├── refresh-token.dto.ts
│ │ ├── forgot-password.dto.ts
│ │ └── reset-password.dto.ts
│ ├── entities/
│ │ └── password-reset.entity.ts
│ ├── guards/
│ │ ├── jwt-auth.guard.ts
│ │ └── roles.guard.ts
│ ├── strategies/
│ │ └── jwt.strategy.ts
│ ├── decorators/
│ │ └── roles.decorator.ts
│ ├── auth.controller.ts
│ ├── auth.service.ts
│ └── auth.module.ts
├── users/
│ ├── entities/
│ │ └── user.entity.ts
│ ├── users.service.ts
│ └── users.module.ts
├── app.module.ts
├── main.ts
└── .env7.2 API测试
7.2.1 注册用户
# POST /auth/register
curl -X POST http://localhost:3000/auth/register \
-H "Content-Type: application/json" \
-d '{
"name": "John Doe",
"email": "john@example.com",
"password": "password123"
}'7.2.2 用户登录
# POST /auth/login
curl -X POST http://localhost:3000/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "john@example.com",
"password": "password123"
}'7.2.3 获取当前用户
# GET /auth/me
curl -X GET http://localhost:3000/auth/me \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"7.2.4 刷新令牌
# POST /auth/refresh
curl -X POST http://localhost:3000/auth/refresh \
-H "Content-Type: application/json" \
-d '{
"refreshToken": "YOUR_REFRESH_TOKEN"
}'7.2.5 请求密码重置
# POST /auth/forgot-password
curl -X POST http://localhost:3000/auth/forgot-password \
-H "Content-Type: application/json" \
-d '{
"email": "john@example.com"
}'7.2.6 重置密码
# POST /auth/reset-password
curl -X POST http://localhost:3000/auth/reset-password \
-H "Content-Type: application/json" \
-d '{
"token": "YOUR_RESET_TOKEN",
"password": "new-password123"
}'7.3 前端集成
7.3.1 React示例
// src/api/auth.js
import axios from 'axios';
const API_URL = 'http://localhost:3000/auth';
const authApi = {
register: async (userData) => {
const response = await axios.post(`${API_URL}/register`, userData);
if (response.data.accessToken) {
localStorage.setItem('user', JSON.stringify(response.data));
}
return response.data;
},
login: async (userData) => {
const response = await axios.post(`${API_URL}/login`, userData);
if (response.data.accessToken) {
localStorage.setItem('user', JSON.stringify(response.data));
}
return response.data;
},
logout: () => {
localStorage.removeItem('user');
},
getCurrentUser: () => {
return JSON.parse(localStorage.getItem('user'));
},
refreshToken: async (refreshToken) => {
const response = await axios.post(`${API_URL}/refresh`, { refreshToken });
if (response.data.accessToken) {
const currentUser = JSON.parse(localStorage.getItem('user'));
localStorage.setItem('user', JSON.stringify({ ...currentUser, ...response.data }));
}
return response.data;
},
forgotPassword: async (email) => {
return axios.post(`${API_URL}/forgot-password`, { email });
},
resetPassword: async (token, password) => {
return axios.post(`${API_URL}/reset-password`, { token, password });
},
};
export default authApi;
// src/components/Login.jsx
import React, { useState } from 'react';
import authApi from '../api/auth';
const Login = () => {
const [formData, setFormData] = useState({
email: '',
password: '',
});
const [error, setError] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
try {
setError('');
await authApi.login(formData);
// Redirect to dashboard
window.location.href = '/dashboard';
} catch (err) {
setError(err.response?.data?.message || 'Login failed');
}
};
return (
<div>
<h2>Login</h2>
{error && <div style={{ color: 'red' }}>{error}</div>}
<form onSubmit={handleSubmit}>
<div>
<label>Email:</label>
<input
type="email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
required
/>
</div>
<div>
<label>Password:</label>
<input
type="password"
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
required
/>
</div>
<button type="submit">Login</button>
</form>
</div>
);
};
export default Login;8. 安全最佳实践
8.1 密码安全
- 使用强哈希算法:使用bcrypt、Argon2等算法
- 添加盐值:防止彩虹表攻击
- 设置最小密码长度:至少6-8个字符
- 密码复杂度要求:包含大小写字母、数字和特殊字符
- 密码尝试限制:防止暴力破解
8.2 令牌安全
- 使用安全的密钥:使用足够长度的随机密钥
- 设置合理的过期时间:访问令牌短,刷新令牌长
- 使用HTTPS:保护令牌传输
- 令牌撤销机制:实现令牌黑名单
- 令牌验证:验证签名和过期时间
8.3 API安全
- 使用HTTPS:加密传输
- 输入验证:验证所有用户输入
- 输出编码:防止XSS攻击
- CORS配置:限制跨域请求
- 速率限制:防止暴力攻击
- 日志记录:记录认证和授权事件
8.4 数据库安全
- 密码哈希存储:不存储明文密码
- 参数化查询:防止SQL注入
- 最小权限原则:数据库用户只拥有必要权限
- 定期备份:防止数据丢失
- 加密敏感数据:加密存储敏感信息
9. 总结
本教程详细介绍了如何在NestJS中实现完整的认证系统,包括用户注册、登录、JWT令牌管理、刷新令牌、基于角色的授权和密码重置功能。通过实战案例,我们学习了如何构建安全、可靠的认证系统,保护API资源不被未授权访问。
认证系统是现代Web应用的重要组成部分,它确保了只有授权用户才能访问受保护的资源。在实现认证系统时,我们需要考虑安全性、可靠性和用户体验等多个方面。
通过本教程的学习,你应该能够:
- 理解认证和授权的基本概念
- 掌握JWT认证的原理和实现方法
- 在NestJS中实现完整的认证系统
- 处理密码重置等常见认证场景
- 遵循认证系统的安全最佳实践
希望本教程对你构建安全、可靠的NestJS应用有所帮助!
10. 互动问答
以下哪个不是认证系统的组件?
A. 用户存储
B. 认证机制
C. 会话管理
D. 前端框架JWT令牌由哪三部分组成?
A. Header、Payload、Signature
B. Header、Body、Footer
C. Token、Secret、Expiration
D. User、Password、Role以下哪个是基于角色的授权实现?
A. JWT认证
B. RolesGuard
C. Passport策略
D. 密码哈希密码重置流程中,以下哪个步骤是正确的?
A. 直接发送新密码到用户邮箱
B. 生成一次性重置令牌并发送重置链接
C. 要求用户提供旧密码
D. 不需要验证用户身份以下哪个不是认证系统的安全最佳实践?
A. 使用HTTPS
B. 存储明文密码
C. 设置令牌过期时间
D. 实现速率限制
答案:
- D
- A
- B
- B
- B