登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

Go html/template 怎么安全把后端数据交给前端:别把 JSON 硬塞进 template.JS

来源:17golang原创

时间:2026-07-17 12:50:07 177浏览 收藏

做 Go 服务端渲染时,首屏经常要把用户信息、功能开关和分页条件交给一小段前端脚本。数据里一旦出现引号、换行或尖括号,很多人会先 json.Marshal,再把结果转成 template.JS 塞进页面。页面看起来立刻正常了,但这一步也绕开了 html/template 原本替你做的上下文转义。

要点速览

  • 把 Go 结构体直接放在普通 的 JavaScript 值位置,html/template 会按上下文生成可用的 JSON。
  • 不要把来自请求、数据库或第三方接口的 JSON 仅靠转为 template.JS 标记成安全内容。
  • 页面只需要一个短字段时,data-user-id 这类属性比塞一整段初始化对象更直观。
  • 回归时要带上引号、 片段和 Unicode 字符,确认页面没有长出额外标签或截断脚本。

问题不在 JSON,而在它被放进了哪个上下文

先看一个常见场景:服务端想让浏览器拿到当前用户和实验开关,于是在模板里写 const pageData = {{ .Page }};。这不是简单的字符串替换。html/template 会识别当前动作位于 JavaScript 上下文;传入的是结构体或 map 时,它会生成适合该位置的 JSON。这个能力的前提是模板由可信开发者编写,而渲染数据可以是不可信的。

真正危险的捷径是把一段外部 JSON 直接包装成 template.JS。这个类型表示“已知安全的 JavaScript 表达式”,责任随之落到调用方。官方文档明确提醒:即便 JSON 在语法上有效,只要数据不可信,就不该因为它像 JSON 而跳过模板的处理。

最小配方:让结构体进入 script 的值位置

先不要手工拼引号,也不用预先把对象编码成字符串。给视图模型一个清晰的边界,再把它直接交给模板。下面是模板片段与数据类型;项目里的渲染入口照常把 Page 作为数据的一部分传入即可。

package view

import "html/template"

type User struct {
	ID   int    `json:"id"`
	Name string `json:"name"`
}

type PageData struct {
	User  User     `json:"user"`
	Flags []string `json:"flags"`
}

type PageModel struct {
	Page PageData
}

const pageHTML = `
` func parsePage() (*template.Template, error) { return template.New("profile").Parse(pageHTML) }

关键是 {{ .Page }} 前后没有再包一层手写引号。PageData 不是普通文本,它在脚本值位置被模板按 JavaScript 规则处理。先在本地用用户名 Li "A" 和包含换行的备注走一遍页面:浏览器应该拿到同一个数据值,但 DOM 中不能多出一段意外的标签或脚本。

Go 结构体经过 html/template 进入 pageData 并被浏览器安全读取的低饱和时间线图
结构体不是先变成一段可拼接文本,而是让模板根据 script 的上下文把数据交给 pageData。

前端读取时,把初始化对象当数据而不是模板碎片

有了 pageData,前端代码就按普通对象读取。这个阶段最容易被忽略的细节是:把数据用于 textContent、属性赋值或受控的组件状态,而不是拿字符串再拼一段 HTML。这样前后端的责任也更清楚——Go 负责输出结构化初始数据,浏览器代码负责按 DOM API 渲染。

const badge = document.querySelector("#profile");

if (pageData.flags.includes("beta")) {
  badge.dataset.mode = "beta";
}

badge.textContent = `${pageData.user.name} #${pageData.user.id}`;

这一步可以顺手做一个核对:打开浏览器开发者工具,确认 pageData 是对象而不是一串双重编码的 JSON 字符串;再确认页面显示的是原始文字,不是 " 一类转义残留。若拿到的是字符串,通常是某层又做了一次 json.Marshal,而不是模板失效。

只传一个短字段时,data-* 往往更省心

并非每个页面都需要一个初始化对象。比如一个“编辑资料”按钮只要当前用户 ID,直接写到 data-user-id 更容易审查,也不必让页面多一段脚本数据。属性值仍由 html/template 处理,前端用 dataset 读取即可。



这里的边界很实际:少量标量放属性没问题;筛选器、权限集合、嵌套对象或上百条列表,不适合拆成一堆 data-*。数据开始变复杂时,回到上面的结构体配方,或者通过一个受认证的 JSON 接口按需加载。

Go 模板将 userID 写入 data-user-id 并由浏览器 dataset 读取的低饱和时间线图
一个字段走 data-*,复杂对象走 script 值位置;两条路径都不需要把不可信字符串标成 template.JS。

template.JS 不是给 JSON 开的绿色通道

template.JS 仍有合理用途,例如模板作者自己写死的一小段 JavaScript 表达式,并且能证明内容不含外部输入。它不是“JSON 字符串专用类型”,更不是给数据库字段、查询参数、CMS 内容或上游接口返回值消除转义的工具。

如果上游给的是 JSON 文本,先用 json.Unmarshal 解析成 Go 结构体或 map,再把该对象传给模板。解析失败就把它当作数据错误处理,而不是为了让页面先出来而原样塞进脚本。这个取舍看似多一步,却让类型、错误和安全边界都留在服务端。

上线前用四个输入检查渲染结果

模板安全问题往往不会在普通中文用户名上暴露。给页面准备四组固定样本,检查响应源码和浏览器显示,比只看“页面能打开”可靠得多:

  • 普通值:Lin,确认对象字段和页面文字都正常。
  • 引号值:Lin "A",确认脚本没有提前结束,JavaScript 仍能读取对象。
  • 标签样本:包含 的文本,确认浏览器没有新增可运行标签。
  • Unicode 与换行:确认中文、emoji 和多行备注不会变成双重编码字符串。

同时别忘了确认服务使用的是 html/template 而不是 text/template。两者 API 很像,但只有前者会针对 HTML、JavaScript、CSS 和 URL 上下文处理输出。升级 Go 版本时,也应把这组样本放进回归测试,尤其是项目有大量 script 模板片段时。

相关问题

能把 JSON 放进 type="application/json" 的 script 标签吗?

可以作为另一种页面数据载体,但要先验证所用 Go 版本与模板写法的上下文处理结果。若只是把 Go 对象交给页面的 JavaScript,普通 script 的值位置配合 {{ .Page }} 更直接,也更容易按官方文档理解。

data-* 能放一个完整 JSON 对象吗?

不建议作为默认方案。属性适合短字段;对象越大,转义、长度和调试成本越高。复杂数据用结构体进入 script 值位置,或通过 API 按需请求更清晰。

template.HTML 和 template.JS 的风险一样吗?

两者都是“已知安全内容”的显式标记,都会把审查责任交给调用方。只有内容确实由可信模板作者固定写死,且能证明不会混入外部数据时,才考虑使用。

为什么页面里的对象字段名不是我想要的小写?

为 Go 的结构体字段补上 json 标签,例如 json:"user"。这样模板在 JavaScript 上下文序列化时能得到稳定、便于前端读取的字段名。

把安全边界留在模板上下文里

服务端渲染页面并不排斥前端交互,关键是别把后端数据降成一段手工拼接的脚本文本。对象就按对象传给 html/template,短字段就用 data-*;只有真正可信的表达式才考虑 typed string。这样处理后,用户输入再复杂,模板上下文仍是那条最可靠的边界。

关于 JavaScript 上下文、typed string 和安全模型,可继续阅读 Go 官方的 html/template 文档包说明源码

[] []
声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>