Skip to content

利用 JWT 来做身份验证

安装 JWT 相关包

yarn add @nestjs/jwt

创建身份验证模块

这里使用硬编码模拟了用户查找,实际项目中还是需要使用数据库持久化存储

ts
import { Injectable } from "@nestjs/common";

// This should be a real class/interface representing a user entity
export type User = any;

@Injectable()
export class UsersService {
  private readonly users = [
    {
      userId: 1,
      username: "john",
      password: "changeme",
    },
    {
      userId: 2,
      username: "maria",
      password: "guess",
    },
  ];

  async findOne(username: string): Promise<User | undefined> {
    return this.users.find((user) => user.username === username);
  }
}

将身份验证服务 export 出去供其他模块调用

ts
import { Module } from "@nestjs/common";
import { UsersService } from "./users.service";

@Module({
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}

JWT 使用

注入 JWT 模块

在 认证模块 (auth module) 中注入 jwt module

ts
// jwt 令牌
const jwtConstants = {
  secret:
    "DO NOT USE THIS VALUE. INSTEAD, CREATE A COMPLEX SECRET AND KEEP IT SAFE OUTSIDE OF THE SOURCE CODE.",
};

@Module({
  imports: [
    UsersModule,
    JwtModule.register({
      global: true,
      secret: jwtConstants.secret,
      signOptions: { expiresIn: "60s" },
    }),
  ],
  providers: [AuthService],
  controllers: [AuthController],
  exports: [AuthService],
})
export class AuthModule {}

验证 token

创建 auth.guard, 验证请求是否携带 认证 token

ts
@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private jwtService: JwtService) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const token = this.extractTokenFromHeader(request); // 从请求头拿到 token, 格式为 Bearer Token
    if (!token) {
      throw new UnauthorizedException();
    }
    try {
      const payload = await this.jwtService.verifyAsync(token, {
        secret: jwtConstants.secret,
      });
      // 💡 We're assigning the payload to the request object here
      // so that we can access it in our route handlers
      request["user"] = payload;
    } catch {
      throw new UnauthorizedException();
    }
    return true;
  }

  private extractTokenFromHeader(request: Request): string | undefined {
    const [type, token] = request.headers.authorization?.split(" ") ?? [];
    return type === "Bearer" ? token : undefined;
  }
}

login 和 getProfile 实现

在认证模块中实现 logingetProfile(get token)

ts
@Controller("auth")
export class AuthController {
  constructor(private authService: AuthService) {}

  @HttpCode(HttpStatus.OK)
  @Post("login")
  signIn(@Body() signInDto: Record<string, any>) {
    return this.authService.signIn(signInDto.username, signInDto.password);
  }

  @UseGuards(AuthGuard)
  @Get("profile")
  getProfile(@Request() req) {
    return req.user; // 返回 token 验证后的身份信息
  }
}

// AuthService
@Injectable()
export class AuthService {
  constructor(
    private usersService: UsersService, // 注入账号密码-身份验证服务
    private jwtService: JwtService // 注入 jwt 服务
  ) {}

  async signIn(
    username: string,
    pass: string
  ): Promise<{ access_token: string }> {
    // 模拟数据库查找用户, 校验密码
    const user = await this.usersService.findOne(username);
    if (user?.password !== pass) {
      throw new UnauthorizedException();
    }

    // payload after implement of the auth.contoller.getProfile
    // token <--> payload 具有一对一对应关系
    const payload = { sub: user.userId, username: user.username };

    // 登陆后返回 token 给前端
    return { access_token: await this.jwtService.signAsync(payload) };
  }
}

全局绑定与公开路由

全局绑定

通过 providers 可以绑定一整个 module

ts
@Module({
  // ...
  providers: [
    {
      provide: APP_GUARD,
      useClass: AuthGuard,
    },
  ],
})
export class AuthModule {}

公开路由

需要一个标记手段来将部分路由跳过 auth.guard

ts
import { SetMetadata } from '@nestjs/common';

// 使用装饰器来给特定公开路由进行标记
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

// public route
@Public()
@Post()
login() {
  // logic...
}

改造 auth.guard:

ts
export class AuthGuard implements CanActivate {
  // 注入了反射器 reflector 来获取 controller/handler 上的 metadata
  constructor(private jwtService: JwtService, private reflector: Reflector) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    if (isPublic) {
      // 💡 See this condition
      return true;
    }

    const request = context.switchToHttp().getRequest();
    const token = this.extractTokenFromHeader(request);
    if (!token) {
      throw new UnauthorizedException();
    }
    try {
      const payload = await this.jwtService.verifyAsync(token, {
        secret: jwtConstants.secret,
      });
      // 💡 We're assigning the payload to the request object here
      // so that we can access it in our route handlers
      request["user"] = payload;
    } catch {
      throw new UnauthorizedException();
    }
    return true;
  }
}
2025( )
今日 8.33%
本周 42.86%
本月 48.39%
本年 4.11%
Powered by Snowinlu | Copyright © 2024- | MIT License