Skip to content

按业务动作授权 ​

面向正在决定要不要给路由起业务名称(例如 wms:warehouse.purge)的团队,也面向要为不属于自己的服务编写 DENY 和限流的平台作者。路由映射把请求的方法和路径变成动作和类型化资源。它同时也改变了哪些语句适用于该请求,这一点要在加映射之前弄清楚。

是否需要映射 ​

http:path:<path> 上的 http:<METHOD> 本身就是一套完整的授权语言:路径模式可以捕获变量,条件可以读取它们,权限只写 "action": "http:GET" 就够了。路由映射是它之上的一层名称。

需要路由映射的情况原因
URL 重构、版本化(/v1、/v2)后策略要保持不变动作名与路径无关
你要在控制台里列出权限并按名称授予目录按动作组织
请求授权和代码里的检查共用一套词汇POST /orders/42/cancel 与 checker.check(ctx, "order:cancel", "order/42") 是同一条语句
可以不用的情况原因
服务在 Spring Cloud Gateway 后面网关不加载路由映射(网关拓扑)
路径本身就是自然词汇的小服务http:GET 和 http:path:/orders/{id} 就够了
业务动作只在代码里检查AuthorizationChecker 直接接收动作

配置映射 ​

服务加载 cosec.authorization.routes.location 指定的映射。未设置时不加载;但如果 classpath:cosec-routes.json 存在,启动会失败,直到该属性指向这个文件(或者有意设为空)。启动日志会列出加载的每份映射、它的服务和路由条数。文件格式以及从 OpenAPI 生成见路由映射;一个完整的服务见不使用空间的应用。

一个请求只有一个动作 ​

请求要么匹配了映射,此时它就是该路由在类型化资源上的 service:verb;要么没有匹配,此时它就是路径上的 http:<METHOD>。绝不会两者兼有。所以:

  • service:verb 上的语句永远不适用于未映射的请求;
  • http:* 或 http:GET 上的语句永远不适用于已映射的请求。

限流规则像 DENY 一样按动作和资源匹配,所以同样如此:http:POST 和 http:path:/auth/login 上的规则,在 POST /auth/login 有了路由之后,就不再适用于该请求。

这对平台影响最大。针对 /admin/** 的平台 DENY 是写在 http:* 上的。服务团队给某个管理路径加了路由后,它的请求就不再是 http: 请求,DENY 也就不再匹配。CoSec 没有针对请求路径的条件,所以平台作者无法一次写出“无论是否映射都关闭”;DENY 要针对两种动作各写一次。

示例 ​

路由映射给 DELETE /admin/warehouses/{id} 起名 wms:warehouse.purge:

json
{
  "service": "wms",
  "routes": [
    {
      "method": "GET",
      "path": "/warehouses/{id}",
      "action": "wms:warehouse.read",
      "resource": "warehouse/{id}"
    },
    {
      "method": "DELETE",
      "path": "/admin/warehouses/{id}",
      "action": "wms:warehouse.purge",
      "resource": "warehouse/{id}"
    }
  ]
}

平台用 http:* 上的 DENY 关闭管理路径,而 admin 角色持有应用的全部权限,包括 wms:warehouse.purge:

json
{
  "schema": 2,
  "revision": 1,
  "publishedAt": "2026-11-01T08:00:00Z",
  "policies": [
    {
      "id": "login",
      "statements": [
        {
          "name": "login",
          "effect": "allow",
          "action": "http:POST",
          "resource": "http:path:/auth/login"
        }
      ]
    },
    {
      "id": "control-api",
      "statements": [
        {
          "name": "reach-the-control-api",
          "effect": "allow",
          "action": [
            "http:GET",
            "http:POST",
            "http:DELETE"
          ],
          "resource": "http:path:/cosec/control/v1/**",
          "condition": {
            "eq": [
              "subject.authenticated",
              true
            ]
          }
        }
      ]
    },
    {
      "id": "admin-closed",
      "statements": [
        {
          "name": "admin-paths-are-closed",
          "effect": "deny",
          "action": "http:*",
          "resource": "http:path:/admin/**"
        }
      ]
    }
  ],
  "bindings": [
    {
      "id": "login",
      "subject": "*",
      "app": "*",
      "space": "*",
      "policy": "login"
    },
    {
      "id": "control-api",
      "subject": "*",
      "app": "*",
      "space": "*",
      "policy": "control-api"
    },
    {
      "id": "admin-closed",
      "subject": "*",
      "app": "*",
      "space": "*",
      "policy": "admin-closed"
    }
  ]
}

wiki/examples/route-maps 里的场景展示了两面:

请求决策
DELETE /admin/x,没有对应路由:http:path:/admin/x 上的 http:DELETEDeny:平台 DENY 匹配
DELETE /admin/warehouses/7,已映射:warehouse/7 上的 wms:warehouse.purgePermit:DENY 不适用,角色允许

修法是在第一条语句旁再加一条针对该动作的语句:

json
{
  "schema": 2,
  "revision": 1,
  "publishedAt": "2026-11-01T08:00:00Z",
  "policies": [
    {
      "id": "login",
      "statements": [
        {
          "name": "login",
          "effect": "allow",
          "action": "http:POST",
          "resource": "http:path:/auth/login"
        }
      ]
    },
    {
      "id": "control-api",
      "statements": [
        {
          "name": "reach-the-control-api",
          "effect": "allow",
          "action": [
            "http:GET",
            "http:POST",
            "http:DELETE"
          ],
          "resource": "http:path:/cosec/control/v1/**",
          "condition": {
            "eq": [
              "subject.authenticated",
              true
            ]
          }
        }
      ]
    },
    {
      "id": "admin-closed",
      "statements": [
        {
          "name": "admin-paths-are-closed",
          "effect": "deny",
          "action": "http:*",
          "resource": "http:path:/admin/**"
        },
        {
          "name": "purge-is-closed",
          "effect": "deny",
          "action": "wms:warehouse.purge"
        }
      ]
    }
  ],
  "bindings": [
    {
      "id": "login",
      "subject": "*",
      "app": "*",
      "space": "*",
      "policy": "login"
    },
    {
      "id": "control-api",
      "subject": "*",
      "app": "*",
      "space": "*",
      "policy": "control-api"
    },
    {
      "id": "admin-closed",
      "subject": "*",
      "app": "*",
      "space": "*",
      "policy": "admin-closed"
    }
  ]
}

现在已映射的请求也被拒绝了(示例里的 scenarios-fixed)。限流规则也一样处理:为动作写一条,或两种都写。

检查清单 ​

  • 往映射里加路由?检查提到该路径的平台 DENY 和限流规则,并为它的动作重复一份。
  • 为不属于你的服务写平台 DENY 或限流?先问每个服务有没有映射,然后既写 http 形式,也写覆盖该路径的动作。
  • 请求的处理结果出乎意料?explain 和决策事件会显示请求被当作哪个动作和资源来决策(解释决策)。

由 OpenAPI 生成 ​

有了 cosec-openapi、springdoc 和 Spring Boot Actuator,cosecServiceGenerator/routes 会从 springdoc 扫描到的 OpenAPI 文档生成映射,cosecServiceGenerator/app 生成应用文档:简写权限的目录,每个动作一个,不需要手写策略,也没有角色。分组不是 spaced;应用使用空间时请把分组标为 spaced(角色权限)。生成会映射每一个操作,所以每条生成的路由都会离开上面那些 http 语句。

**下一步:**边缘上的同一个问题,见网关拓扑。

基于 Apache License 2.0 发布