1、概要
系统划分user模块和auth认证模块,user模块只实现数据表的增删改查,不涉及认证的相关业务,auth模块包括注册、登录等接口,接口中使用user提供的新增用户、查询用户服务层接口,两个模块各负其责。
1、用户模块
1.1、新建用户模块
1.2、修改用户实体和dto
//user.entity.ts
import { ApiProperty } from "@nestjs/swagger";
import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity('user')
export class UserEntity {
@ApiProperty({
example: "自动生成",
description: "用户ID",
})
@PrimaryGeneratedColumn({type: 'int'})
id: number;
@ApiProperty({
example: "admin",
description: "用户名",
})
@Column({ type: 'varchar', length: 32, comment: '用户登录账号' })
username: string;
@ApiProperty({
example: "admin123",
description: "密码",
})
@Column({ type: 'varchar', length: 200, nullable: false, comment: '用户登录密码' })
password: string;
@ApiProperty({
example: "sdfsdfsd",
description: "哈希加密的盐",
})
@Column({ type: 'varchar', length: 50, nullable: false, comment: '哈希加密的盐' })
salt: string;
@ApiProperty({
example: "0",
description: "用户类型 0 管理员 1 普通用户",
})
@Column({ type: 'int', comment: '用户类型 0 管理员 1 普通用户', default: 1 })
userType: number;
@ApiProperty({
example: "aa@163.com",
description: "用户邮箱",
})
@Column({ type: 'varchar', comment: '用户邮箱', default: ''})
email: string;
@ApiProperty({
example: "0",
description: "是否冻结用户 0 不冻结 1 冻结",
})
@Column({ type: 'int', comment: '是否冻结用户 0 不冻结 1 冻结', default: 0 })
freezed: number;
@ApiProperty({
example: "",
description: "用户头像(base64编码的图片字符串)",
})
@Column({ type: 'varchar', comment: '用户头像', default: ''})
avatar: string;
@ApiProperty({
example: "一些备注信息",
description: "用户备注",
})
@Column({ type: 'varchar', comment: '用户备注', default: ''})
desc: string;
@ApiProperty({
description: "创建时间,自动生成",
})
@CreateDateColumn({ type: 'timestamp', comment: '创建时间' })
createTime: Date
}
//create-user.dto.ts
import { IsString, IsNotEmpty, IsEmail, IsNumber } from 'class-validator'
export class CreateUserDto {
@IsNotEmpty({ message: '账号不能为空' })
@IsString({ message: '账号必须为string类型'})
username: string
@IsNotEmpty({ message: '密码不能为空' })
@IsString({ message: '密码必须为string类型'})
password: string
@IsNotEmpty({ message: '确认密码不能为空' })
confirmPassword: string
@IsNotEmpty({ message: '邮箱不能为空' })
@IsString({ message: '邮箱必须为string类型'})
@IsEmail()
email: string
@IsNotEmpty({ message: '验证码不能为空' })
code: string
}
//update-user.dto.ts
import { IsBoolean, IsEmail, IsNotEmpty, IsNumber, IsOptional, IsString } from "class-validator";
export class UpdateUserDto {
@IsNumber({}, {message: 'id 类型为number'})
@IsNotEmpty({ message: 'id 不能为空' })
id: number;
@IsString({ message: '账号必须为string类型'})
@IsOptional()
username?: string
@IsString({ message: '邮箱必须为string类型'})
@IsEmail()
@IsOptional()
email?: string
@IsNumber({}, { message: '冻结状态必须为number类型'})
@IsOptional()
freezed?: number
@IsOptional()
roleIds?: number[]
}
//user.service.ts
import { Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { UserEntity } from './entities/user.entity';
import { Repository } from 'typeorm';
@Injectable()
export class UserService {
constructor(
private readonly userRepository: Repository<UserEntity>,
) {}
create(createUserDto: CreateUserDto) {
//一些业务规则
return this.userRepository.save(createUserDto);
}
findAll() {
//一些业务规则
return this.userRepository.find();
}
findOne(id: number) {
//一些业务规则
return this.userRepository.findOneBy({ id });
}
update(id: number, updateUserDto: UpdateUserDto) {
//一些业务规则
return this.userRepository.update(id,updateUserDto);
}
remove(id: number) {
//一些业务规则
return this.userRepository.delete({id});
}
}
具体的业务方法待认证模块用到时再补充
2、认证模块
双 Token(Access Token + Refresh Token)验证逻辑的详细实现方案:
1). 用户登录
- 用户提交凭证(如邮箱+密码)。
- 后端验证通过后生成双 Token:
- **Access Token**:短期有效(如 15 分钟),用于 API 请求鉴权。
- **Refresh Token**:长期有效(如 7 天),用于刷新 Access Token。
- 后端存储 Refresh Token(如数据库),并通过 **HttpOnly + Secure Cookie** 返回给前端。
- 前端将 Access Token 存储在内存或 `localStorage` 中。
2). API 请求
- 前端在请求头(`Authorization: Bearer <access_token>`)携带 Access Token。
- 后端验证 Access Token 有效性:
- 有效 → 返回数据。
- 无效 → 返回 `401 Unauthorized`。
3). Access Token 过期处理
- 前端拦截 `401` 错误,发起刷新 Token 请求。
- 后端验证 Refresh Token(从 Cookie 读取):
- 有效 → 生成新 Access Token 和可选的新 Refresh Token,返回新 Access Token。
- 无效 → 返回 `401`,前端跳转登录页。
4). 主动登出
- 前端清除本地 Access Token。
- 后端删除或标记 Refresh Token 失效。
代码实现:
2.1、准备工作
2.1.1、环境变量
.env文件中增加生成和验证token的密钥以及超时时间,accessToken和refreshToken两对配置,其中密钥使用一些在线工具随机生成:
//.env
# jwt
JWT_SECRET=jXFECnYwcsc8S06TAgCPrqicJGXr6zIAjCv66dtmPvQ=
JWT_EXPIRES_IN=60s
REFRESH_JWT_SECRET=514oCWLgVJNwC3NicDWPiEnZ/hFcheFHIX/ZDKrGXLA=
REFRESH_JWT_EXPIRES_IN=7d
2.1.2、设置配置类,方便使用
jwt.config.ts:注意选项类型是模块级别的,所以后边生成签名时没有明确使用jwt选项。而refresh.config是签名选项,在生成token时具体写明了使用refreshtokenconfig。
import { registerAs } from '@nestjs/config';
import { JwtModuleOptions } from '@nestjs/jwt';
export default registerAs('jwt',():JwtModuleOptions=>({
secret:process.env.JWT_SECRET,
signOptions:{expiresIn:process.env.JWT_EXPIRES_IN},
}))
refresh.config.ts:
import { registerAs } from '@nestjs/config';
import { JwtSignOptions } from '@nestjs/jwt';
export default registerAs("refresh-jwt",():JwtSignOptions=>({
secret:process.env.REFRESH_JWT_SECRET,
expiresIn:process.env.REFRESH_JWT_EXPIRES_IN,
}))
2.2、实现登录业务
2.2.1、创建模块:
nest g mo modules/auth
nest g co modules/auth --no-spec
nest g s modules/auth --no-spec
2.2.2、安装依赖
pnpm install --save @nestjs/jwt
2.2.3、auth.service功能实现
import { Inject, Injectable } from '@nestjs/common';
import { UserService } from '../user/user.service';
import { JwtService } from '@nestjs/jwt';
import { ConfigType } from '@nestjs/config';
import refreshConfig from './config/refresh.config';
import jwtConfig from './config/jwt.config';
@Injectable()
export class AuthService {
constructor(private usersService: UserService,
private jwtService: JwtService,
@Inject(jwtConfig.KEY)
private jwtTokenConfig: ConfigType<typeof jwtConfig>,
@Inject(refreshConfig.KEY)
private refreshTokenConfig: ConfigType<typeof refreshConfig>,
) {}
async signIn(userId:number,username:string): Promise<any> {
//登录前未进行用户的验证是因为后期我们会使用守卫来进行验证
//生成token
const {accessToken,refreshToken}=await this.generateToken(userId);
//返回token和用户信息
return {
id:userId,
username,
accessToken,
refreshToken
}
}
async generateToken(userId: number) {
// sub 是 JWT 标准中的一个字段,用于表示用户的唯一标识符。
const payload = { sub: userId };
//生成双toke,注意生成时第二个参数确定了token的过期时间
const [ accessToken,refreshToken ] =await Promise.all([
this.jwtService.signAsync(payload,this.jwtTokenConfig),
this.jwtService.signAsync(payload,this.refreshTokenConfig)
]);
// 返回生成的 JWT 令牌作为登录结果的一部分,通常用于后续的请求中进行身份验证和授权。
return { accessToken,refreshToken };
}
}
auth.controller.ts
import { Controller, Post,Request } from '@nestjs/common';
import { AuthService } from './auth.service';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post("signin")
async signin(){
//因为还没有使用守卫验证用户信息,请求头中不包含user信息,所以此处测试暂时写死
const req={
user:{
id:1,
name:"admin"
}
}
return await this.authService.signIn(req.user.id,req.user.name)
}
}
auth.module.ts:注意导入JwtModule,UserEntity,UserService,因为用户服务使用了用户实体类,所以需要引入userEntity。
import { Module } from '@nestjs/common';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { UserModule } from '../user/user.module';
import { UserService } from '../user/user.service';
import { JwtModule } from '@nestjs/jwt';
import jwtConfig from './config/jwt.config';
import { ConfigModule } from '@nestjs/config';
import refreshConfig from './config/refresh.config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserEntity } from '../user/entities/user.entity';
@Module({
imports: [
JwtModule.registerAsync(jwtConfig.asProvider()),
ConfigModule.forFeature(jwtConfig),
ConfigModule.forFeature(refreshConfig),
TypeOrmModule.forFeature([UserEntity])
],
controllers: [AuthController],
providers: [AuthService,UserService],
})
export class AuthModule {}
测试: 启动服务端程序,在postman中测试:
2.3、实现刷新token业务
auth.controller.ts增加接口:
@Post("refresh")
refreshToken(){
const req={
user:{
id:1,
username:"admin"
}
}
return this.authService.refreshToken(req.user.id,req.user.username)
}
auth.service.ts中实现token生成,与登录接口逻辑基本一致
async refreshToken(userId:number,username:string) {
const {accessToken,refreshToken}=await this.generateToken(userId);
return {
id:userId,
username,
accessToken,
refreshToken
}
}
测试:
2.4、使用守卫实现用户验证
2.4.1、安装依赖
pnpm i passport-local
2.4.2、创建用户验证策略
在/auth下创建strategies目录,创建一个策略文件local.strategy.ts
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
import { AuthService } from './../auth.service';
import { Injectable } from '@nestjs/common';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private readonly authService: AuthService) {
super({
usernameField: 'username',
passwordField: 'password',
});
}
async validate(username: string, password: string): Promise<any> {
return await this.authService.validateUser(username, password);
}
}
2.4.3、用户验证接口
在auth.service.ts中实现validateUser接口
async validateUser(username: string, pass: string): Promise<any> {
const user = await this.userService.findOneByName(username);
if(!user) throw new UnauthorizedException("用户不存在");
//TODO:下一步优化:使用加密方式验证
const isPasswordMatch =user.password===pass;
if(!isPasswordMatch) throw new UnauthorizedException("Invalid password");
return {id:user.id,email:user.email,name:user.username};
}
2.4.4、模块中引入策略
2.4.5、测试
strategy实现了用户验证功能,并在验证通过后自动把用户信息写到请求头中,这样我们在接口上实现这个策略的守卫之后,系统就使用了策略中的验证用户方法,不用我们手工在user.service登录接口中手工验证用户。测试方法如下:
2.5、实现接口的jwt保护
jwt保护与用户密码验证类似,只是jwt是使用的token验证。 因为刷新token接口验证的refreshToken,与jwt验证基本一致,代码就一块贴出来了。
2.5.1、安装依赖
pnpm i passport-jwt
2.5.2、创建jwt验证策略
auth-jwtPayload.d.ts定义一个payload类型:
@Injectable()
export type AuthJwtPayload={
sub:number,
}
jwt.strategy.ts
import { Injectable,Inject } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { ExtractJwt, Strategy } from "passport-jwt";
import jwtConfig from "../config/jwt.config";
import { ConfigType } from "@nestjs/config";
import { AuthService } from "../auth.service";
import { AuthJwtPayload } from "../types/auth-jwtPayload";
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy){
constructor(
@Inject(jwtConfig.KEY)
private jwtConfiguration:ConfigType<typeof jwtConfig>,
private authService:AuthService, // 注入 AuthService 服务,用于验证 JWT 负载中的用户信息是否合法。
){
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: jwtConfiguration.secret,
ignoreExpiration: false,
})
}
async validate(payload:AuthJwtPayload): Promise<any> {
const userId=payload.sub; //sub 是 JWT 标准中的一个字段,用于表示用户的唯一标识符。
return await this.authService.validateJwtUser(userId); // 验证 JWT 负载中的用户信息是否合法。如果合法,则返回用户信息,否则抛出异常。
}
}
refresh-token.strategy.ts
import { Injectable,Inject } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { ExtractJwt, Strategy } from "passport-jwt";
import { ConfigType } from "@nestjs/config";
import type { AuthJwtPayload } from "../types/auth-jwtPayload";
import { AuthService } from "../auth.service";
import refreshConfig from "../config/refresh.config";
@Injectable()
export class RefreshStrategy extends PassportStrategy(Strategy,'refresh-jwt'){
constructor(
@Inject(refreshConfig.KEY)
private refreshTokenConfig:ConfigType<typeof refreshConfig>,
private authService:AuthService, // 注入 AuthService 服务,用于验证 JWT 负载中的用户信息是否合法。
){
super({
jwtFromRequest: ExtractJwt.fromBodyField("refresh"),//.fromAuthHeaderAsBearerToken(),
secretOrKey: refreshTokenConfig.secret,
ignoreExpiration: false,
})
}
async validate(payload:AuthJwtPayload): Promise<any> {
const userId=payload.sub; //sub 是 JWT 标准中的一个字段,用于表示用户的唯一标识符。
return await this.authService.validateRefreshToken(userId); // 验证 JWT 负载中的用户信息是否合法。如果合法,则返回用户信息,否则抛出异常。
}
}
2.5.3、jwt验证接口
auth.service.ts中增加下面接口:
async validateJwtUser(userId: number) {
const user = await this.userService.findOne(userId);
if(!user){
throw new UnauthorizedException("用户不存在");
}
const currentUser={id:user.id};
return currentUser;
}
async validateRefreshToken(userId: number) {
const user = await this.userService.findOne(userId);
if(!user){
throw new UnauthorizedException("用户不存在");
}
const currentUser={id:user.id};
return currentUser;
}
2.5.4、模块中引入策略
2.5.5、测试
在auth.controller中增加一个测试接口,受jwt保护:
@UseGuards(AuthGuard("jwt"))
@Get("protected")
getAll(@Request() req){
console.log('auth.controller--------getAll', req.user);
return {
message:`Now you can access protected API,this is your user id:${req.user.id}`
}
}
2.6、其他技巧
2.6.1、正确使用守卫
上面代码中我们在controller接口上使用了路由守卫,在守卫中验证本地策略(用户名+密码)、jwt策略(jwtToken),refresh策略(refreshToken),但是有时候会自定义一些其他的守卫,多个守卫在一个路由接口上是有先后执行顺序的,如下: 写法1:guard是从下往上执行的。
@UseGuards(AuthGuard("local"))
@UseGuards(AdminGuard)
@Post("signin")
async signin(@Request() req){
}
假设AdminGuard是一个自定义守卫,用于验证角色,但是因为在AuthGuard("local")之前运行,导致角色验证时request中还不存在user信息。 写法2:从前往后执行
@UseGuards(AdminGuard,AuthGuard("local"))
@Post("signin")
async signin(@Request() req){
}
2.6.2、三种jwt策略
我们上边演示的都是路由守卫,jwt守卫也可以加在controller上,也可以配置全局守卫。
全局守卫有两个地方可以配置,在main.ts中:
在app.module中:
与在main.ts中启用全局守卫相比,此方法可以自动注册守卫中用到的一些其他依赖,如依赖userService,所以更推荐使用这种方式。
2.6.3、无token接口的处理方法
如果开启了全局守卫或controller守卫,如登录时获取验证码是不需要token验证的,那怎么办呢?可以自定义一个不需要jwt认证的注解,并在jwt守卫中识别这个注解,给这个接口放行。实现如下:
步骤1:创建一个不需要token的注解
// decorators/token.decorator.ts
import { SetMetadata } from '@nestjs/common'
/**
* 接口允许token访问
*/
export const ALLOW_NO_TOKEN = 'allowNoToken'
export const AllowNoToken = () => SetMetadata(ALLOW_NO_TOKEN, true)
步骤2:创建自定义守卫
//guards/jwt-auth.guard.ts
import { ExecutionContext, ForbiddenException, HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { ALLOW_NO_TOKEN } from '../decorators/token.decorator';
import { AuthService } from '../auth.service';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(
private reflector: Reflector,
private authService: AuthService
) {
super();
}
async canActivate(ctx: ExecutionContext): Promise<boolean> {
// 接口是否允许无 token 访问
const allowNoToken = this.reflector.getAllAndOverride<boolean>(ALLOW_NO_TOKEN, [ctx.getHandler(), ctx.getClass()])
if (allowNoToken) return true
// 验证用户是否登录
const req = ctx.switchToHttp().getRequest()
const access_token = req.get('Authorization')
if (!access_token) throw new HttpException('您还未登录,请先登录后使用', HttpStatus.UNAUTHORIZED)
const userId = this.authService.verifyToken(access_token)
// 判断是否登录过期
if (!userId) throw new HttpException('登录过期,请重新登录', HttpStatus.UNAUTHORIZED)
return super.canActivate(ctx) as Promise<boolean>
}
}
步骤3:auth.service增加token验证 token验证使用jwtService.verify方法:
async verifyToken(token: string): Promise<number> {
if (!token) return null;
try{
const id =await this.jwtService.verifyAsync(token.replace('Bearer ', ''));
return id;
}
catch(err){
throw new UnauthorizedException('登录过期,请重新登录');
}
}
步骤4:开启全局守卫
//user.module.ts
import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserEntity } from './entities/user.entity';
import { APP_GUARD } from '@nestjs/core';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
@Module({
controllers: [UserController],
providers: [
UserService,
// 应用jwt登录态验证守卫
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
],
imports: [
TypeOrmModule.forFeature([UserEntity])
],
})
export class UserModule {}
步骤5:测试
测试接口如下:
import { Controller, Get, Post, Request, UseGuards } from '@nestjs/common';
import { AuthService } from './auth.service';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { AllowNoToken } from './decorators/token.decorator';
import { LocalAuthGuard } from './guards/local-auth.guard';
import { RefreshAuthGuard } from './guards/refresh-auth.guard';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
/*
* 登录@Request() req
*/
@Post("signin")
@AllowNoToken()
@UseGuards(LocalAuthGuard)
async signin(@Request() req){
return await this.authService.signIn(req.user.id,req.user.username)
}
@Get("protected")
getAll(@Request() req){
return {
message:`Now you can access protected API,this is your user id:${req.user.id}`
}
}
@Post("refresh")
@AllowNoToken()
@UseGuards(LocalAuthGuard,RefreshAuthGuard)
refreshToken(@Request() req){
return this.authService.refreshToken(req.user.id,req.user.username)
}
}
1)登录接口测试 登录接口的注解 @AllowNoToken()表示登录接口不验证token,登录成功后才返回token。 注解@UseGuards(LocalAuthGuard) 表示使用用户名和密码到数据库中进行验证,存在该用户则在request中自动增加上user,所以该接口不再需要到数据库中验证,直接生成token。
2)受保护接口测试 拷贝登录接口返回的accessToken和refreshToken,后边测试使用。
3)刷新token接口测试 刷新窗口的注解@AllowNoToken()表示不验证accesstoken,因为此时因为accesstoken已过期,所以才使用refreshtoken进行重新生成token,所以不再验证accesstoken。 注解@UseGuards(LocalAuthGuard,RefreshAuthGuard),第一个注解和登录接口一个意思,验证用户名和密码。第二个注解是验证refreshtoken并返回新的一对token。