创建AuthService
新建 src/auth/auth.service.ts 文件,编写 AuthService 类来提供服务,主要实现:
validateUser:查询数据库验证用户的账号和密码是否正确,返回用户信息createToken:生成Tokenlogin:拼接Token给到前端
typescript
// src/auth/auth.service.ts
import { Injectable, Logger, Req, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from 'src/user/entities/user.entity';
import { removeUserData } from 'src/utils';
import { Repository } from 'typeorm';
@Injectable()
export class AuthService {
private logger = new Logger('AuthService');
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
private readonly jwtService: JwtService,
) {}
async validateUser(account: string, password: string): Promise<User | null> {
// 查询数据库来验证用户
const user: User = await this.userRepository.findOne({
where: { account, isDeleted: 0 },
});
// 如果有用户再验证密码
if (
user &&
user.account === account &&
(await user.validatePassword(password))
) {
delete user.passwordHash;
delete user.isDeleted;
return user;
}
return null;
}
// 生成 Token
async createToken(data) {
return await this.jwtService.signAsync(data);
}
async login(user: any) {
const payload = {
account: user.account,
userId: user.id,
roleId: user.roleId,
roleType: user.roleType,
roleWeight: user.roleWeight,
};
const token = await this.createToken(payload);
return {
token,
};
}
// 查询当前账户信息
async queryCurrentUser(@Req() req) {
try {
if (req?.user?.userId) {
const user = await this.userRepository.findOneBy({
id: req.user.userId,
});
return removeUserData(user);
}
} catch (error) {
this.logger.error('@@@@ 当前登录信息过期,请重新登录:', error);
throw new UnauthorizedException('当前登录信息过期,请重新登录');
}
}
}
新建AuthController
新建 src/auth/auth.controller.ts 文件,编写 AuthController 类给前端提供接口,主要实现:
login:帐号登录,调用之前写的服务先进行用户账号密码验证,然后进行 token 获取,最后拼接信息给到前端logout:帐号登出,也就是把 cookie 中的 token 设置过期就行查询当前账户信息:通过token查询当前登录账户信息
less
import {
Controller,
Post,
Body,
BadRequestException,
Req,
Response,
} from '@nestjs/common';
import { ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { AuthService } from './auth.service';
import { LoginDto } from './dto/login.dto';
@Controller('auth')
@ApiTags('身份验证')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('login')
@ApiOperation({
summary: '帐号登录',
description: '根据帐号和密码登录',
})
@ApiBody({ type: LoginDto })
@ApiResponse({ status: 200, description: '帐号登录成功' })
async login(@Body() loginDto: LoginDto) {
const user = await this.authService.validateUser(
loginDto.account,
loginDto.password,
);
if (!user) {
throw new BadRequestException('帐号或密码不存在');
}
try {
return await this.authService.login(user);
} catch (err) {
throw new BadRequestException('登录失败');
}
}
@Post('logout')
@ApiOperation({
summary: '帐号登出',
description: '帐号登出',
})
@ApiResponse({ status: 200, description: '帐号登出成功' })
logout(@Req() req, @Response() res): void {
// 清除cookie中的jwt
res.cookie('jwt', '', { httpOnly: true, expires: new Date(0) });
// 如果是localStorage存的token需要前端自己删除
res.status(200).send({
code: 200,
msg: 'success',
result: { message: '登出成功' },
});
}
@Get('queryCurrentUser')
@ApiOperation({
summary: '查询当前账户信息',
description: '通过token查询当前登录账户信息',
})
@ApiResponse({ status: 200, description: '账户信息查询成功' })
queryCurrentUser(@Req() req) {
return this.authService.queryCurrentUser(req);
}
}
新建LoginDto
新建 src/auth/dto/login.dto.ts 文件:
less
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString, IsEmail, Matches } from 'class-validator';
export class LoginDto {
@IsEmail({}, { message: '账号必须是邮箱格式' })
@IsString({ message: '账号必须是字符串' })
@IsNotEmpty({ message: '账号不能为空' })
@ApiProperty({
description: '账号(邮箱格式)',
example: 'niunai@niunai.com',
})
account: string;
@Matches(/^(?=.*[a-zA-Z])(?=.*\d).{8,16}$/, {
message: '请输入8-16位数字+字母的密码',
})
@IsString({ message: '密码必须是字符串' })
@IsNotEmpty({ message: '密码不能为空' })
@ApiProperty({ description: '密码', example: 'admin123' })
password: string;
}
新建AuthModule
新建 src/auth/auth.module.ts 文件:
typescript
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { JwtStrategy } from './jwt.strategy';
import { AuthController } from './auth.controller';
import { User } from 'src/user/entities/user.entity';
import { TypeOrmModule } from '@nestjs/typeorm';
import { JwtAuthGuard } from './jwt-auth.guard';
const jwtModule = JwtModule.register({
global: true,
secret: 'niunai', // 在生产环境中使用更安全的密钥管理方式
signOptions: { expiresIn: '24h' }, // 设置 token 的过期时间
});
@Module({
imports: [TypeOrmModule.forFeature([User]), PassportModule, jwtModule],
controllers: [AuthController],
providers: [AuthService, JwtStrategy, JwtAuthGuard],
exports: [AuthService, JwtAuthGuard], // 如果需要在其他模块中使用 AuthService
})
export class AuthModule {}
配置JWT策略
新建 src/auth/jwt.strategy.ts 文件,确保JWT策略使用 AuthService 来验证用户
typescript
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: 'niunai', // 与 JwtModule.register 中的 secret 匹配
});
}
async validate(payload: any) {
// 这里返回的数据会被注入到 @Req.user 对象内
return {
userId: payload.userId,
account: payload.account,
roleId: payload.roleId,
roleType: payload.roleType,
roleWeight: payload.roleWeight,
};
}
}
新建JwtAuthGuard路由守卫
新建 src/auth/jwt-auth.guard.ts 文件,可以用于全局、某个controller、某个接口进行jwt校验
typescript
import {
Injectable,
ExecutionContext,
Logger,
UnauthorizedException,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { JwtService } from '@nestjs/jwt';
import { excludedRoutes } from './excluded.routes';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
private logger = new Logger('JwtAuthGuard');
constructor(private readonly jwtService: JwtService) {
super();
}
async canActivate(context: ExecutionContext): Promise<any> {
const request = context.switchToHttp().getRequest();
const { path, method } = request;
// 检查当前请求是否在排除列表中
const isExcluded = excludedRoutes.some(
(route) => route.path === path && route.method === method,
);
if (isExcluded) {
return true; // 如果请求被排除,则不进行 JWT 验证
}
const token = request.get('Authorization');
this.logger.log('@@@@ token:', token);
if (!token) {
throw new UnauthorizedException('没有token,请先登录');
}
// Bearer token 格式
const bearerToken = token.split(' ');
if (bearerToken.length < 2 || bearerToken[0] !== 'Bearer') {
throw new UnauthorizedException('token格式或类型不正确');
}
try {
const decoded = await this.jwtService.verifyAsync(bearerToken[1]);
// 令牌验证成功
if (decoded) {
request.user = decoded;
return this.activate(context);
}
} catch (error) {
throw new UnauthorizedException('没有权限,请登录');
}
}
async activate(context: ExecutionContext): Promise<boolean> {
return super.canActivate(context) as Promise<boolean>;
}
}
其中 excludedRoutes 就是一个排除路由守卫的数组:
ini
export const excludedRoutes = [{ path: '/auth/login', method: 'POST' }];
全局配置JWT路由守卫
我这边全局配置了 JWT 路由守卫,其实也可以某个 controller、某个接口进行 jwt 验证,但我的应用比较小,也不会有多个 jwt,就简单点使用了全局配置,至于排除就在路由里增加了一个 excludedRoutes 去排除就行了。
csharp
// ...其他代码
import { JwtAuthGuard } from './auth/jwt-auth.guard';
async function bootstrap() {
// ...其他代码
app.useGlobalGuards(app.get(JwtAuthGuard));
await app.listen(8004);
}
bootstrap();
权限管理功能接口完成
请求响应加密封装
后端 NestJS 使用 crypto 来实现数据加解密,使用 RSA 非对称加密算法和 AES 的对称加密算法进行混合加密,RSA 公钥对 AES 密钥进行加密,AES 对数据进行加密。
同时前端对接口的请求方法进行封装,提供 url 、params、config 参数给到调用方。
后端加密流程:
生成 RSA 密钥对:使用 OpenSSL 或其他工具生成 RSA 公钥和私钥。加密 AES 密钥:使用 RSA 公钥加密 AES 密钥。加密数据:使用 AES 对称加密算法加密实际的数据。发送加密信息:将加密后的 AES 密钥和加密后的数据发送给前端。
前端请求封装:
获取公钥:前端需要从后端获取 RSA 公钥。加密AES密钥:前端使用获取到的公钥加密 AES 密钥。加密数据:前端使用加密后的 AES 密钥加密请求数据。发送请求:前端将加密后的请求数据发送到后端。
目前这块我还没有做,后面有空补上
安全性
前后端都要做一些基础的安全性校验和拦截:
- 数据验证 :对输入的数据进行必要的格式、类型和长度校验,避免
SQL注入、XSS等攻击。 - 防范跨站脚本攻击 (XSS) :确保用户生成的内容在输出前进行转义处理,
NestJS内置的管道可以帮助自动转义输出。 - 防止跨站请求伪造 (CSRF) :使用
CSRF保护模块,为每个请求生成唯一的令牌,并将其与用户会话关联。
公共模块开发
新建 common 文件夹作为公共的模块
Excel文件导出
在 common 目录下新建 excel目录来存放控制器、服务和实例,用于给其他模块调用excel的导出能力
新建 excel/excel.service.ts:
kotlin
import * as XLSX from 'xlsx';
import { Injectable } from '@nestjs/common';
@Injectable()
export class ExcelService {
/**
* 将数据列表导出为 Excel 文件
* @param data 数据列表
* @param fileName 文件名
* @returns 文件缓冲区
*/
exportAsExcelFile(data: any[]): Buffer {
const worksheet: XLSX.WorkSheet = XLSX.utils.json_to_sheet(data);
const workbook: XLSX.WorkBook = {
Sheets: { data: worksheet },
SheetNames: ['data'],
};
const excelBuffer: any = XLSX.write(workbook, {
bookType: 'xlsx',
type: 'buffer',
});
return excelBuffer;
}
}
新建 excel/excel.controller.ts:
less
import {
Controller,
Res,
HttpStatus,
Body,
Post,
BadRequestException,
} from '@nestjs/common';
import { ExcelService } from './excel.service';
import { ExcelDto } from './dto/excel.dto';
import { ApiBody, ApiTags } from '@nestjs/swagger';
@Controller('export')
@ApiTags('公共Excel导出')
export class ExcelController {
constructor(private readonly excelService: ExcelService) {}
@Post('/exportExcel')
@ApiBody({ type: ExcelDto })
exportExcel(@Body() body: ExcelDto, @Res() res) {
try {
// 导出为 Excel 文件
const buffer = this.excelService.exportAsExcelFile(body.data);
// 设置响应头
res.setHeader(
'Content-Type',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
);
res.setHeader(
'Content-Disposition',
`attachment; filename=${encodeURIComponent(body.filename)}`,
);
// 发送文件
res.status(HttpStatus.OK).send(buffer);
} catch {
throw new BadRequestException('Excel公共导出接口调用失败');
}
}
}
新建 excel/excel.module.ts:
python
import { Module } from '@nestjs/common';
import { ExcelController } from './excel.controller';
import { ExcelService } from './excel.service';
@Module({
controllers: [ExcelController],
providers: [ExcelService],
exports: [ExcelService],
})
export class ExcelModule {}
新建实例 excel/dto/excel.dto.ts:
less
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty } from 'class-validator';
export class ExcelDto {
@ApiProperty({
description: 'excel名称',
example: '用户表',
})
@IsNotEmpty({ message: 'filename必填' })
filename: string;
@ApiProperty({
description: 'excel数据',
example: [
{ id: 1, name: 'Alice', age: 25 },
{ id: 2, name: 'Bob', age: 30 },
{ id: 3, name: 'Charlie', age: 35 },
],
})
@IsNotEmpty({ message: 'data必填' })
data: any[];
}
工具函数开发
新建 src/utils/index.ts 文件,用于在后续业务开发中对一些可复用的地方进行封装,目前主要是对一些表的用户信息设置进行封装:
ini
import * as moment from 'moment';
// 设置创建用户信息
export const setCreatedUser = (req: any, table: any) => {
const user = req?.user;
table.createdBy = user?.userId;
table.createdByAccount = user?.account;
table.updatedBy = user?.userId;
table.updatedByAccount = user?.account;
return table;
};
// 设置更新用户信息
export const setUpdatedUser = (req: any, table: any) => {
const user = req?.user;
table.updatedBy = user?.userId;
table.updatedByAccount = user?.account;
return table;
};
// 设置删除用户信息
export const setDeletedUser = (req: any, table: any) => {
const user = req?.user;
table.updatedBy = user?.userId;
table.updatedByAccount = user?.account;
table.isDeleted = 1;
return table;
};
// 移除一些非必要的数据
export const removeUnnecessaryData = (data: any) => {
return data.map((item) => {
const obj = {
...item,
createdTime: item.createdTime
? moment(item.createdTime).format('YYYY-MM-DD HH:mm:ss')
: '',
updatedTime: item.updatedTime
? moment(item.updatedTime).format('YYYY-MM-DD HH:mm:ss')
: '',
};
delete obj.passwordHash;
delete obj.isDeleted;
return obj;
});
};
// 移除一些用户信息
export const removeUserData = (data: any) => {
const filterData = data;
delete filterData.createdBy;
delete filterData.createdByAccount;
delete filterData.createdTime;
delete filterData.updatedBy;
delete filterData.updatedByAccount;
delete filterData.updatedTime;
delete filterData.passwordHash;
delete filterData.isDeleted;
return filterData;
};