安全的权限控制必须在后端handler中完成,而非HTML模板内;应使用Casbin动态校验sub/obj/act三元组,并将布尔结果传入模板做存在性判断,禁止在模板中硬编码角色逻辑。

模板里直接判断用户角色会埋雷
Gin 的 HTML 模板(html/template)本身不支持运行时权限校验逻辑,硬要在 {{if .Role == "admin"}} 这类表达式里做判断,等于把权限规则写死在前端——角色字段被篡改、模板缓存未刷新、上下文漏传 .Role 都会导致越权显示。真正安全的控制点必须落在后端渲染前,而不是模板渲染时。
用 c.HTML() 前过滤好可渲染的数据结构
权限控制应发生在 handler 内部:从 JWT 或 session 解析出用户角色和权限列表,查数据库或 Casbin 获取该角色允许访问的菜单/按钮集合,再构造一个精简的 menuTree 或 btnMap 传入模板。这样模板只需做“是否存在”,不做“是否合法”判断。
- 不要传原始
user.Role,而是传user.PermitMenus = []string{"/admin/users", "/admin/logs"} - 按钮级控制更推荐传 map:
btnPerm = map[string]bool{"user:delete": true, "post:edit": false} - 模板中写
{{if .btnPerm "user:delete"}}{{end}},语义清晰且无逻辑泄露
router.Group().Use() 对模板路由无效
Gin 的中间件绑定只影响注册在该分组下的 HTTP 路由(如 GET /admin),不影响模板渲染路径本身。比如你写了 admin := router.Group("/admin"); admin.Use(AuthMiddleware()),但 admin.GET("/index", IndexHandler) 渲染的 index.html 仍需在 IndexHandler 里手动校验权限——中间件不会自动阻止未授权用户访问该 HTML 页面。
- 错误做法:以为加了
Use(AuthMiddleware())就能拦住所有/admin/*页面请求 - 正确做法:在每个返回 HTML 的 handler 开头调用
checkUserAuthority(c),失败则c.Redirect(http.StatusFound, "/login")或c.AbortWithStatus(http.StatusForbidden) - 特别注意:静态资源(
/static/)、健康检查(/ping)若挂载在受保护分组下,也会被中间件拦截,需加白名单逻辑
Casbin + 模板变量组合才是生产级方案
单纯靠角色字符串匹配(如 role == "admin")无法支撑复杂权限场景。真实项目应让 Casbin 在 handler 中完成 e.Enforce(sub, obj, act) 判断,其中 obj 是菜单路径(如 "menu:admin/users"),act 是操作类型("view" 或 "button"),然后把结果转为布尔值塞进模板变量。
- 示例:在
IndexHandler中执行canViewUsers, _ := e.Enforce(strconv.Itoa(int(roleID)), "menu:admin/users", "view") - 传参:
c.HTML(http.StatusOK, "index.html", gin.H{"canViewUsers": canViewUsers}) - 模板:
{{if .canViewUsers}}@#@#@#@#@#@#@#@#@#@0{{end}} - 关键点:Casbin 策略可动态更新,无需重启服务;而硬编码角色判断一旦上线就锁死逻辑
最易被忽略的是按钮权限的粒度——菜单可见不等于按钮可用,一个页面里可能同时存在“编辑”“导出”“审核”三个按钮,它们的权限来源可能完全不同。务必为每个按钮定义独立的 sub/obj/act 三元组,并在模板中逐个判断,别图省事合并成一个大开关。











