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

往期回顾

前端框架搭建------从零开始搭建一个高颜值后台管理系统全栈框架(一)

后端框架搭建------从零开始搭建一个高颜值后台管理系统全栈框架(二)

实现登录功能jwt or token+redis?------从零开始搭建一个高颜值后台管理系统全栈框架(三)

封装axios,让请求变得丝滑------从零开始搭建一个高颜值后台管理系统全栈框架(四)

实现前后端全自动化部署,解放你的双手。------从零开始搭建一个高颜值后台管理系统全栈框架(五)

雪花算法,附件方案,邮箱验证,修改密码。------从零开始搭建一个高颜值后台管理系统全栈框架(六)

基于react-router v6实现动态菜单、动态路由。内含vue动态路由实现。------从零开始搭建一个高颜值后台管理系统全栈框架(七)

通过RBAC模型实现前后端动态菜单和动态路由------从零开始搭建一个高颜值后台管理系统全栈框架(八)

使用黑科技实现前端按钮权限控制,太优雅了。------从零开始搭建一个高颜值后台管理系统全栈框架(九)

集成WebSocket实现用户权限变更消息推送,自动刷新。------从零开始搭建一个高颜值后台管理系统全栈框架(十)

通过ip获取用户登录地点,实现登录日志功能。------从零开始搭建一个高颜值后台管理系统全栈框架(十一)

前言

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

接口鉴权这里我推荐使用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项目。

sh 复制代码
npm init midway

安装casbin依赖

sh 复制代码
pnpm i casbin --save

创建casbin模型描述文件

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

ini 复制代码
[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文件,文件内容如下:

csv 复制代码
p, alice, data1, read
p, bob, data2, write

后面讲解这些内容的含义

在home.controler中使用casbin方法

ts 复制代码
// 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(...) 函数中的参数。

config 复制代码
[request_definition]
r = sub, obj, act

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

小结

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

policy_definition

描述

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

ini 复制代码
[policy_definition]
p = sub, obj, act
p2 = sub, act

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

arduino 复制代码
p, alice, data1, read
p2, bob, write-all-objects

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

css 复制代码
(alice, data1, read) -> (p.sub, p.obj, p.act)
(bob, write-all-objects) -> (p2.sub, p2.act)

小结

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

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

ini 复制代码
[policy_definition]
p = sub, obj, act

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

arduino 复制代码
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时,该如何对多个决策结果进行集成以实现统一决策。

小结

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

ini 复制代码
[policy_effect]
e = some(where (p.eft == allow))

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

策略定义中

ini 复制代码
[policy_definition]
p = sub, obj, act, eft

csv中

arduino 复制代码
p, alice, data1, read, allow
p, bob, data2, write, allow

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

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

csv中添加一条数据:

arduino 复制代码
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] 是策略匹配器的定义。 匹配器是表达式。 它确定了如何根据请求评估策略规则。

css 复制代码
[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act

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

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

内置匹配器函数

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

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

自定义函数

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

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

测试一下

ts 复制代码
const result = await e.enforce('b', 'data2', 'write');

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

ts 复制代码
const result = await e.enforce('bb', 'data2', 'write');

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

小结

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

RBAC模型实战

前言

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

model

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

ini 复制代码
[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官方文档有讲解配置的含义。

arduino 复制代码
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文件。

javascript 复制代码
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角色。

ts 复制代码
const result = await e.enforce('张三', '/api/book', 'get');
ts 复制代码
const result = await e.enforce('张三', '/api/book', 'post');

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

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

javascript 复制代码
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
ts 复制代码
const result = await e.enforce('张三', '/api/book/1', 'get');

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

改造model文件

从数据库中加载策略

前言

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

实战

安装依赖

sh 复制代码
pnpm i typeorm-adapter --save
pnpm i mysql2 --save

创建数据库

这个不会自动创建数据库,需要我们自己建一个数据库,使用工具连接数据库创建casbin-demo数据库。不用自己建表,typeorm-adapter会自动帮我们建表。数据库方面的知识可以看下我这篇文章juejin.cn/post/723673...

通过typeorm创建casbin实例

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

ts 复制代码
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代码

ts 复制代码
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-...

相关推荐
清灵xmf2 分钟前
揭开 Vue 3 中大量使用 ref 的隐藏危机
前端·javascript·vue.js·ref
su1ka1117 分钟前
re题(35)BUUCTF-[FlareOn4]IgniteMe
前端
测试界柠檬9 分钟前
面试真题 | web自动化关闭浏览器,quit()和close()的区别
前端·自动化测试·软件测试·功能测试·程序人生·面试·自动化
多多*10 分钟前
OJ在线评测系统 登录页面开发 前端后端联调实现全栈开发
linux·服务器·前端·ubuntu·docker·前端框架
2301_8010741510 分钟前
TypeScript异常处理
前端·javascript·typescript
ᅠᅠᅠ@11 分钟前
异常枚举;
开发语言·javascript·ecmascript
小阿飞_11 分钟前
报错合计-1
前端
hai4058712 分钟前
Spring Boot中的响应与分层解耦架构
spring boot·后端·架构
caperxi13 分钟前
前端开发中的防抖与节流
前端·javascript·html
霸气小男13 分钟前
react + antDesign封装图片预览组件(支持多张图片)
前端·react.js