Revel模板只能访问首字母大写的导出字段;小写字段如password、token等即使传入模板也会被静默忽略,不会报错但渲染为空,安全做法是在结构体中保持敏感字段小写并配合json:"-"标签彻底屏蔽。

模板里访问结构体字段前,先确认它是否导出
Revel 模板底层用的是 Go 的 text/template,它**只允许访问导出字段**——也就是首字母大写的字段名。如果你在模板中写 {{.User.Password}} 却什么也没渲染出来,大概率是因为 Password 字段在结构体里是小写开头(比如 password string),Go 反射机制直接跳过它,模板引擎也拿不到值,不会报错,只会静默忽略。
常见错误现象:
- 结构体字段名是
token、secretKey、dbConn这类小写开头 → 模板中完全不可见 - 即使你在控制器里把整个结构体传进模板,
{{.data.token}}依然为空字符串或 nil
所以“隐藏”私密字段最可靠的方式不是靠模板逻辑判断,而是从源头切断访问可能:保持字段小写。例如:
type User struct {
ID int64 `json:"id"`
Name string `json:"name"`
password string `json:"-"` // ✅ 小写 + json:"-"`
token string `json:"-"`
}
这样既不会被模板读取,也不会被 json.Marshal 输出,一并屏蔽了模板和 API 两路泄露风险。
别在模板里用 if/else 去“条件隐藏”敏感字段
有人会想:我干脆在模板里加个判断,比如 {{if ne .Env "prod"}}{{.User.password}}{{end}} —— 这种做法无效且危险。因为 .User.password 本身在模板作用域里就不存在,ne 比较的左边是空值,整个表达式恒为 false,但更重要的是:你已经在模板里写了这个字段名,等于暴露了字段意图;而且一旦结构体定义稍作调整(比如改成导出),它就立刻可见。
正确思路是:让敏感字段根本不出现在模板上下文里。控制器传参时主动过滤:
- 不要传整个
User实例,而是构造一个只含公开字段的视图结构体:type UserView { ID int64; Name string } - 或者用 map 显式控制键名:
c.RenderArgs["user"] = map[string]interface{}{"id": u.ID, "name": u.Name}
字段导出与否,和 JSON tag 没有必然关系
新手容易混淆:以为加了 json:"-" 就能阻止模板访问。其实这两者完全独立:json:"-" 只影响 json.Marshal 行为,对模板引擎零作用。同样,json:"password" 也不代表模板就能读到它——关键还是看字段名是否导出。
典型误操作:
-
password string `json:"password"`→ 模板仍不可见(小写) -
Password string `json:"-"`→ 模板可见(导出),但 JSON 不输出(tag 生效)→ ❌ 敏感字段意外暴露给前端 HTML
真正安全的组合只有一种:password string `json:"-"`(小写 + 禁止 JSON 序列化)。
需要动态控制字段可见性?用中间结构体而非模板逻辑
如果业务上真要按角色显示不同字段(比如管理员看 email,普通用户不看),不要在模板里写 {{if eq .Role "admin"}}{{.User.Email}}{{end}}。因为 .User.Email 本身必须是导出字段,意味着它已存在于模板上下文中,只是没渲染——源码审查或调试时仍可发现。
更稳妥的做法是在控制器层做裁剪:
- 定义两个结构体:
UserPublic和UserAdmin - 根据
c.Session.Get("role")决定传哪个实例给模板 - 确保
UserPublic里压根不包含Email字段
这种分离把“可见性策略”从模板(易绕过、难审计)移到 Go 代码(可测试、可追踪),也避免了模板里堆砌权限逻辑带来的维护负担。
最常被忽略的一点:字段是否导出,是编译期确定的,不是运行时可控的。所谓“隐藏”,本质是设计阶段的访问控制决策,不是模板里的 if 判断能补救的。


















