验证分为两种,登入权限验证以及角色验证
举例说明:我们将 API 分为三种情境
不需要登入
呼叫 API 时因为完全不需要验证,所以我们不会设定 Guard
登入不需要权限 @UseGuards(AuthGuard)
我们设定某些 API 是需要登入後才能呼叫的,也就是 API 的 headers 必须带着有效的 Token ,Guard 会进行 Token 解析,通过便能呼叫此 API ,如果失败就会回传 400 或者 401 的状态
登入後需要角色验证 @UseGuards(AuthGuard, RolesGuard)
最後便是角色权限的验证,像是某些 API 只能由角色是Admin 来呼叫,这时我们就会多一个 RolesGuard 的验证,@UseGuards 是有先後顺序的,我们通常会先验证 AuthGuard 再来验证 RolesGuard ,如果 RolesGuard 验证失败就会回传 403 状态
在 NestJs 中有一个主题是在说明 Authentication,有很多种做法,我这边会针对 GraphQL 来说明如何实作 Auth 的验证
NestJs 中提供了几个套件能够验证
@nestjs/common
提供的介面,我们需要实作此介面@nestjs/passport
提供的Class,我们会透过扩充的方式来使用它我会使用 NestJs 提供的套件 @nestjs/jwt 来产生 Token 以及解析 Token
@Module({
imports: [
JwtModule.register({
secret: 'test', // 为了测试方便先直接明文写
signOptions: { expiresIn: '1h' }, // 有效时长
}),
]
})
在登入後使用 jwtService 产生一个有效的 Token ,里面放了 username 以及角色,方便我後续做角色验证
@Mutation(() => String)
async login (
@Args() userArgs: UserArgs
) {
const user = await this.userService.findUser(userArgs);
const accessToken = this.jwtService.sign({
role: user.role,
username: user.username
});
return accessToken;
}
将 request (req) object 传给 context
@Module({
imports: [
GraphQLModule.forRoot({
context: ({ req }) => ({ req })
}),
UsersModule
]
})
export class AppModule {}
取得 Request headers 中的 Token ,透过在 usersService 实作好的 validateToken 来解析 Token ,将解析出来的 User 回传,最後再塞回去 req 中
import {
ExecutionContext,
Injectable,
UnauthorizedException,
BadRequestException
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GqlExecutionContext } from '@nestjs/graphql';
import { UsersService } from './users.service';
@Injectable()
// export class GqlAuthGuard implements CanActivate {
export class GqlAuthGuard extends AuthGuard('jwt') {
constructor(private readonly usersService: UsersService) {
super() // 实作 CanActivate 不需要
}
getRequest(context: ExecutionContext) {
const ctx = GqlExecutionContext.create(context);
return ctx.getContext().req;
}
async canActivate(context: ExecutionContext): Promise<boolean> {
const req = this.getRequest(context);
const authHeader = req.headers.authorization as string;
if (!authHeader) {
throw new BadRequestException('Authorization header not found.');
}
const { isValid, user } = await this.usersService.validateToken(authHeader)
if (isValid) {
req.user = user;
return true;
}
throw new UnauthorizedException('Token not valid');
}
}
能在 req 中取得刚刚放进去的 user ,再将它回传出去,我们就能够在使用 @CurrentUser
时取得 User
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
export const CurrentUser = createParamDecorator(
(data: unknown, context: ExecutionContext) => {
const ctx = GqlExecutionContext.create(context);
return ctx.getContext().req.user;
},
);
接着在到需要验证的 API 上加上需要的 Decorator
@UseGuards(GqlAuthGuard)
@Query(() => TaskConnection)
async doneTasks(
@CurrentUser() user: User,
@Args() taskArgs: TaskArgs
) {
const tasks = await this.taskService.queryTasks(taskArgs, TaskStatus.DONE );
const taskCount = await this.taskService.taskCount(taskArgs, TaskStatus.DONE);
return { tasks, taskCount};
}
如果有跨 Module 记得要将 UserService Export ,因为我们在 AuthGuard 有使用 UserService
角色验证相对来说就简单一些
使用 Reflector 可以取得 Decorator 的内容, super-roles
是我自定义的 Decorator ,等等会说明该如何建立
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
import { Reflector } from '@nestjs/core';
import { GqlExecutionContext } from '@nestjs/graphql';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(
private readonly reflector: Reflector
) {}
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const roles = this.reflector.get<string[]>('super-roles', context.getHandler());
const { role } = GqlExecutionContext.create(context).getContext().req.user;
if (roles.indexOf(role) === -1) return false;
return true;
}
}
import { SetMetadata } from '@nestjs/common';
export const SuperRoles = (...roles: string[]) => SetMetadata('super-roles', roles);
接着到 API 上设定 Role
在 @UseGuards 上加上 RolesGuard,并使用 @SuperRoles
设定角色,RolesGuard 就能取得角色来做验证
@UseGuards(GqlAuthGuard, RolesGuard)
@SuperRoles('vip')
@Query(() => TaskConnection)
async doneTasks(
@CurrentUser() user: User,
@Args() taskArgs: TaskArgs
) {
const tasks = await this.taskService.queryTasks(taskArgs, TaskStatus.DONE );
const taskCount = await this.taskService.taskCount(taskArgs, TaskStatus.DONE);
return { tasks, taskCount};
}
@Mutation(() => Task)
async createTask(@Args('taskData') taskData: TaskInput) {
const task = await this.taskService.createTask(taskData);
return task;
}
@UseGuards(RolesGuard)
@Query(() => TaskConnection)
async doneTasks() {}
@UseGuards(RolesGuard)
@Resolver()
export class TasksResolver {}
main.ts
const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new RolesGuard());
app.module.ts
providers: [{
provide: APP_GUARD,
useClass: RolesGuard,
}]
<<: [Golang]恢复panic(recover、defer)-心智图总结
>>: GitHub - 使用 SSH 来 push commit 吧!
疯狂程设是一个练习CPE常用的软件,疯狂程设拥有许多练习题,不是只有CPE。 如何下载疯狂程设?可以...
实战演练开始前,稍微来讲解一下 Requests 的基本使用,当作是暖身。 用 Requests 送...
本文内容 本文为阅读 Angular 的 Route 其中一项设定 pathMath 的笔记内容。 ...
前言 我很喜欢这篇 CodeLab,我自己认为,如果这篇的内容看得懂那 Provider 基本上都会...
今天就来完成登入验证的部分! 昨天已经完成发送帐号密码到api,验证ok即发送一笔JWT给clien...