撰于 阅读 181

nestjs中rbac权限模型

rbac基于用户(user)、角色(role)、权限(permission)的权限控制,用nestjs实现一个小demo。

github:链接

用户登录成功后返回token,token中携带当前用户的角色信息和权限信息

  /**
   * 用户登录
   * @param loginDto
   */
  async login(loginDto: LoginDto) {
    const { email, password, code, uuid } = loginDto;
    const isUUID = await this.redisService.get(`captcha_${uuid}`);

    if (!isUUID) {
      throw new BussException("验证码错误");
    }
    if (isUUID.toString().toLocaleLowerCase() !== code.toString().toLocaleLowerCase()) {
      throw new BussException("验证码错误");
    }
    // 当前用户
    const currentUser = await this.userRepository.findOne({
      where: {
        email
      },
      relations: ["roles", "roles.permissions"]
    });
    if (!currentUser) {
      throw new BussException("当前用户未注册");
    }
    const isPass = await verify(currentUser.password, password);

    if (!isPass) {
      throw new BussException("密码错误");
    }

    if (!currentUser.status) {
      throw new BussException("该账户已被冻结,请联系管理员");
    }
    // 获取当前用户的角色和权限
    const roles = currentUser.roles.map(v => v.name);
    const permissions = currentUser.roles.flatMap(role => role.permissions);
    // 生成本次验证token
    const token = this.jwtService.sign(
      {
        userId: currentUser.id,
        email: currentUser.email,
        username: currentUser.username,
        roles,
        permissions
      },
      {
        expiresIn: this.configService.get("jwt_expiresIn") || "30m"
      });
    // 重置token
    const refreshToken = this.jwtService.sign(
      {
        userId: currentUser.id
      },
      {
        expiresIn: "7d"
      }
    );
    // 剔除一些不需要返回的状态
    const { password: discardPassword, status: discardStatus, ...userInfo } = currentUser;
    return {
      token,
      refreshToken,
      userInfo
    };
  }

通过nestjs guard判断鉴权

login guard

@Injectable()
export class LoginGuard implements CanActivate {
  @Inject()
  private readonly reflector: Reflector;

  @Inject(JwtService)
  private readonly jwtService: JwtService;


  canActivate(
    context: ExecutionContext
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    // 获取是否需要鉴权
    const requireLogin = this.reflector.getAllAndOverride("require-login", [
      context.getClass(),
      context.getHandler()
    ]);
    const authorization = request.headers.authorization;
    // 没有则放行,公共接口(不需要登录)
    if (!requireLogin) {
      return true;
    }
    // 需要鉴权,切没有token
    if (!authorization) {
      throw new UnauthorizedException("用户未登录");
    }
    try {
      const token = authorization.split(" ")[1];
      const data = this.jwtService.verify(token);
      // userInfo中包含登录成功的用户信息
      let { iat, exp, ...userInfo } = data;
      request.userInfo = {
        ...userInfo,
      };
      return true;
    } catch (error) {
      throw new UnauthorizedException("用户未登录");
    }
  }
}

permission guard

@Injectable()
export class PermissionGuard implements CanActivate {
  @Inject()
  private readonly reflector: Reflector;

  canActivate(
    context: ExecutionContext
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();

    if (!request.userInfo) {
      return true;
    }
    // 拿到用户的权限信息
    const permissions = request.userInfo.permissions;
    // 是否需要鉴权
    const requirePermission = this.reflector.getAllAndOverride("require-permission", [
      context.getClass(),
      context.getHandler()
    ]);

    if (!requirePermission) {
      return true;
    }

    for (let i = 0; i < requirePermission.length; i++) {
      const curPermission = requirePermission[i];
      const found = permissions.find((item: any) => item.identifying === curPermission);
      if (!found) {
        throw new BussException("您没有访问该接口的权限");
      }
    }

    return true;
  }
}

通过decorator设置装饰器

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

// 登录鉴权
export const requireLogin = () => SetMetadata('require-login', true)

// 用户权限鉴权
export const requirePermission = (...permissions: string[]) => SetMetadata('require-permission', permissions)


// 获取用户信息
export const UserInfo = createParamDecorator(
    (data: string, ctx: ExecutionContext) => {
        const request: any = ctx.switchToHttp().getRequest<Request>();
        if (!request.userInfo) {
            return null;
        }
        return data ? request.userInfo[data] : request.userInfo;
    },
)

在需要用到鉴权的controller设置requireLogin和require-permission
require-permission中传入当前接口需要的key值,也就是权限字段的值。