场景假设

    假设商品查询服务是中台服务,上游有着很多的业务方。对于大多数业务方来说,中台提供的能力是够用的,但有一个业务方突然站出来说,我这边的平台对信息的违规审查异常严格,你们提供的图片审查能力是不够用的,需要做升级。

    于是我们找到了专门做图片审查的第三方公司。他们说可以很容易的接入使用,但是得收费,为了满足上游业务方的诉求,我们经过开会讨论和向上申请决定可以使用收费服务。但是领导还要求尽量节省开支,所以要求不严格的渠道还继续使用我们自己的图审查能力。 首先想到的是可以使用排他网关,如果是这个渠道走三方图片审查服务,如果是其他渠道就走默认图片审查。当下这个问题解决了,但是留下了一些问题:

    🟢 如果再新增其他渠道是不是还要修改判断逻辑

    🟢 如果又有其他渠道要使用新的风控组件审查图片,是不是还得继续新增判断逻辑、新增服务节点、修改流程图

    🟢 在中台产品看来,商品查询流程图上只有一个图片审查的功能,但是引入排他网关判断后会让商品查询的流程图变得不那么纯粹,不仅复杂难以理解,还包含了上游业务的特殊逻辑在里面,变得不易维护

     应对这种问题,是时候使用 Kstry 的RBAC(Role-based access control)模式了

# 4.1 RBAC初体验

增加图片校验服务:

@TaskComponent(name = "risk-control")
public class RiskControlService {

    // 已有的本地图片审查服务
    @TaskService(name = "check-img")
    public void checkImg(CheckInfo checkInfo) {

        AssertUtil.notNull(checkInfo);
        AssertUtil.notBlank(checkInfo.getImg());
        log.info("check img: " + checkInfo.getImg());
    }

    // 新增的三方图片审查服务
    @TaskService(name = "check-img", ability = "triple")
    public void tripleCheckImg(CheckInfo checkInfo) {

        AssertUtil.notNull(checkInfo);
        AssertUtil.notBlank(checkInfo.getImg());
        log.info("triple check img: " + checkInfo.getImg());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

    🟢 @TaskService注解增加了ability = RiskControlCompKey.triple属性,代表检测服务新增服务能力,名字是:triple

注册角色:

@Component
public class RoleRegister implements BusinessRoleRegister {

    @Override
    public List<BusinessRole> register() {

        List<String> list = Lists.newArrayList();
        list.add("r:check-img@triple");// r:服务名称@能力名,能力名对应 @TaskService 的 ability 属性值
        List<Permission> permissions = PermissionUtil.permissionList(String.join(",", list));

        Role role = new ServiceTaskRole();
        role.addPermission(permissions);
        BusinessRole businessRole = new BusinessRole("special-channel", "kstry-demo-goods-show", role);
        return Lists.newArrayList(businessRole);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

    🟢 实现BusinessRoleRegister接口,注册角色

    🟢 注册类需要托管给 Spring 容器

修改入参:

@RestController
@RequestMapping("/goods")
public class GoodsController {

    @Resource
    private StoryEngine storyEngine;

    @PostMapping("/show")
    public GoodsDetail showGoods(@RequestBody GoodsDetailRequest request) {

        StoryRequest<GoodsDetail> req = ReqBuilder.returnType(GoodsDetail.class).startId("kstry-demo-goods-show").request(request).build();
        // 新增部分,如果 businessId 不为空,设置到 Kstry 入参中
        if (StringUtils.isNotBlank(request.getBusinessId())) {
            req.setBusinessId(request.getBusinessId());
        }
        TaskResponse<GoodsDetail> fire = storyEngine.fire(req);
        if (fire.isSuccess()) {
            return fire.getResult();
        }
        return null;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

测试(未设置 businessId 时):

// 入参
{
    "id": 1,
    "source": "app"
}
// 日志:check img: https://xxx.png
1
2
3
4
5
6

测试(设置 businessId 时):

// 入参
{
    "id": 1,
    "source": "app",
    "businessId": "special-channel"
}
// 日志:triple check img: https://xxx.png
1
2
3
4
5
6
7

    🟢 未传入 businessId 时,使用的是内部图片审查服务,传入businessId=special-channel后使用三方图片审查服务

# 4.2 注册角色

# 4.2.1 权限分类

权限有四种分类

    🟢 COMPONENT_SERVICE:带服务组件名的服务权限。以pr:作为前缀,服务组件名和服务节点名以@符号分隔。举例:pr:goods@init-base-info

    🟢 COMPONENT_SERVICE_ABILITY:带服务组件名的服务能力权限。以pr:作为前缀,服务组件名、服务节点名、能力点名以@符号分隔。举例:pr:risk-control@check-img@triple

    🟢 SERVICE:普通服务权限。以r:作为前缀,只有服务节点名。举例:r:init-base-info

    🟢 SERVICE_ABILITY:服务能力权限。以r:作为前缀,服务节点名和能力点名以@符号分隔。举例:r:check-img@triple

    配置中r:pr:前缀修饰的权限在一般情况下是等价的。只有在不同服务组件名下定义了相同服务节点名时才必须使用pr:前缀的权限。如若不然框架将无法区分是哪个服务组件定义的服务节点

上面的角色注册步骤可以修改成如下格式:

@Component
public class RoleRegister implements BusinessRoleRegister {

    @Override
    public List<BusinessRole> register() {

        List<String> list = Lists.newArrayList();
        list.add("pr:goods@init-base-info");
        list.add("r:check-img@triple");
        list.add("r:get-logistic-insurance");
        list.add("r:get-shopInfo-goodsId");
        list.add("r:detail-post-process");
        list.add("r:init-sku");
        list.add("r:get-evaluation-info");
        list.add("r:get-goods-ext-info");
        list.add("r:get-order-info");
        List<Permission> permissions = PermissionUtil.permissionList(String.join(",", list));

        Role role = new BasicRole();
        role.addPermission(permissions);
        // businessId:special-channel
        // startId:kstry-demo-goods-show
        BusinessRole businessRole = new BusinessRole("special-channel", "kstry-demo-goods-show", role);
        return Lists.newArrayList(businessRole);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

    🟢 使用 cn.kstry.framework.core.util.PermissionUtil可以将字符串解析成 Permission 对象

    🟢 ServiceTaskRole继承自BasicRole具有后者的全部能力。在后者的基础上ServiceTaskRole在未分配服务能力节点权限时,默认就具备了服务节点的权限。流程执行中之所以未分配角色就可以执行定义的服务节点,是因为框架执行时默认就为请求分配了ServiceTaskRole

# 4.2.2 角色关系注册

角色分配权限:

Role role = new BasicRole();
role.addPermission(permissions);
1
2

注册角色关系:

    🟢 新建类实现BusinessRoleRegister接口代表需要注册角色关系,将该类托管至 Spring 容器

    🟢 实现接口中List<BusinessRole> register()方法,每一个BusinessRole都代表一个 startId、businessId 和 Role 的对应关系

    🟢 BusinessRole 可以指定:

        🔷 一个或多个 startId 与 Role 的对应关系

        🔷 一个或多个 businessId 和一个或多个 startId 与 Role 的对应关系

public BusinessRole(String startId, Role role);

public BusinessRole(List<String> startIdList, Role role);

public BusinessRole(String businessId, String startId, Role role);

public BusinessRole(String businessId, List<String> startIdList, Role role);

public BusinessRole(List<String> businessIdList, String startId, Role role);

public BusinessRole(List<String> businessIdList, List<String> startIdList, Role role);
1
2
3
4
5
6
7
8
9
10
11

# 4.3 匹配角色

# 4.3.1 静默匹配

    每执行一个 Story 时,入参中 startId 是必传参数,businessId 参数选填。容器会根据这两个值来匹配 BusinessRole,如果匹配成功,拿到的 BusinessRole 中含有承载着权限的角色对象,如果角色对象不为空,每将要执行一个节点时,都会判断角色中是否含有当前要执行节点的权限,判断依据是:权限的 identityId 和identityType 与当前服务节点的@TaskService.name@TaskService.ability 等属性相对应

根据 startId、businessId 匹配 BusinessRole 情况分类:

    🟢 Story入参只传入startId:

        🔷 匹配 businessIdList.isEmpty() 并且 startIdList.contains(startId) 的 BusinessRole,选取第一个匹配成功的返回

    🟢 Story入参传入 startId 和 businessId:

        🔷 匹配 businessIdList.contains(businessId) 并且 startIdList.contains(startId) 的 BusinessRole,选取第一个匹配成功的返回

        🔷 如果上一步返回为空,匹配 businessIdList.isEmpty() 并且 startIdList.contains(startId) 的 BusinessRole,选取第一个匹配成功的返回

# 4.3.2 显示指定

@RestController
@RequestMapping("/goods")
public class GoodsController {

    @Resource
    private StoryEngine storyEngine;

    @PostMapping("/show")
    public GoodsDetail showGoods(@RequestBody GoodsDetailRequest request) {

        // 创建角色,调用时显示指定
        Role role = new BasicRole();
        StoryRequest<GoodsDetail> req = ReqBuilder.returnType(GoodsDetail.class).role(role).startId("kstry-demo-goods-show").request(request).build();

        TaskResponse<GoodsDetail> fire = storyEngine.fire(req);
        if (fire.isSuccess()) {
            return fire.getResult();
        }
        return null;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

    🟢 执行 Story 前如果显示指定了角色,角色的静默匹配会失效

# 4.3.3 角色为空

    一个Story中,调用前未显示指定角色并且静默匹配也失败时,系统会为当前请求分配一个默认角色ServiceTaskRole。使用该角色,无需授权就可以执行全部的服务节点,但是一切指定了 @TaskService.ability 属性的服务能力节点都会失效。相当于未开启RBAC模式,上面流程编排的例子全是这种情况。

    如果ServiceTaskRole分配了服务能力节点的权限,与之对应的服务节点权限将不允许被分配,否则会出现匹配到多个能力的异常

# 4.4 动态修改权限

场景假设

在RBAC模式下,有一个需求:当“初始化商品基本信息”后流程才有权限“加载SKU信息”

角色关系注册时,不再添加”加载SKU信息“服务节点的权限:

@Component
public class RoleRegister implements BusinessRoleRegister {

    @Override
    public List<BusinessRole> register() {

        List<String> list = Lists.newArrayList();
        list.add("r:init-base-info");
        list.add("r:check-img@triple");
        list.add("r:get-logistic-insurance");
        list.add("r:get-shopInfo-goodsId");
        list.add("r:detail-post-process");
//        list.add("r:init-sku");
        list.add("r:get-evaluation-info");
        list.add("r:get-goods-ext-info");
        list.add("r:get-order-info");
        List<Permission> permissions = PermissionUtil.permissionList(String.join(",", list));

        Role role = new BasicRole();
        role.addPermission(permissions);
        BusinessRole businessRole = new BusinessRole("special-channel", "kstry-demo-goods-show", role);
        return Lists.newArrayList(businessRole);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

定义角色自定义组件:

@TaskComponent(name = "goods-custom-role")
public class GoodsCustomRole {

    @CustomRole
    @TaskService(name = "goods-detail")
    public void goodsDetail(@StaTaskParam GoodsDetail goodsDetail, Role role) {

        if (goodsDetail != null && role != null) {
            BasicRole basicRole = new BasicRole();
            basicRole.addPermission(PermissionUtil.permissionList("r:init-sku"));
            role.addParentRole(Sets.newHashSet(basicRole));
            log.info("add permission: {}", JSON.toJSONString(basicRole));
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

    🟢 使用@CustomRole@TaskService注解声明该服务节点是自定义角色节点

    🟢 只要服务节点方法参数中有Role类型的变量,会被自动注入在Story中流转的Role对象,其他类型参数值获取方式同普通的服务节点。但普通服务节点(非自定义角色服务节点)参数中如果出现Role类型时不会被赋值,会拿到null

增加自定义角色声明:

image-20211216003124692

    🟢 定义custom-role属性,指定该服务节点执行完成后需要进行一次自定义角色操作。值格式:component-name@service-name

    🟢 除了声明custom-role属性外,自定义角色节点也可以像普通服务节点一样在流程图中定义并参与执行

测试日志:

- add permission: {"parentRole":[],"permission":{"SERVICE_TASK":[{"identityId":"init-sku","identityType":"SERVICE_TASK"}]}}
1

# 4.5 角色匹配表达式

场景假设

公司为了把控成本,要求如果使用到了图审查付费服务的业务要做好统计工作,记录使用次数,方便后面公司财务做账。也就是说,使用了三方服务的请求需要被统计,走内部审查服务的无需统计。换句话说在RBAC模式下只有使用到了 r:check-img@triple 权限的 Story 需要进行统计

BPMN图示如下:

image-20211216010953498

    🟢 r:check-img@triple 表达式代表当前执行的Story中Role不为空,并且含有该权限

    🟢 !r:check-img@triple 表达式代表当前执行的Story中Role为空,或者Role不为空但是不含有该权限

    🟢 除!之外表达式还支持()&|四个符号组成的组合运算符。比如:(r:init-base-info||r:init-sku)&&!r:get-order-info。翻译就是:(有加载商品基础信息的权限||有加载商品SKU的权限)&&没有获取订单信息的权限

# 4.6 动态获取角色

@Component
public class RoleRegister implements DynamicRole {

    private final Role role4;

    {
        role4 = new ServiceTaskRole();
        role4.addPermission(PermissionUtil.permissionList(KeyUtil.pr("hello_service", "say_hello1")));
    }

    @Override
    public Optional<Role> getRole(String key) {
        if (Objects.equals(TaskServiceUtil.joinName("story-def-test-role-004", "aq-110"), key)) {
            return Optional.of(role4);
        }
        return Optional.empty();
    }

    @Override
    public String getKey(ScopeDataQuery scopeDataQuery) {
        return DynamicRole.super.getKey(scopeDataQuery);
    }

    @Override
    public long version(String key) {
        return -1;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

    🟢 创建实现DynamicRole接口的类实例,放入Spring容器中,动态获取角色组件即会生效,DynamicRole接口提供一下三个方法:

        🔷 getRole(String key):传入key,根据key动态判断生成角色对象并返回

        🔷 getKey(ScopeDataQuery scopeDataQuery):从ScopeDataQuery中获取参数,生成上面方法传入的key。该方法有默认实现,不重写时key默认是:开始事件Id@业务Id

        🔷 version(String key):控制角色版本。获取角色时,如果版本未更新会先从缓存中取,缓存有效期1天。该方法有默认实现,不重写时默认返回-1,代表不使用角色缓存

    🟢 需要与上面介绍的BusinessRoleRegister做区分,实现该接口进行角色定义的方式是静态的,配置完之后如果想要修改需要重启应用,而DynamicRole却是动态的

    🟢 与动态流程思路一样,流程执行时先获取静态角色,未获取到时再尝试动态获取,如果都没有获取到则创建默认角色:ServiceTaskRole