基本介绍
lina-core的路由体系围绕版本前缀、中间件链、权限声明和插件扩展接口四个维度统一设计,主框架通过/api/v1前缀管理自身API版本,通过有序的中间件链落地CORS、鉴权与权限治理,通过g.Meta结构体标签将接口属性与代码一体化维护,并为两类插件提供差异化的路由接入策略。
API 版本管理
主框架的所有控制面API统一挂载在/api/v1路由组下。版本前缀通过server.Group声明,后续扩展到/api/v2时只需新增分组,已有v1接口不受影响。
server.Group("/api/v1", func(group *ghttp.RouterGroup) {
bindHostAPIMiddlewares(group, middlewareSvc)
bindPublicStaticAPIRoutes(group, ...)
bindProtectedStaticAPIRoutes(group, middlewareSvc, ...)
})
| 路径前缀 | 用途 |
|---|---|
/api/v1 | 主框架当前稳定API版本,包含认证、用户、权限、插件治理等控制面接口 |
/x/{plugin-id}/... | 源码插件和动态插件共同使用的插件API命名空间;APIPrefix()返回/x/{plugin-id},后续路径由插件自行组织 |
/x-assets/{plugin-id}/{version} | 插件声明式公开静态资源托管入口 |
/admin | 默认管理工作台前端路由基准;本地默认通过http://localhost:5666/admin访问,不属于主框架控制面API |
/ | 根路径不再默认回退到管理工作台,通常留给门户、自定义页面或源码插件自管路由 |
版本管理的设计原则是:路由组即版本边界。不同版本的API在同一服务进程内共存,各自拥有独立的中间件配置和处理器集合,不依赖Content-Type或请求头进行版本协商。
中间件体系
主框架将中间件分为两类:挂载在路由组上的请求链中间件,以及注册在服务器级别的全局中间件。请求链中间件按照声明顺序依次执行,任意一个中间件调用r.ExitAll()后链条即终止。
公共基础中间件
以下中间件对/api/v1路由组和插件API路由组/x均生效,构成所有请求的基础处理链:
| 中间件 | 作用 |
|---|---|
ghttp.MiddlewareNeverDoneCtx | 将请求Context替换为永不取消的副本,防止客户端断连导致业务逻辑提前终止 |
middlewareSvc.Response | 统一序列化JSON响应体,localize业务错误文案,处理304、204及流式响应透传 |
middlewareSvc.CORS | 执行CORSDefault,允许跨域请求,处理OPTIONS预检 |
middlewareSvc.RequestBodyLimit | 非multipart请求默认限制100MB;multipart上传请求按sys.upload.maxSize配置动态计算上限 |
middlewareSvc.Ctx | 注入业务上下文(用户身份占位、租户占位、请求locale),设置Content-Language响应头 |
鉴权与权限中间件
以下中间件仅在受保护的路由子组中挂载,公开接口(如登录、健康探针)不经过这一层:
| 中间件 | 作用 |
|---|---|
middlewareSvc.Auth | 从Authorization: Bearer <token>解析JWT,校验Token签名与会话有效性,写入用户身份至请求上下文 |
middlewareSvc.Tenancy | 根据请求上下文解析租户身份,注入租户ID;未启用多租户时默认注入平台租户 |
middlewareSvc.Permission | 从DTO的g.Meta标签或手动声明读取permission字段,校验当前用户是否拥有所需权限 |
中间件执行顺序如下图所示:
源码插件可用的中间件
主框架通过RouteMiddlewares接口将上述中间件发布给源码插件,插件按需组合使用,无需直接依赖主框架内部包:
routes.Group(routes.APIPrefix(), func(group pluginhost.RouteGroup) {
group.Middleware(
middlewares.NeverDoneCtx(),
middlewares.HandlerResponse(),
middlewares.CORS(),
middlewares.RequestBodyLimit(),
middlewares.Ctx(),
)
// 公开路由子组
group.Group("/", func(group pluginhost.RouteGroup) {
group.Bind(demoController.Ping)
})
// 受保护路由子组
group.Group("/", func(group pluginhost.RouteGroup) {
group.Middleware(
middlewares.Auth(),
middlewares.Tenancy(),
middlewares.Permission(),
)
group.Bind(demoController.ListRecords, ...)
})
})
鉴权路由设计
主框架将路由分为公开路由和受保护路由两层,以路由子组的中间件差异来区分,而非依赖路径惯例或特殊标记。
路由分层结构
权限声明方式
受保护接口的权限标识通过DTO的g.Meta标签内联声明,Permission中间件在运行时读取该标识并校验当前用户的角色权限集合:
type UserListReq struct {
g.Meta `path:"/users" method:"get" tags:"User" summary:"List users" permission:"user:list"`
Page int `json:"page"`
PageSize int `json:"pageSize"`
}
Auth中间件的鉴权流程如下:
- 从
Authorization请求头读取Bearer Token - 解析
JWT,验证签名与过期时间 - 调用
SessionStore.TouchOrValidate刷新会话活跃时间,并验证会话是否仍然有效(支持强制登出和超时清理) - 将用户身份(用户
ID、租户ID、TokenId等)写入请求上下文
Permission中间件的权限校验流程如下:
- 从
g.Meta标签读取permission字段,支持多个权限用逗号分隔 - 加载当前用户的访问上下文(权限列表、数据范围)
- 匹配所需权限,任一满足即通过(
OR语义);通配符*:*:*表示超级管理员直接放行
鉴权机制的详细设计(包括JWT签发、会话管理、RBAC权限模型)请参见权限管理章节。
API 标签一体化管理
lina-core基于g.Meta机制,将接口的所有属性——路径、方法、分组标签、摘要、描述、权限、MIME类型等——全部内联在请求DTO的结构体标签中,实现代码与文档同源。
主框架接口标签示例
type CreateRecordReq struct {
g.Meta `path:"/records" method:"post" mime:"multipart/form-data" tags:"Source Plugin Demo" summary:"Create source plugin sample record" dc:"创建示例记录" permission:"linapro-demo-source:example:create"`
Title string `json:"title" v:"required|length:1,128" dc:"记录标题"`
Content string `json:"content" dc:"记录内容"`
}
源码插件DTO中的path是控制器绑定到插件路由组后的相对路径。最终公开路径由源码插件注册的routes.APIPrefix()和DTO path共同组成,例如/x/linapro-demo-source/records。
动态插件接口标签示例
动态插件使用gmeta.Meta(而非g.Meta,沙箱环境的组件依赖缘故),标签内额外增加access和operLog字段:
type CreateDemoRecordReq struct {
gmeta.Meta `path:"/demo-records" method:"post" tags:"Dynamic Plugin Demo" summary:"创建示例记录" access:"login" permission:"linapro-demo-dynamic:record:create" operLog:"create"`
Title string `json:"title" v:"required|length:1,128"`
Content string `json:"content"`
}
常用标签字段说明
| 标签字段 | 适用范围 | 说明 |
|---|---|---|
path | 主框架 / 源码插件 / 动态插件 | 接口路由路径 |
method | 主框架 / 源码插件 / 动态插件 | HTTP方法,如get、post |
tags | 主框架 / 源码插件 / 动态插件 | 接口分组标签,用于OpenAPI文档分类 |
summary | 主框架 / 源码插件 / 动态插件 | 接口简介,展示在文档和插件管理页 |
dc | 主框架 / 源码插件 | 接口详细描述(description缩写) |
permission | 主框架 / 源码插件 / 动态插件 | 权限标识,由Permission中间件强制校验 |
mime | 主框架 / 源码插件 | 请求体MIME类型,如multipart/form-data |
access | 动态插件 | 访问控制,public表示匿名,login表示需要登录 |
operLog | 动态插件 | 操作日志类型,如create、update、delete、other |
这种方式让接口定义、文档元数据和权限声明集中在同一个DTO文件中,主框架自动从标签中聚合OpenAPI文档,不需要单独维护接口注释或文档文件,从根本上消除了代码与文档之间的漂移风险。
源码插件路由策略
源码插件随主框架编译交付,通过pluginhost.HTTPRegistrar提供的Routes()接入主框架路由器。源码插件的插件API必须进入自己的/x/{plugin-id}命名空间;同时,源码插件仍可以注册非保留公开路由,用于门户页面、公开资产、自管fallback或其他HTTP响应逻辑。
注册方式
源码插件在init()阶段通过回调声明路由注册函数,主框架启动时在registerSourcePluginHTTPRoutes阶段统一触发回调:
plugin.HTTP().RegisterRoutes(
pluginhost.ExtensionPointHTTPRouteRegister,
pluginhost.CallbackExecutionModeBlocking,
registerRoutes,
)
registerRoutes回调接收HTTPRegistrar参数,插件通过Routes().Group()创建路由组,通过Middlewares()获取主框架发布的中间件目录:
func registerRoutes(ctx context.Context, registrar pluginhost.HTTPRegistrar) error {
routes := registrar.Routes()
middlewares := routes.Middlewares()
routes.Group(routes.APIPrefix(), func(group pluginhost.RouteGroup) {
// 组合公共基础中间件
group.Middleware(
middlewares.NeverDoneCtx(),
middlewares.HandlerResponse(),
middlewares.CORS(),
middlewares.RequestBodyLimit(),
middlewares.Ctx(),
)
// 公开子组
group.Group("/", func(group pluginhost.RouteGroup) {
group.Bind(demoController.Ping)
})
// 受保护子组(必须遵循 Auth -> Tenancy -> Permission 顺序)
group.Group("/", func(group pluginhost.RouteGroup) {
group.Middleware(
middlewares.Auth(),
middlewares.Tenancy(),
middlewares.Permission(),
)
group.Bind(demoController.CreateRecord, demoController.ListRecords, ...)
})
})
return nil
}
插件API命名空间与自定义路由
RouteRegistrar.APIPrefix()返回当前插件的强制API命名空间:
/x/{plugin-id}
源码插件业务接口必须在该命名空间下,后续路径由插件自行组织,例如:
/x/linapro-demo-source/records
/x/linapro-demo-source/records/{id}
源码插件不得在/x下注册其他插件的路径。主框架会拒绝越过自身/x/{plugin-id}边界的源码插件路由注册。
源码插件可以注册/、/portal/...、/assets/...等非保留路径。这种自由度带来的开发规范要求是:
- 避免与主框架路由冲突:主框架已占用
/api、/x和/x-assets,源码插件公开页面应使用明确的非保留命名空间;/admin通常留给默认工作台前端路由,不建议源码插件复用 - 避免插件间路由冲突:多个源码插件同时安装时,非保留路径冲突会导致路由注册失败并终止程序启动,开发者需确保路径唯一性
- 受保护路由必须遵循中间件顺序:凡是使用
middlewareSvc.Auth中间件的路由子组,必须按照middlewareSvc.Auth → middlewareSvc.Tenancy → middlewareSvc.Permission的顺序组合中间件,主框架有自动化测试保障这一约束
源码插件路由捕获
主框架在路由注册阶段会捕获源码插件注册的所有SourceRouteBinding,并将可文档化的接口聚合进主框架OpenAPI文档,开发者无需额外操作即可在接口文档中查看源码插件暴露的接口。
动态插件路由策略
动态插件(WASM插件)的路由由主框架完全管理,由于动态插件本身运行在沙箱中,插件本身不直接接触HTTP Server的路由注册机制,路由能力受到明确约束。
路由命名空间约束
动态插件的所有API路由强制挂载在/x/{plugin-id}/前缀下。后续路径来自动态插件自己的route contract,例如:
/x/linapro-demo-dynamic/backend-summary
/x/linapro-demo-dynamic/demo-records
/x/linapro-demo-dynamic/demo-records/{id}
这个约束由主框架在/x路由组上绑定的通配符处理器统一拦截后实现。插件无法绑定到主框架/api/v1控制面或任何/x之外的API路径,确保动态插件路由不会对主框架路由结构造成混乱。
路由声明方式
动态插件的路由通过内嵌在WASM产物中的RouteContract声明,而非运行时注册。主框架加载产物时解析路由契约,后续请求到达时由主框架侧的PrepareDynamicRouteMiddleware负责路径匹配:
// 动态插件路由契约(内嵌在 WASM 产物中)
type RouteContract struct {
Path string // 插件内部路径,如 /demo-records
Method string // HTTP 方法
Tags []string // 分组标签
Summary string // 接口简介
Access string // "public" 或 "login"
Permission string // 权限标识
Meta map[string]string // 插件自定义元数据
RequestType string // 反射分发时使用的请求类型名
}
Path字段是插件内部路径,主框架在对外暴露时自动拼接/x/{plugin-id}前缀。若内部路径是/demo-records,最终公开路径就是/x/{plugin-id}/demo-records。
动态路由请求处理流程
动态插件权限声明
动态插件路由的权限通过RouteContract的access和permission字段声明:
| 字段 | 取值 | 说明 |
|---|---|---|
access | public | 匿名访问,无需任何身份验证 |
access | login | 需要已登录身份,主框架验证JWT有效性 |
permission | 如linapro-demo-dynamic:record:create | 需要特定权限,由主框架查询用户权限集合校验 |
动态插件与源码插件路由策略对比
| 维度 | 源码插件 | 动态插件 |
|---|---|---|
| 路由注册方式 | 启动阶段通过HTTPRegistrar回调注册 | 运行时从WASM产物中解析路由契约 |
| 路由路径限制 | 插件API必须使用/x/{plugin-id};可额外注册非保留公开路径 | 强制在/x/{plugin-id}/前缀下 |
| 中间件组合 | 插件自行从RouteMiddlewares选择并组合 | 主框架统一管理,插件通过access字段影响鉴权行为 |
| 权限声明位置 | DTO的g.Meta标签 | RouteContract的permission字段 |
| OpenAPI 文档 | 自动聚合到主框架文档 | 主框架从路由契约中读取并聚合 |
| 路由冲突风险 | 开发者自行规避 | 主框架通过命名空间约束规避 |
全局中间件扩展
除了路由组级别的中间件,源码插件还可以通过GlobalMiddlewares()注册服务器级别的全局中间件,作用于服务器上所有匹配指定模式的请求:
err := registrar.GlobalMiddlewares().Bind(
pluginhost.MiddlewareScope("/*"),
func(r *ghttp.Request) {
// 插件的全局请求拦截逻辑(仅在插件启用时执行)
r.Middleware.Next()
},
)
主框架在全局中间件中自动注入插件启用状态检查,当插件被禁用时中间件逻辑自动跳过,开发者不需要在自己的中间件中手动处理插件状态。