likes
comments
collection
share

使用这个库实现接口鉴权,太简单了。(上)——从零开始搭建一个高颜值后台管理系统全栈框架(十二)

作者站长头像
站长
· 阅读数 31

往期回顾

前言

前面我们实现了按钮权限控制,但是只在前端控制按钮显示和隐藏并没有多大用处,别人只要知道接口,可以通过一些请求工具直接调你的接口,所以后端在接受到请求的时候首先判断用户有没有权限,有权限则通过,无权限则拒绝访问。

接口鉴权这里我推荐使用casbin这个库,使用起来真的很简单,并且支持多个平台,node、java、go、php这些常用的后端语言都支持。我们公司的项目(java)接口鉴权这一块的功能是一个后端大佬自己从零开始开发的,我接触过casbin之后,向我们后端推荐了这个库,现在我们公司项目已经使用这个库了。

Casbin

概述

Casbin 是一个强大的、高效的开源访问控制框架,其权限管理机制支持多种访问控制模型。

支持很多种语言

使用这个库实现接口鉴权,太简单了。(上)——从零开始搭建一个高颜值后台管理系统全栈框架(十二)

Casbin能做什么

  1. 支持自定义请求的格式,默认的请求格式为{subject, object, action}。
  2. 具有访问控制模型model和策略policy两个核心概念。
  3. 支持RBAC中的多层角色继承,不止主体可以有角色,资源也可以具有角色。
  4. 支持内置的超级用户 例如:root 或 administrator。超级用户可以执行任何操作而无需显式的权限声明。
  5. 支持多种内置的操作符,如 keyMatch,方便对路径式的资源进行管理,如 /foo/bar 可以映射到 /foo*

Casbin不能做什么

  1. 身份认证 authentication(即验证用户的用户名和密码),Casbin 只负责访问控制。应该有其他专门的组件负责身份认证,然后由 Casbin 进行访问控制,二者是相互配合的关系。
  2. 管理用户列表或角色列表。 Casbin 认为由项目自身来管理用户、角色列表更为合适, 用户通常有他们的密码,但是 Casbin 的设计思想并不是把它作为一个存储密码的容器。 而是存储RBAC方案中用户和角色之间的映射关系。

性能测试

使用这个库实现接口鉴权,太简单了。(上)——从零开始搭建一个高颜值后台管理系统全栈框架(十二) 上面是官网给出的性能测试数据,可以看出性能是没有问题的。

入门

前言

上面的介绍大家可能看的云里雾里,下面带着大家实战一下,让大家更深入的了解casbin的用法。

初始化一个midway项目

找一个合适的目录,执行下面命令创建midway项目。

npm init midway

安装casbin依赖

pnpm i casbin --save

创建casbin模型描述文件

在项目src目录下创建basic_model.conf文件,文件内容如下:

[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act

后面讲解这些内容的含义

创建casbin策略文件

在项目src目录下创建basic_policy.csv文件,文件内容如下:

p, alice, data1, read
p, bob, data2, write

后面讲解这些内容的含义

在home.controler中使用casbin方法

// src/controller/home.controller.ts
import { App, Controller, Get } from '@midwayjs/core';
import { newEnforcer } from 'casbin';
import * as koa from '@midwayjs/koa';
import { join } from 'path';

@Controller('/')
export class HomeController {
  @App()
  app: koa.Application;

  @Get('/')
  async home(): Promise<boolean> {
    // this.app.getBaseDir() 获取当前项目基本目录,开发环境是src,打包过后是dist
    // 模型文件路径
    const casbinModelPath = join(this.app.getBaseDir(), '/basic_model.conf');

    // 策略文件路径
    const casbinPolicyPath = join(this.app.getBaseDir(), '/basic_policy.csv');

    // new一个casbin实例,有两个参数,一个是模型描述文件的路径,一个是策略文件的路径
    const e = await newEnforcer(casbinModelPath, casbinPolicyPath);

    // 这里判断bob这个人是否有data2的写权限
    // 从策略文件中可以看到bob是拥有data2的write权限的,所以这里应该返回为true
    // 策略文件里的内容
    // p, alice, data1, read
    // p, bob, data2, write
    const result = await e.enforce('bob', 'data2', 'write');

    console.log(true);

    return result;
  }
}

启动项目测试

在终端中使用npm run dev启动项目,项目启动成功后,访问http://127.0.0.1:7001/,可以看到和我们上面猜测的一样返回了true。 使用这个库实现接口鉴权,太简单了。(上)——从零开始搭建一个高颜值后台管理系统全栈框架(十二)

改一下代码,bob没有data2的read权限,这里应该返回fase。

使用这个库实现接口鉴权,太简单了。(上)——从零开始搭建一个高颜值后台管理系统全栈框架(十二)

和我们猜测一样 使用这个库实现接口鉴权,太简单了。(上)——从零开始搭建一个高颜值后台管理系统全栈框架(十二)

小结

这里我们简单的入了门,知道了如何在midway项目中使用casbin库。

model是什么

上面我们创建了一个basic_model.conf文件,可能大家对里面的内容有点迷惑,这里给大家解答一下。

model config至少包含四个部分,[request_definition], [policy_definition], [policy_effect], [matchers]

request_definition

描述

[request_definition] 是访问请求的定义。 它定义了 e.Enforce(...) 函数中的参数。

[request_definition]
r = sub, obj, act

上面 sub, obj, act 表示经典三元组: 访问实体 (Subject),访问资源 (Object) 和访问方法 (Action)。 但是, 你可以自定义你自己的请求表单, 如果不需要指定特定资源,则可以这样定义 sub、act ,或者如果有两个访问实体, 则为 sub、sub2、obj、act。

小结

这里没啥好说的,文档已经很清楚了。

policy_definition

描述

[policy_definition] 是策略的定义。 它界定了该策略的含义。 例如,我们有以下模式:

[policy_definition]
p = sub, obj, act
p2 = sub, act

这些是我们对policy规则的具体描述

p, alice, data1, read
p2, bob, write-all-objects

policy部分的每一行称之为一个策略规则, 每条策略规则通常以形如p, p2的policy type开头。 如果存在多个policy定义,那么我们会根据前文提到的policy type与具体的某条定义匹配。 上面的policy的绑定关系将会在matcher中使用, 罗列如下:

(alice, data1, read) -> (p.sub, p.obj, p.act)
(bob, write-all-objects) -> (p2.sub, p2.act)

小结

看完上面描述,大家可能还有疑惑。这里我举个🌰。

我们刚才的例子中basic_model.conf文件里[policy_definition] 的配置是下面这样的:

[policy_definition]
p = sub, obj, act

然后我们的csv策略描述文件里的数据格式是这样的:

p, alice, data1, read
p, bob, data2, write

这个p和上面的p是对应的,bob相当于sub,data2相当于obj,write相当于act。

为啥要定义这个呢,因为有时候需要支持多种策略定义,后面说到RBAC模型的时候,再详细解释。

policy_effect

描述

[policy_effect] 部分是对policy生效范围的定义, 原语定义了当多个policy rule同时匹配访问请求request时,该如何对多个决策结果进行集成以实现统一决策。

小结

官方文档看完后,大家可能更迷惑,这里说一下我的理解。

[policy_effect]
e = some(where (p.eft == allow))

开始我一直不理解p.eft从哪来的,仔细看完文档后,才发现策略定义里面把这个省略了,最后一个参数就是eft,默认值都是allow。

策略定义中

[policy_definition]
p = sub, obj, act, eft

csv中

p, alice, data1, read, allow
p, bob, data2, write, allow

这样改造后大家应该理解了吧。

前面的some,表示如果匹配到了多个,只要有一个是allow就返回true。举个🌰:

csv中添加一条数据:

p, alice, data1, read, allow,
p, bob, data2, write, allow,
p, bob, data2, write, deny,

如果我们拿'bob', 'data2', 'write'去匹配,会匹配出两条数据,一个结果是allow,一个结果是deny,因为判断那里写了,只要有一个allow就返回true,所以匹配结果是true。

matchers

描述

[matchers] 是策略匹配器的定义。 匹配器是表达式。 它确定了如何根据请求评估策略规则。

[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act

上面的这个匹配器是最简单的,它表示请求的三元组:主题、对象、行为都应该匹配策略规则中的表达式。

在匹配器中,你可以使用算术运算符如 +, -, * , / ,也可以使用逻辑运算符如:&&,||,!。

内置匹配器函数

上面匹配表达式中除了使用简单的比较以外,还可以使用函数。casbin内置了一些常用的函数。

使用这个库实现接口鉴权,太简单了。(上)——从零开始搭建一个高颜值后台管理系统全栈框架(十二)

keyMatch2这个函数我们后面会用到,用来匹配动态参数接口。

自定义函数

如果上面函数不满足你的需求,还可以自定义函数。举个🌰

使用这个库实现接口鉴权,太简单了。(上)——从零开始搭建一个高颜值后台管理系统全栈框架(十二) 使用这个库实现接口鉴权,太简单了。(上)——从零开始搭建一个高颜值后台管理系统全栈框架(十二) 使用这个库实现接口鉴权,太简单了。(上)——从零开始搭建一个高颜值后台管理系统全栈框架(十二)

上面规则表示只要策略中的sub的其中一个值包含传过来的字符串,就返回true。

测试一下

const result = await e.enforce('b', 'data2', 'write');

因为bob包含b,所以肯定返回true

const result = await e.enforce('bb', 'data2', 'write');

因为上面策略中的sub的值没有包含bb的,所以肯定返回false。

小结

匹配器支持自定义函数,让这个库有更大的扩展空间。

RBAC模型实战

前言

下面我们用这个库实现接口鉴权功能,这个可以使用casbin内置的RBAC模型,这个模型实现了用户、角色、资源(接口)的权限控制。

model

可以从github上复制内置的RBAC模型配置,关于RBAC官方文档有讲解配置的含义。

[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[role_definition]
g = _, _

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act

policy

可以从github上复制内置的RBAC策略示例数据,关于RBAC官方文档有讲解配置的含义。

p, alice, data1, read
p, bob, data2, write
p, data2_admin, data2, read
p, data2_admin, data2, write
g, alice, data2_admin

上面数据表示alice用户拥有data2_admin角色,data2_admin角色有data2资源的read权限和data2资源的write权限,所以alice用户有data2资源的read权限和data2资源的write权限,上面两行表示用户alicedate1资源的read权限,bob用户有data2write权限。

测试

使用这个库实现接口鉴权,太简单了。(上)——从零开始搭建一个高颜值后台管理系统全栈框架(十二)

和我们猜测的一样,返回true 使用这个库实现接口鉴权,太简单了。(上)——从零开始搭建一个高颜值后台管理系统全栈框架(十二)

升级

如果把资源替换成接口呢,改造一个csv文件。

p, admin, /api/book, get
p, admin, /api/book, post
p, admin, /api/book, delete
p, user,  /api/book, get
g, 张三, user
g, 李四, admin

admin角色拥有/api/book这个接口的get,post,delete权限,user角色只有get权限。张三是user角色,李四是admin角色。

const result = await e.enforce('张三', '/api/book', 'get');

使用这个库实现接口鉴权,太简单了。(上)——从零开始搭建一个高颜值后台管理系统全栈框架(十二)

const result = await e.enforce('张三', '/api/book', 'post');

使用这个库实现接口鉴权,太简单了。(上)——从零开始搭建一个高颜值后台管理系统全栈框架(十二)

假设我们现在有个根据id获取单个book的接口,根据restful规范,接口应该设计成/api/book/:id,从前端拿到的请求url是/api/book/1这样的,那我们怎么匹配这种情况呢。

改造csv,给user角色添加一个/api/book/:id接口权限

p, admin, /api/book, get
p, admin, /api/book, post
p, admin, /api/book, delete
p, user,  /api/book, get
p, user,  /api/book/:id, get
g, 张三, user
g, 李四, admin
const result = await e.enforce('张三', '/api/book/1', 'get');

这样肯定返回false,因为model匹配那里写的是==,明显/api/book/1不等于/api/book/:id。这时候就需要用到内置函数keyMatch2了。

改造model文件 使用这个库实现接口鉴权,太简单了。(上)——从零开始搭建一个高颜值后台管理系统全栈框架(十二)

使用这个库实现接口鉴权,太简单了。(上)——从零开始搭建一个高颜值后台管理系统全栈框架(十二)

从数据库中加载策略

前言

上面我们都是从csv中加载的策略,有人会说,谁的管理系统会把用户、角色、接口这些信息存到csv中,一般都是存到数据库中。casbin支持从数据库中加载策略数据,并且已经有人写好了库,可以直接使用。

实战

安装依赖

pnpm i typeorm-adapter --save
pnpm i mysql2 --save

创建数据库

通过typeorm创建casbin实例

在项目启动的时候,创建一个单例service,全局每个地方都可以使用。

import { Singleton, Autoload, Init, App } from '@midwayjs/core';
import * as koa from '@midwayjs/koa';
import { Enforcer, newEnforcer } from 'casbin';
import { join } from 'path';
import TypeORMAdapter from 'typeorm-adapter';

@Autoload()
@Singleton()
export class CasbinService {
  @App()
  app: koa.Application;
  enforcer: Enforcer;

  @Init()
  async init() {
    const casbinModelPath = join(this.app.getBaseDir(), '/basic_model.conf');

    const adapter = await TypeORMAdapter.newAdapter({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: '12345678',
      database: 'casbin-demo',
    });

    // 这里创建casbin实例,第二个参数由以前的csv改成了从数据库中加载
    const e = await newEnforcer(casbinModelPath, adapter);

    // 从数据库中加载策略
    await e.loadPolicy();

    this.enforcer = e;
  }
}

启动项目后,发现数据库中自动创建了一个表。

使用这个库实现接口鉴权,太简单了。(上)——从零开始搭建一个高颜值后台管理系统全栈框架(十二)

把csv中的数据存迁移到数据库中

使用这个库实现接口鉴权,太简单了。(上)——从零开始搭建一个高颜值后台管理系统全栈框架(十二)

改造home.controler代码

import { App, Controller, Get, Inject } from '@midwayjs/core';
import * as koa from '@midwayjs/koa';
import { CasbinService } from '../casbin';

@Controller('/')
export class HomeController {
  @App()
  app: koa.Application;
  @Inject()
  casbinService: CasbinService;

  @Get('/')
  async home(): Promise<boolean> {
    const result = await this.casbinService.enforcer.enforce(
      '张三',
      '/api/book/1',
      'get'
    );

    return result;
  }
}

测试一下

使用这个库实现接口鉴权,太简单了。(上)——从零开始搭建一个高颜值后台管理系统全栈框架(十二)

总结

上面带着大家简单的入了一下门,至于怎么把系统中的用户、角色、接口信息转换成策略表的数据格式存到数据库中,我会在下一篇文章中以实战的方式分享给大家。

项目体验地址:fluxyadmin.cn/user/login

前端仓库地址:github.com/dbfu/fluxy-…

后端仓库地址:github.com/dbfu/fluxy-…