基于角色的权限控制例如:
| 角色 | 权限 |
|---|---|
| admin | 增删改查 |
| user | 只能查看 |
| guest | 无权限 |
先理解 RBAC
当前:
登录 ↓ JWT ↓ 认证成功只能证明:
你是谁但是不知道:
你能干什么RBAC解决的是:
你能访问哪些资源最终效果
普通用户:
DELETE /books/1返回:
{ "statusCode":403, "message":"Forbidden" }管理员:
DELETE /books/1成功:
{ "message":"删除成功" }第一步:给 User 增加角色
修改:
src/users/entities/user.entity.ts现在:
@Entity() export class User { @PrimaryGeneratedColumn() id:number; @Column({ unique:true, }) username:string; @Column() password:string; }增加:
@Column({ default:'user', }) role:string;完整:
@Entity() export class User { @PrimaryGeneratedColumn() id:number; @Column({ unique:true, }) username:string; @Column() password:string; @Column({ default:'user', }) role:string; }数据库会变:
user| id | username | password | role |
|---|---|---|---|
| 1 | admin | xxx | admin |
| 2 | tom | xxx | user |
第二步:注册时默认 user
修改:
users.service.ts创建用户:
const user = this.userRepository.create({ username, password, role:'user', });后面可以做:
后台创建管理员暂时手动修改数据库。
例如:
update user set role='admin' where id=1;第三步:登录时携带角色
当前:
const payload = { sub:user.id, username:user.username, }改:
const payload = { sub:user.id, username:user.username, role:user.role, }JWT:
{ "sub":1, "username":"admin", "role":"admin" }第四步:创建 Roles 装饰器
目录:
src/auth/decorators新增:
roles.decorator.ts内容:
import { SetMetadata } from '@nestjs/common'; export const ROLES_KEY = 'roles'; export const Roles = ( ...roles:string[] ) => SetMetadata( ROLES_KEY, roles, );使用:
@Roles('admin')等价:
roles:['admin']第五步:创建 RolesGuard
目录:
src/auth/guards新增:
roles.guard.ts内容:
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { ROLES_KEY } from '../decorators/roles.decorator'; import { JwtPayload } from '../interfaces/jwt-payload.interface'; @Injectable() export class RolesGuard implements CanActivate { constructor(private reflector: Reflector) {} canActivate(context: ExecutionContext) { const roles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [ context.getHandler(), context.getClass(), ]); if (!roles) { return true; } const request = context.switchToHttp().getRequest<{ user: JwtPayload; }>(); const user = request.user; return roles.includes(user.role); } }新增
src\auth\interfaces\jwt-payload.interface.tsexport interface JwtPayload { sub: number; username: string; role: string; }逻辑:
读取 @Roles() ↓ 获取 request.user.role ↓ 比较 ↓ 通过 / 拒绝第六步:注册全局 RolesGuard
打开:
app.module.ts增加:
import { RolesGuard } from './auth/guards/roles.guard';providers:
providers:[ AppService, { provide:APP_GUARD, useClass:JwtAuthGuard, }, { provide:APP_GUARD, useClass:RolesGuard, }, ]执行顺序:
JwtAuthGuard ↓ RolesGuard ↓ Controller第七步:给接口加角色限制
例如:
books.controller.ts删除图书:
@Roles('admin') @Delete(':id') remove( @Param('id') id:string, ){ return this.booksService.remove( +id, ); }查看图书:
@Get() findAll(){ return this.booksService.findAll(); }不限制。
第八步:测试
用户
数据库:
role=user登录:
{ "access_token":"xxx" }请求:
DELETE /books/1返回:
{ "statusCode":403, "message":"Forbidden resource" }管理员
数据库:
role=admin登录:
{ "access_token":"xxx" }请求:
DELETE /books/1成功。
进一步优化(推荐)
不要用字符串:
@Roles('admin')创建:
// src/users/enums/role.enum.ts export enum Role { ADMIN='admin', USER='user', }使用:
import { Role } from '../enums/role.enum';User 实体:
@Column({ default: Role.USER, }) role: Role;Roles 装饰器:
@Roles(Role.ADMIN)你当前项目推荐结构
src ├── auth │ ├── decorators │ ├── guards │ ├── interfaces │ │ └── jwt-payload.interface.ts │ ├── users │ ├── entities │ │ └── user.entity.ts │ ├── enums │ │ └── role.enum.ts │ ├── booksrole.enum.ts
export enum Role { ADMIN = 'admin', USER = 'user', }user.entity.ts
import { Role } from '../enums/role.enum'; @Column({ type: 'enum', enum: Role, default: Role.USER, }) role: Role;roles.decorator.ts
import { Role } from '../../users/enums/role.enum'; export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);books.controller.ts
@Roles(Role.ADMIN) @Delete(':id') remove(@Param('id') id: string) { return this.booksService.remove(+id); }这样从现在开始,你整个 RBAC 系统就都是强类型的:
Role.ADMIN Role.USER而不是:
'admin' 'user'避免拼写错误导致权限失效。
RBAC完善
支持多角色
实际上你前面写的Roles()已经天然支持多角色。
因为:
export const Roles = (...roles: Role[]) => SetMetadata( ROLES_KEY, roles, );这里:
...roles就是剩余参数。
例如:
@Roles(Role.ADMIN)得到:
['admin']例如:
@Roles( Role.ADMIN, Role.USER, )得到:
[ 'admin', 'user', ]你的 Guard:
return roles.includes( user.role, );已经支持多角色。
所以:
@Roles( Role.ADMIN, Role.USER, ) @Get() findAll() {}表示:
admin 可以访问 或 user 可以访问实现 CurrentUser 装饰器
目前你可能这样拿用户:
@Get() findAll( @Req() req, ){ console.log(req.user); }不优雅。
Nest 推荐封装装饰器。
创建目录
src └── auth └── decorators └── current-user.decorator.ts编写装饰器
import { createParamDecorator, ExecutionContext, } from '@nestjs/common'; import { Request } from 'express'; export const CurrentUser = createParamDecorator( ( data: unknown, ctx: ExecutionContext, ) => { const request = ctx .switchToHttp() .getRequest<Request>(); return request.user; }, );给 Request.user 类型
否则会报:
Property 'user' does not exist on type Request创建:
src └── types └── express.d.ts内容:
import { JwtPayload } from '../auth/interfaces/jwt-payload.interface'; declare global { namespace Express { interface Request { user: JwtPayload; } } } export {};完善 JwtPayload
你应该已经有:
src/auth/interfaces/jwt-payload.interface.ts内容:
import { Role } from '../../users/enums/role.enum'; export interface JwtPayload { sub: number; username: string; role: Role; }完善 JwtStrategy
现在:
validate( payload: JwtPayload, ){ return payload; }Controller 使用
例如:
import { CurrentUser } from '../auth/decorators/current-user.decorator'; import { JwtPayload } from '../auth/interfaces/jwt-payload.interface';@Get() findAll( @CurrentUser() user: JwtPayload, ){ console.log(user); return this.booksService.findAll(); }请求:
GET /books Authorization: Bearer xxx打印:
{ sub: 1, username: 'admin', role: 'admin' }直接获取字段(高级写法)
改造装饰器:
export const CurrentUser = createParamDecorator( ( data: keyof JwtPayload, ctx: ExecutionContext, ) => { const request = ctx.switchToHttp().getRequest(); const user = request.user; return data ? user[data] : user; }, );然后:
获取整个用户:
@CurrentUser() user: JwtPayload得到:
{ sub:1, username:'admin', role:'admin' }只获取用户名:
@CurrentUser('username') username: string得到:
admin只获取角色:
@CurrentUser('role') role: Role得到:
admin测试示例
@Roles( Role.ADMIN, Role.USER, ) @Get('profile') profile( @CurrentUser() user: JwtPayload, ){ return user; }请求:
GET /books/profile Authorization: Bearer xxx返回:
{ "sub": 1, "username": "admin", "role": "admin" }到这里,你的认证授权体系已经比较完整:
JWT ↓ JwtStrategy ↓ CurrentUser ↓ JwtAuthGuard ↓ RolesGuard ↓ RBAC现在你的权限体系
JWT认证 ↓ JwtAuthGuard ↓ 获取用户 ↓ RolesGuard ↓ 角色校验 ↓ Controller这已经是很多企业后台管理系统的基础权限架构了。