Go 的 http.ResponseWriter 不支持中途读取响应体,因此加密必须在 handler 中主动完成:先构造结构化数据,再用反射识别带 secure:"true" tag 的导出字段,对 string/[]byte 类型做 AES 加密(含 PKCS7 填充与随机 IV),最后统一序列化输出。

Go 的 http.ResponseWriter 不支持中途读取或重放响应体,所以无法在标准中间件里“拦截并加密已写入的内容”。必须让 handler 主动构造数据、加密后再写入——反射在这里只负责字段级识别和加解密调度,不参与 HTTP 流程劫持。
为什么不能在中间件里直接 AES 加密响应
常见错误是写一个中间件,在 next.ServeHTTP() 之后调用 r.Body.Read() 或尝试从 ResponseWriter 提取原始字节。这在线上环境必然失败:响应一旦写出(尤其是大响应或流式响应),底层连接可能已关闭或缓冲区已被清空。即使使用 httptest.ResponseRecorder 模拟,也仅限测试,不可用于生产。
真正可控的路径只有一条:handler 先生成结构化数据(如 Resp{Code: 0, Data: user}),再交由加密逻辑处理,最后统一写入。反射此时的作用,是动态识别哪些字段需要加密,而不是替代加密本身。
- 中间件只能包装
http.Handler,不能修改其输出内容的语义 - 加密必须发生在
json.Marshal之后、Write之前 - 若用
io.MultiWriter或自定义ResponseWriter包装器,仍需确保所有写入都经你控制——这等价于重写整个响应流程,不如让 handler 主动负责
如何用 reflect 动态识别并加密结构体敏感字段
核心是结合 struct tag(如 secure:"true")与反射遍历,只对导出字段做操作。注意:未导出字段(小写首字母)无法被 reflect.Value.Field(i) 访问,调用 .Interface() 会 panic。
立即学习“go语言免费学习笔记(深入)”;
- 必须用指针传入结构体:
Encrypt(&user),否则反射拿到的是副本,改了也白改 - 遍历时检查
field.CanInterface() && field.CanSet(),跳过不可读/不可写的字段 - 用
field.Tag.Get("secure") == "true"判断是否加密,而非硬编码字段名 - 只处理
string和[]byte类型;其他类型(如int,time.Time)先跳过,避免panic: reflect.Set: value of type []byte is not assignable to type int - 加密函数(如
AesEncrypt)应接收value string和key []byte,返回[]byte,再转成string赋值回字段
加密逻辑必须与 handler 强耦合,不能塞进中间件
典型错误是把加密逻辑写成中间件,然后期望它自动处理所有 handler 的响应。但 handler 返回的是 error 或直接 w.Write(),没有统一的数据出口。可行做法是定义标准响应结构,并在每个 handler 末尾显式调用加密:
func userHandler(w http.ResponseWriter, r *http.Request) {
user := User{ID: 123, Name: "Alice", Email: "a@example.com"}
resp := Resp{Code: 0, Data: user}
// 主动加密 Data 字段内嵌的敏感内容
if err := EncryptSensitive(&resp.Data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
encryptedJSON, _ := json.Marshal(resp)
w.Header().Set("Content-Type", "application/json")
w.Write(encryptedJSON)
}
其中 EncryptSensitive 内部用 reflect.ValueOf(v).Elem() 解包指针,再递归遍历字段。IV 必须每次随机生成(crypto/rand.Read(iv)),密文前缀拼上 base64 编码的 IV,前端才能正确解密。
性能与安全边界必须人工划定
反射不是银弹。每次调用 reflect.ValueOf 和 FieldByName 都有查表开销,高频接口(如每秒万级请求)中应缓存 reflect.Type 和字段索引。更关键的是:密钥绝不能硬编码在方法里,而要通过参数或 context 注入;空值(nil 指针、空字符串)必须提前判空,否则 reflect.Value.String() 会 panic。
最易被忽略的一点:AES-CBC 要求明文长度是块大小(16 字节)的整数倍,Go 不自动填充,必须手动实现 Pkcs7Pad;解密时也要对应去填充,否则解出来全是乱码。这个逻辑和反射无关,但和加密成败直接相关。


















