问题描述
业务线有PC端、H5、小程序、App四种类型,有五种类型的测评,因为时间的关系,App和小程序要使用iframe方式嵌入的都是H5,现在要重构之前的分享报告,主要解决的问题是多端通信的兼容性和安全性。
画流程图理清思路,思路对一个程序员解决问题尤为重要,一个问题从理解,理清思路,在分层次的表达出来这个问题已经解决了一大半了,所以针对架构问题先画流程图,流程图如下:

理清思路后,下面是给AI的提示词:
bash
我是一名Go语言后端开发工程师,现在要实现PC端 、H5 、小程序 、App 有分享测评报告的功能 ,要求是对分享的url链接进行加密 要求不能篡改 分享在加密串在答题提交后失效 生成新的加密串查看报告详情 并且兼容之前的用户校验逻辑 被分享人使用分享人的token
需要注意的点:
1、各端 Http Header头信息 是不同的
2、App中使用iframe方式嵌入的都是H5
3、报告信息分属于2个不同的服务项
帮我出一套程序架构的设计思路
AI巴拉巴拉给我出了一堆方案,我把符合我实际业务和需要的思路整理提取出来,进行编码和程序设计。
核心需求梳理
- 多端差异:PC/H5 / 小程序 / App Header 不同,App 内嵌 H5 共享一套逻辑
- 安全要求:URL 加密 + 防篡改,禁止篡改报告 ID、用户 ID 等核心参数
- 业务规则:分享签名使用一次,答题后查看报告生成新加密串
- 兼容要求:不破坏原有用户校验逻辑,复用分享人 Token
- 业务拆分:整合之前多服务痛点,需统一分享、答题、查看报告入口
设计原则
- 无侵入兼容:原有鉴权、业务逻辑 0 修改,仅新增分享模块
- 统一中间层:屏蔽多端差异,所有端共用一套分享加密 / 解密逻辑
- 安全闭环:加密 + 签名 + 过期 + 防重放 + 防篡改,全链路防护
- 服务解耦:双服务报告通过统一标识区分,不耦合业务
- Go 语言适配:基于 Go 原生库实现,无 heavy 依赖,高性能
核心模块详细设计
我复述一下主要的业务逻辑:
机构端PC对学生档案的学生进行分享,生成一次性加密和签名,依赖加密串和签名用户鉴权身份,进行获取答题题目和选项,提交答题,答题后回收旧签名,(如果一直没有答题,15天自动过期)生成新签名(之前测评中心在两个服务,业务代码上进行了整合、优化,统一处理),核心在于加密串和签名串的传递,加密和解密结构体的一致性,解析完传递的结构体,分析完这些思路已经很清晰了,剩下的细节和前端的同学进行联调和测试。
加密串签名逻辑:分享人生成签名 → 被分享人打开链接 → 获取题目 → 提交答题 → 后端使旧签名失效 → 生成新签名用于查看报告
1、全局配置
配置文件中要添加 AES 加密密钥、签名密钥、分享链接默认过期时间配置,这个地方非常重要,每个服务要保持一致,才能反解出加密结构体。
bash
# 测评报告
ShareReportAuth:
AesSecretKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxx" # AES 加密密钥
SignSecretKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxx" # 签名密钥
ShareExpireHours: 3 # 分享链接默认过期时间(3小时)
2、设计规则
- URL加密: 核心参数 全部进行AES 加密 ,SHA256 签名,任何修改都会验签失败
- 携带分享人 StrId,被分享人打开直接用分享人权限和信息
- 过期时间、随机串防重放,防止被利用
- 参数还原,转发到具体的业务逻辑
3、生成加密串、签名
加密步骤:①拼接好业务需要的参数结构体 ②序列化成 JSON ③ 进行AES 加密 ⑤ Base64 转成可放在 URL 中的字符串(统一使用 StdEncoding),⑥生成防篡改签名,最终生成加密串(encrypted_string)和签名(sign),全局都是这一个名字,方便参数传递。
go
type ShareReportRequest struct {
//业务参数略...
ExpireAt int64 `json:"exp,optional"` // 过期时间戳(防永久有效)
Nonce string `json:"nc,optional"` // 随机串(防重放攻击)
}
// AES 加密 + SHA256 签名
func (l *EvaluationService) SetEncryptSign(payload *ShareReportRequest) (encryptStr string, signStr string, err error) {
expireHours := l.svcCtx.Config.ShareReportAuth.ShareExpireHours
AesSecretKey := l.svcCtx.Config.ShareReportAuth.AesSecretKey
SignSecretKey := l.svcCtx.Config.ShareReportAuth.SignSecretKey
mergedPayload := &ShareReportRequest{
//业务参数略...
ExpireAt: time.Now().Add(time.Duration(expireHours) * time.Hour).Unix(),
Nonce: utils.GenerateUniqueRandomString(16),
}
//2. 序列化成 JSON
jsonBytes, err := json.Marshal(mergedPayload)
if err != nil {
return "", "", errors.New("JSON序列化失败")
}
// 3. AES 加密
encryptBytes, err := aesEncrypt(jsonBytes, []byte(AesSecretKey))
if err != nil {
return "", "", errors.New("AES加密失败")
}
// 4. Base64 转成可放在 URL 中的字符串(统一使用 StdEncoding)
encryptStr = base64.StdEncoding.EncodeToString(encryptBytes)
//5. 生成防篡改签名
signStr = generateSign(encryptStr, SignSecretKey)
return
}
4、解密步骤
解密业务需要的结构体、需要注意的是参数保持一致,清理空字节,不然解密会失败。 解密步骤:① 对加密串和签名进行验签 ②Base64 解码(统一使用 StdEncoding)③ AES 解密 ④清理空字节 ⑤检查是否为有效JSON ⑥ 校验过期时间、校验必要字段
go
func (l *EvaluationService) GetReportSign(encryptStr, signStr string) (sign *ShareReportRequest, err error) {
AesSecretKey := l.svcCtx.Config.ShareReportAuth.AesSecretKey
SignSecretKey := l.svcCtx.Config.ShareReportAuth.SignSecretKey
var payload ShareReportRequest
// 先验签
expectedSign := utils.GenerateSign(encryptStr, SignSecretKey)
if !verifySign(encryptStr, signStr, SignSecretKey) {
return nil, errors.New("分享链接已被篡改,验签失败")
}
// Base64 解码(统一使用 StdEncoding)
var encryptBytes []byte
encryptBytes, err = base64.StdEncoding.DecodeString(encryptStr)
if err != nil {
return nil, fmt.Errorf("base64解码失败: %v", err)
}
// 4. AES 解密
jsonBytes, err := utils.AesDecrypt(encryptBytes, []byte(AesSecretKey))
if err != nil {
return nil, fmt.Errorf("解密失败: %v", err)
}
// 5. 清理空字节
jsonBytes = removeNullBytes(jsonBytes)
// 6. 检查是否为有效JSON
if len(jsonBytes) == 0 {
return nil, errors.New("解密后数据为空,请检查加密密钥是否正确")
}
if err := json.Unmarshal(jsonBytes, &payload); err != nil {
return nil, fmt.Errorf("json解析失败: %v, 原始数据: %s", err, string(jsonBytes))
}
// 7. 校验过期
if payload.ExpireAt > 0 && payload.ExpireAt < time.Now().Unix() {
return nil, errors.New("分享链接已过期")
}
// 8. 校验必要字段
if payload.StudentId == 0 || payload.CorpId == 0 {
return nil, errors.New("分享信息不完整")
}
return &payload, nil
}
5、完成
最后生成的URL如下,所有的信息和参数都在参数中传递和解码完成服务间的通信。
bash
# https://你的域名/share/{加密字符串}.{签名}