Skip to content

注册、登录、JWT 鉴权、密码加密的思路

注册环节

  1. 第一步:客户端接口请求
shell
# 执行:使用 API-POST 工具调用注册接口
http://localhost:3000/api/v1/auth/register
  1. 第二步:进入 src/auth/auth.controller.ts 控制器
shell
# 执行:
# 1. 调用 @Public() 装饰器:注入元信息,跳过 JWT 全局守卫拦截
# 2. 调用 register 方法:执行 authService 的注册方法

@Public() # 不加 jwt 拦截
@Post('/register')
async register(@Body() userDto: any) {
 const { username, password } = userDto;
 return this.authService.register(username, password);
}
  1. 第三步:进入 src/auth/auth.service.ts 服务
shell
# 执行:
# 1. 调用 register 方法,调用 src/user/user.service.ts 中的用户查询和保存方法完成校验和入库
# --> 在入库前完成密码的 argon2 加密

async register(uname: string, pwd: string): Promise<any> {
 const user = await this.userService.findOne(uname);
 if (user) {
   throw new ForbiddenException('此用户已经存在');
 }
 # argon2 加密
 const hashedPassword = await this.passwordService.hashPassword(pwd);
 const result = await this.userService.create({
   username: uname,
   password: hashedPassword,
 });
 if (result && result.username) {
   return {
     message: '注册成功',
     data: {
       id: result.id,
       username: result.username,
     },
   };
 }
}
  1. 第四步:结果
shell
{
    "message": "注册成功",
    "data": {
        "id": 7,
        "username": "aliswsssas"
    }
}

登录环节

面向切面编程思路:登录守卫 + passport 协议 + jwt

  1. 第一步:客户端接口请求
shell
# 执行:使用 API-POST 工具调用登录接口
http://localhost:3000/api/v1/auth/login
  1. 第二步:进入 src/auth/auth.controller.ts 控制器
shell
# 执行:
# 1. 调用 @Public() 装饰器:注入元信息,跳过 JWT 全局守卫拦截
# 2. 调用 LoginAuthGuard 登录守卫:通过 passport 调用 src/auth/login.strategy.ts 的 validate 方法

@UseGuards(LoginAuthGuard)
@Public()
@Post('/login')
async login(@Request() req: any) {
 return this.authService.login(req.user);
}
  1. 第三步:进入 src/auth/auth.service.ts 服务
shell
# 调用 src/auth/auth.service.ts 的 validateUser 方法,传入 req 作为参数:调用 src/user/user.service.ts 中的用户查询方法,获取用户信息并校验用户
# --> 调用密码验证方法 verifyPassword: 把客户端的密码和数据库获取的密码传入 verifyPassword 验证密码是否正确
# --> 错误则抛出异常、正确则返回用户信息总除了密码以外的所有值
# --> return result; 的结果会挂载到 req 上通过 req.user 访问
async validateUser(username: string, pass: string): Promise<any> {
 const user = await this.userService.findOne(username);
 if (!user) {
   throw new UnauthorizedException('用户不存在');
 }
 const isValid = await this.passwordService.verifyPassword(
   user.password,
   pass,
 );
 if (!isValid) {
   throw new UnauthorizedException('密码错误');
 }
 const { password, ...result } = user;
 return result;
}
  1. 第四步:进入 src/auth/auth.controller.ts 控制器
shell
# 调用 src/auth/auth.controller.ts 中的 login 方法:此时 req 会挂载 user 参数,值为 src/auth/auth.service.ts 的 validateUser 方法的返回值,即:用户信息
async login(@Request() req: any) {
 return this.authService.login(req.user);
}
  1. 第五步:进入 src/auth/auth.service.ts 服务
shell
# 调用 src/auth/auth.service.ts 中的 login 方法并传入 用户信息
# --> 结合用户信息,生成 JWT token,并返回给客户端
async login(user: any) {
 const payload = { username: user.username, sub: user.id };
 return {
   username: payload.username,
   access_token: `Bearer ${this.jwtService.sign(payload)}`,
 };
}
  1. 第六步:结果
shell
{
    "username": "alias",
    "access_token": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFsaWFzIiwic3ViIjozLCJpYXQiOjE3NjQ5MzcwODcsImV4cCI6MTc2NTAyMzQ4N30.tocUiatXo2GjMlJaUNQlFIgfJ9EgLbs8Duv48p4t584"
}

JWT 鉴权

  1. 第一步:登录获取 token
shell
# 登录接口
http://localhost:3000/api/v1/auth/login

# 登录参数
{
   "username": "alias",
   "password": "123456"
}

# 获取 toke
{
    "username": "alias",
    "access_token": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFsaWFzIiwic3ViIjozLCJpYXQiOjE3NjQ5Mzc0NTUsImV4cCI6MTc2NTAyMzg1NX0.IKbCW9wOVy5TWv8ptq60IojHLD5m2KAOohGu97LiFfU"
}
  1. 第二步:调用需要 JWT 鉴权的接口,并传入 token
shell
# 调用接口
http://localhost:3000/api/v1/auth/getUser

# 传入 token
Headers 中添加 Authorization 属性,值为 access_token
  1. 第三步:进入 src/auth/auth.controller.ts 控制器
ts
// 执行:默认触发 jwt 全局路由守卫

@Get('/getUser')
async findOne(@Request() req: any) {
 return await this.authService.findOne(req.username);
}
  1. 第四步:触发 src/app.module.ts 中注册的 JWT 全局守卫 JwtAuthGuard
shell
# 执行:注册 jwt 全局路由守卫

import { APP_GUARD } from '@nestjs/core';
import { JwtAuthGuard } from './auth/jwt-auth.guard';

# 注册 jwt 全局路由守卫
const JwtGlobalGuard = {
  provide: APP_GUARD,
  useClass: JwtAuthGuard,
};

@Module({
  providers: [JwtGlobalGuard, AppService],
})
  1. 第五步:进入 src/auth/jwt-auth.guard.ts 守卫
shell
# 执行:
# 1. 判断当前路由上是否存在 @Public() 元信息装饰器,存在则跳过 JWT 守卫
# --> 不存在则执行 src/auth/jwt.strategy.ts 策略

import { Injectable,ExecutionContext } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Reflector } from '@nestjs/core';
import { IS_PUBLIC_KEY } from '../meta'

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  constructor(private reflector: Reflector) {
    super();
  }

  canActivate(context: ExecutionContext) {
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    if (isPublic) {
      return true;
    }
    return super.canActivate(context);
  }
}
  1. 第六步:进入 src/auth/jwt.strategy.ts 策略
shell
# 执行:
# 1. 此处 JWT 鉴权逻辑已经高度集成,通过向 super 传入指定选项,完成 JWT 鉴权
# 2. 选项:jwtFromRequest 为从请求中获取 token 、secretOrKey 为自定义的 JWT 加密密钥
# 3. 如果 JWT 校验失败则抛出异常,如果成功则,调用 validate 方法,参数为从 token 中解析出的用户信息
# --> 由于登录时存入 token 中的信息是 { username: user.username, sub: user.id },
# --> 所以最终返回结果为 { userId: payload.sub, username: payload.username }

import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { jwtConstants } from './constants';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: jwtConstants.secret,
    });
  }

  async validate(payload: any) {
    return { userId: payload.sub, username: payload.username };
  }
}
  1. 第七步:进入 src/auth/auth.controller.ts 控制器
ts
// 执行:
// 1. 能来到这说明已经通过 JWT 鉴权,并已经获取到了用户信息,且已经将信息挂载到 req 上
// 2. 将解析出的用户信息传入 src/auth/auth.service.ts 中的 findOne 方法

@Get('/getUser')
async findOne(@Request() req: any) {
 return await this.authService.findOne(req.username);
 // 如果通过了 jwt 全局守卫,则可以从 jwt 中解析用户信息,结果是jwt.strategy.ts中 validate 方法的返回值
 // req.username: '赵毅'
 // req.userId: 1
 // 其他 http 请求方式参数,也可以通过这一方式获取,当然也可以从 @Param() @Body @Query 等装饰器中获取http 对应方法的参数
 // req.params - 路由参数
 // req.query - 查询参数
 // req.body - 请求体
 // req.headers - 请求头
 // req.method - HTTP 方法
 // req.url - 请求 URL
}
  1. 第八步:src/auth/auth.service.ts 服务
shell
# 执行:
# 1. 执行 findOne 方法

async findOne(username: string): Promise<User> {
   return await this.userService.findOne(username);
}

# 2. 进入 src/user/user.service.ts 服务中,调用 findOne 方法,将结果返回给客户端
async findOne(username: string): Promise<User> {
 const options: FindOneOptions<User> = {
   where: { username },
 };
 return await this.userRepository.findOne(options);
}
  1. 第九步:结果
shell
{
    "id": 1,
    "username": "shangsan",
    "password": "$argon2id$v=19$m=65536,t=3,p=4$zOqN/Vv2LoglGRvVV8PMTA$MolXtGP830dCMOWcIc61p5RhEaSavzjn6X8r/UeT4CM",
    "createdAt": "2025-06-06T06:56:47.909Z",
    "updatedAt": "2025-06-06T06:56:47.909Z",
    "deletedAt": null,
    "version": 1
}

密码加密环节

加解密算法为 argon2,此法:加解密无需密钥,只是解密的时候需要通过传入加密和解密的密码即可,能解开说明密码正确,否则错误

  1. 注册的时候加密,在 src/auth/auth.service.ts 中
shell
#注册 & 密码加密 argon2
async register(uname: string, pwd: string): Promise<any> {
 const user = await this.userService.findOne(uname);
 if (user) {
   throw new ForbiddenException('此用户已经存在');
 }

 # argon2 加密
 const hashedPassword = await this.passwordService.hashPassword(pwd);
 const result = await this.userService.create({
   username: uname,
   password: hashedPassword,
 });

 if (result && result.username) {
   return {
     message: '注册成功',
     data: {
       id: result.id,
       username: result.username,
     },
   };
 }
}
  1. 登录的时候解密,同样在 src/auth/auth.service.ts 中
shell
# 登录 & 密码解析
async validateUser(username: string, pass: string): Promise<any> {
 const user = await this.userService.findOne(username);
 if (!user) {
   throw new UnauthorizedException('用户不存在');
 }
 const isValid = await this.passwordService.verifyPassword(
   user.password,
   pass,
 );
 if (!isValid) {
   throw new UnauthorizedException('密码错误');
 }
 const { password, ...result } = user;
 return result;
}

总结

  1. 关于用户登录 生成 token 全流程(守卫 + passport 协议 + jwt)
shell
注意:实际上在 auth.service.ts 中即可实现 jwt 签名和校验,用守卫&passport实际上只是一个例子,模仿 token 校验全过程
第一步:
 auth.controller.ts 路由匹配后,不执行 login 方法,而是首先执行 @UseGuards(LoginAuthGuard) 装饰器,
并将 @Request() req :any 参数传递给 @UseGuards(LoginAuthGuard) 装饰器,
然后此装饰器会触发 login.strategy.ts 下的 validate 方法,并将 @Request() req :any 中的 body 参数传递给 validate 方法,
然后在此 validate 方法中执行数据库查询方法,获取失败后会被拦截,获取成功后,返回用户信息
,注意啦,此用户信息会与原来的@Request() req :any组合,然后返回给 auth.controller.ts 的 login 方法
第二步:
开始执行 auth.controller.ts 中的 login 方法,并从 req.user 中获取到成功状态的 user 用户信息,
然后将此信息传递给 auth.service.ts 中的 login 方法
第三步:
在auth.service.ts 中的 login 方法中 将用户信息塞入 jwt,执行 jwt 签名后,将生成的 token 返回给 auth.controller.ts,返回给 用户
第四步:
使用 API Post 工具测试,在 headers 中新增参数名 Authorization,值为 Bearer + 空格符 + 生成的 token
  1. 关于 token 校验全过程(与上方登录类似)
shell
第一步:
 auth.controller.ts 路由匹配后,不执行 findOne 方法,而是首先执行 @UseGuards(JwtAuthGuard) 装饰器,
并将 @Request() req :any 参数传递给 @UseGuards(JwtAuthGuard) 装饰器,
然后此装饰器会触发 jwt.strategy.ts 下的 JwtStrategy 类下的一系列内部解析方法处理,大概流程是:获取 header 中的Authorization,然后解析 token
(所谓的内部实现就是封装在 passport 插件内部我们无法看到,这也是借用插件的好处,很多事不需要我们自己做),
解析失败报异常,解析成功则将结果 —— 用户信息 传递给 validate 方法,并 return
,注意啦,此用户信息会与原来的@Request() req :any组合,然后返回给 auth.controller.ts 的 findOne 方法
第二步:
开始执行 auth.controller.ts 中的 findOne 方法,并从 req.user 中获取到成功状态的 user 用户信息,
然后将此信息传递给 auth.service.ts 中的 findOne 方法
第三步:
在auth.service.ts 中的 findOne 方法中,执行核心数据库操作逻辑,将结果返回给 auth.controller.ts,返回给 用户
第四步:
使用 API Post 工具测试,在 headers 中新增参数名 Authorization,值为 Bearer + 空格符 + 生成的 token
  1. 关于 token 时效
shell
token 是无状态的,生成后失效取决于过期时间,在开发中会出现:在过期时间内多次生成的 token,都可以作为登录态
不会出现先生成的 token 失效,如果想让先生成的 token 失效,可以在用户表中加一个版本号字段,每次登录加一
然后将此版本号塞入到 token 中,每次解析时,如果发现当前版本号与当前库里的版本号不符合,则报异常,登录失效
或者 通过短期 token 和刷新 token 的机制来规避风险