Skip to content

身份认证

基础知识

向军大叔每晚八点在 抖音bilibli 直播

身份验证过程是,客户端将首先使用用户名和密码进行身份验证。经过身份验证后,服务器将发出一个 JWT。然后在请求的头信息中携带 JWT 来标识身份。

我们需要完成以下几步

  • 对用户进行身份验证
  • 然后,我们将发给用户TOKEN
  • 最后,我们将创建一个受保护的路由,用于检查请求上的有效 TOKEN

官网文档 对新手来讲由于内容较多,可能不太好理解,本章向军大叔教大家快速搭建JWT。

以下代码会使用到配置项,登录注册等知识,这是前面章节讲过的,所以还是按顺序学习

安装配置

使用JWT需要安装 @nest/jwt 等依赖包。

pnpm add @nestjs/passport passport passport-local @nestjs/jwt passport-jwt
pnpm add -D @types/passport-local @types/passport-jwt

下面在auth.module.ts模块中定义Jwt模块

  • 过期时间设置请参考 vercel/ms 扩展包
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';

@Module({
  imports: [
    JwtModule.registerAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => {
        return {
          //设置加密使用的 secret
          secret: config.get('app.token_secret'),
          //过期时间
          signOptions: { expiresIn: '300d' },
        };
      },
    }),
  ],
  providers: [AuthService],
  controllers: [AuthController],
})
export class AuthModule {}

获取令牌

下面操作在用户登录时获取 Token,首先在 auth.service.ts中实现获取 token逻辑

import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
...

export class AuthService {
  constructor(private readonly jwt:JwtService){}

  async login({ email, password }: CreateAuthDto) {
    const user = await this.prisma.users.findUnique({
      where: { email },
    });

    const psMatch = await argon2.verify(user.password, password);

    if (!psMatch) throw new ForbiddenException('密码输入错误');

    return this.token(user);
  }
  
	//获取token
  async token(user: users) {
    return {
      token: await this.jwt.signAsync({
        username: user.email,
        sub: user.id,
      }),
    };
  }
}

现在使用postman等访问login接口,会得到以下内容

image-20220714225043288

身份校验

下面来使用token进行身份验证

策略编写

策略是实现JWT的验证逻辑,策略就像你家小区的门禁验证规则,对你的身份进行查验 。

我们可以编写多个策略,比如根据用户名与密码的验证策略,或根据TOKEN的验证策略。

下面定义 jwt.strategy.ts 文件,定义使用 token 进行身份验证的JWT策略。

import { PrismaService } from './../prisma/prisma.service';
import { ConfigService } from '@nestjs/config';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
  constructor(configService: ConfigService, private prisma: PrismaService) {
    super({
      //解析用户提交的header中的Bearer Token数据
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      //加密码的 secret
      secretOrKey: configService.get('TOKEN_SECRET'),
    });
  }

  //验证通过后获取用户资料
  async validate({ sub: id }) {
    return this.prisma.users.findUnique({
      where: { id }, 
    });
  }
}

然后在 auth.module.ts 中注册为提供者

import { JwtStrategy } from './strategy';
...
@Module({
  ...
  controllers: [AuthController],
  //注入容器
  providers: [AuthService, JwtStrategy],
})
export class AuthModule {}

验证使用

现在创建个模块auth用于实验token的验证

nest g res user

只保留控制器中的 findOne 方法

import { Controller, Get, Param, Req, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Request } from 'express';
import { UserService } from './user.service';

@Controller('user')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Get(':id')
  //AuthGuard守卫使用jwt策略进行验证
  @UseGuards(AuthGuard('jwt'))
  //jwt.strategy.ts 中 validate结果会保存到req请求数据中
  findOne(@Req() req: Request) {
    return req.user;
  }
}

现在通过postman请求user/1 来测试结果

image-20220714233007126

简化操作

我们将验证的 @UseGuards(AuthGuard('jwt')) 代码通过 装饰器 进行简化操作。

模块可能有多个装饰器所以创建装饰器目录 auth/结构如下

src/decorator
├── auth.decorator.ts 验证装饰器
└── user.decorator.ts 用户资料装饰器

装饰器聚合

我们将在控制器方法中使用的 @UseGuards(JwtGuard) 验证简化为 @Auth(),这需要定义Auth装饰器完成。

装饰器 decorator/auth.decorator.ts 内容如下,使用 装饰器聚合 功能完成。

你可以把装饰器聚合 理解为应用多个装饰器。

import { applyDecorators, UseGuards } from '@nestjs/common'
import { AuthGuard } from '@nestjs/passport'

export function Auth() {
  return applyDecorators(UseGuards(AuthGuard('jwt')))
}

现在在控制器直接使用 @Auth() 装饰器

import { Auth } from 'src/auth/decorator';

export class UserController {
	...
  @Auth()
  findOne() {
  }
}

用户装饰器

decorator/user.decorator.ts 用于获取request中的user用户信息,user来源于上面讲解的策略。

import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const User = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    return request.user;
  },
);

然后在 user.controller.ts 等控制器中使用

import { Auth,User } from 'src/auth/decorator';
...

@Controller('user')
export class UserController {
  ...
  @Auth()
  findOne(@User() user: users) {
    return user;
  }
}

守卫定义

守卫是根据选择的策略对身份进行验证,保护路由访问。上面例子中一直在使用AuthGuard守卫,我们也可以自定义守卫。

根据运行时出现的某些条件(例如权限,角色,访问控制列表等)来确定给定的请求是否由路由处理程序处理,这通常称为授权。

img

用户验证

创建 admin.guard.ts 守卫,验证用户 ID为1时,身份验证通过。

  • 如果当前登录用户 id1 时通过验证
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'
import { Observable } from 'rxjs'

@Injectable()
export class AdminGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
    const { user } = context.switchToHttp().getRequest()
    return user?.id == 1
  }
}

然后在 admin.decorator.ts 文件中定义装饰器聚合函数

  • 使用 Auth 装饰器获取当前登录用户,为 UseGuards 守卫提供 user 当前用户数据
import { applyDecorators, UseGuards } from '@nestjs/common'
import { AdminGuard } from '../guard/admin.guard'
import { Auth } from './auth.decorator'

export function Admin() {
  return applyDecorators(Auth(), UseGuards(AdminGuard))
}

现在可以在控制器中使用了

import { Post } from '@nestjs/common'
import { Admin } from 'src/auth/decorator/admin.decorator'

@Controller('article')
export class ArticleController {
  @Post()
  @Admin()
  create() {
    return 'houdunren.com'
  }
  ...
}

角色验证

下面通过对角色的验证来检测用户是否有对控制器方法的访问权限

mysql 用户表结构如下

+----------+--------------+------+-----+---------+----------------+
| Field    | Type         | Null | Key | Default | Extra          |
+----------+--------------+------+-----+---------+----------------+
| id       | int unsigned | NO   | PRI | <null>  | auto_increment |
| name     | varchar(191) | NO   | UNI | <null>  |                |
| password | varchar(191) | NO   |     | <null>  |                |
| role     | varchar(191) | YES  |     | <null>  |                |
+----------+--------------+------+-----+---------+----------------+

创建策略文件 auth/strategy/jwt.strategy.ts 用于获取当前登录用户信息

import { PrismaService } from '@/prisma/prisma.service'
import { Injectable } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { PassportStrategy } from '@nestjs/passport'
import { ExtractJwt, Strategy } from 'passport-jwt'

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
  constructor(configService: ConfigService, private prisma: PrismaService) {
    super({
      //解析用户提交的Bearer Token header数据
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      //加密码的 secret
      secretOrKey: configService.get('TOKEN_SECRET'),
    })
  }

  //验证通过后结果用户资料
  async validate({ sub: id }) {
    return await this.prisma.user.findUnique({
      where: { id },
    })
  }
}

创建 auth/enum.ts 文件,用于定义角色类型

export enum Role {
  ADMIN = 'admin',
}

下面创建 auth/decorators/auth.decorator.ts 聚合装饰器

  • 通过设置元信息Roles 来声明该方法可访问的角色
import { applyDecorators, SetMetadata, UseGuards } from '@nestjs/common'
import { AuthGuard } from '@nestjs/passport'
import { Role } from '../enum'
import { RolesGuard } from '../guards/roles.guard'

export function Auth(...roles: Role[]) {
  return applyDecorators(SetMetadata('roles', roles), UseGuards(AuthGuard('jwt'), RolesGuard))
}

然后创建 auth/guards/roles.guard.ts 守卫文件,用于对角色进行验证。

使用 reflector 反射获取上面在控制器方法中定义的角色数据

  • context.getHandler 当前请求方法
  • context.getClass 当前控制器
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'
import { Reflector } from '@nestjs/core'
import { user } from '@prisma/client'
import { Observable } from 'rxjs'
import { Role } from '../enum'

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}
  canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
    const user = context.switchToHttp().getRequest()?.user as user

    const roles = this.reflector.getAllAndOverride<Role[]>('roles', [context.getHandler(), context.getClass()])
    return roles ? roles.some((r) => user.role == r) : true
  }
}

然后在控制器中使用

import { Auth } from '@/auth/decorators/auth.decorator'
import { Role } from '@/auth/enum'
...

@Controller('article')
export class ArticleController {
  constructor(private readonly articleService: ArticleService) {}

  @Auth(Role.ADMIN)
  @Post()
  create(@Body() createArticleDto: CreateArticleDto) {
    return this.articleService.create(createArticleDto)
  }
  ...
}