跳到主要内容
版本:0.2.x

基本介绍

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业务错误文案,处理304204及流式响应透传
middlewareSvc.CORS执行CORSDefault,允许跨域请求,处理OPTIONS预检
middlewareSvc.RequestBodyLimitmultipart请求默认限制100MBmultipart上传请求按sys.upload.maxSize配置动态计算上限
middlewareSvc.Ctx注入业务上下文(用户身份占位、租户占位、请求locale),设置Content-Language响应头

鉴权与权限中间件

以下中间件仅在受保护的路由子组中挂载,公开接口(如登录、健康探针)不经过这一层:

中间件作用
middlewareSvc.AuthAuthorization: Bearer <token>解析JWT,校验Token签名与会话有效性,写入用户身份至请求上下文
middlewareSvc.Tenancy根据请求上下文解析租户身份,注入租户ID;未启用多租户时默认注入平台租户
middlewareSvc.PermissionDTOg.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, ...)
})
})

鉴权路由设计

主框架将路由分为公开路由受保护路由两层,以路由子组的中间件差异来区分,而非依赖路径惯例或特殊标记。

路由分层结构

权限声明方式

受保护接口的权限标识通过DTOg.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中间件的鉴权流程如下:

  1. Authorization请求头读取Bearer Token
  2. 解析JWT,验证签名与过期时间
  3. 调用SessionStore.TouchOrValidate刷新会话活跃时间,并验证会话是否仍然有效(支持强制登出和超时清理)
  4. 将用户身份(用户ID、租户IDTokenId等)写入请求上下文

Permission中间件的权限校验流程如下:

  1. g.Meta标签读取permission字段,支持多个权限用逗号分隔
  2. 加载当前用户的访问上下文(权限列表、数据范围)
  3. 匹配所需权限,任一满足即通过(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,沙箱环境的组件依赖缘故),标签内额外增加accessoperLog字段:

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方法,如getpost
tags主框架 / 源码插件 / 动态插件接口分组标签,用于OpenAPI文档分类
summary主框架 / 源码插件 / 动态插件接口简介,展示在文档和插件管理页
dc主框架 / 源码插件接口详细描述(description缩写)
permission主框架 / 源码插件 / 动态插件权限标识,由Permission中间件强制校验
mime主框架 / 源码插件请求体MIME类型,如multipart/form-data
access动态插件访问控制,public表示匿名,login表示需要登录
operLog动态插件操作日志类型,如createupdatedeleteother

这种方式让接口定义、文档元数据和权限声明集中在同一个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

动态路由请求处理流程

动态插件权限声明

动态插件路由的权限通过RouteContractaccesspermission字段声明:

字段取值说明
accesspublic匿名访问,无需任何身份验证
accesslogin需要已登录身份,主框架验证JWT有效性
permissionlinapro-demo-dynamic:record:create需要特定权限,由主框架查询用户权限集合校验

动态插件与源码插件路由策略对比

维度源码插件动态插件
路由注册方式启动阶段通过HTTPRegistrar回调注册运行时从WASM产物中解析路由契约
路由路径限制插件API必须使用/x/{plugin-id};可额外注册非保留公开路径强制在/x/{plugin-id}/前缀下
中间件组合插件自行从RouteMiddlewares选择并组合主框架统一管理,插件通过access字段影响鉴权行为
权限声明位置DTOg.Meta标签RouteContractpermission字段
OpenAPI 文档自动聚合到主框架文档主框架从路由契约中读取并聚合
路由冲突风险开发者自行规避主框架通过命名空间约束规避

全局中间件扩展

除了路由组级别的中间件,源码插件还可以通过GlobalMiddlewares()注册服务器级别的全局中间件,作用于服务器上所有匹配指定模式的请求:

err := registrar.GlobalMiddlewares().Bind(
pluginhost.MiddlewareScope("/*"),
func(r *ghttp.Request) {
// 插件的全局请求拦截逻辑(仅在插件启用时执行)
r.Middleware.Next()
},
)

主框架在全局中间件中自动注入插件启用状态检查,当插件被禁用时中间件逻辑自动跳过,开发者不需要在自己的中间件中手动处理插件状态。