发布于2026-07-15 阅读(0)
扫一扫,手机访问
业务线涵盖了PC端、H5、小程序、App这四种终端,而测评类型则有五种。由于时间紧张,App和小程序侧的测评页面统一采用iframe嵌入H5的方式。现在,打算对原有的分享报告功能进行重构,核心焦点在于解决多端通信的兼容性与安全性问题。
说到这儿,先画张流程图理理思路——思路这东西,对程序员来说,比代码本身还重要。一个问题,从理解到理清脉络,再到分层次表达出来,其实已经解决了大半。对于架构设计,更是如此。流程如下:

思路清晰之后,轮到AI出场了。给它的提示词是这样的:
我是一名Go语言后端开发工程师,现在要实现PC端 、H5 、小程序 、App 有分享测评报告的功能 ,要求是对分享的url链接进行加密 要求不能篡改 分享在加密串在答题提交后失效 生成新的加密串查看报告详情 并且兼容之前的用户校验逻辑 被分享人使用分享人的token 需要注意的点: 1、各端 Http Header头信息 是不同的 2、App中使用iframe方式嵌入的都是H5 3、报告信息分属于2个不同的服务项 帮我出一套程序架构的设计思路
AI给出的方案虽然详尽,但最终还是要根据实际业务场景来裁剪。我把那些真正贴合需求的思路摘出来,开始了编码与设计。
简单复述一下核心业务逻辑:
机构端在PC上对学生档案进行分享,生成一次性的加密串和签名。被分享人凭借这个加密串和签名来完成身份鉴权,接着获取题目、提交答题。答题完成后,旧签名回收作废(如果一直没人答题,15天后自动过期),同时生成新签名用于查看报告。之前的测评中心分散在两个服务中,现在对业务代码进行了整合和优化,统一处理。整个设计的核心,就在于加密串和签名串的传递,以及加密、解密结构体的一致性。结构体解析完,思路也就彻底清晰了,剩下的就是跟前端同学联调测试。
签名逻辑的流转路径如下:
分享人生成签名 → 被分享人打开链接 → 获取题目 → 提交答题 → 后端使旧签名失效 → 生成新签名用于查看报告
配置文件中需要添加AES加密密钥、签名密钥以及分享链接的默认过期时间。这一步非常重要,每个服务的配置必须保持一致,否则无法正确反解出加密结构体。
# 测评报告 ShareReportAuth: AesSecretKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxx" # AES 加密密钥 SignSecretKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxx" # 签名密钥 ShareExpireHours: 3 # 分享链接默认过期时间(3小时)
加密的步骤很明确:先拼接好业务需要的参数结构体,再序列化成JSON,接着进行AES加密,然后用Base64(统一使用StdEncoding)转成适合放在URL中的字符串,最后生成防篡改签名。全局统一使用`encrypted_string`和`sign`这两个名字,方便参数传递。
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
}
解密时,需要解析出业务所需的结构体。这里有一个关键点:参数必须保持一致,并且要清理掉空字节,否则解密会失败。具体步骤是:首先对加密串和签名进行验签,接着Base64解码(同样使用StdEncoding),然后AES解密,清理空字节,检查是否为有效的JSON,最后校验过期时间和必要字段。
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
}
最终生成的URL格式如下,所有的信息与参数都在这个字符串中传递、解码,完成服务间的通信。
# https://你的域名/share/{加密字符串}.{签名}
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
正版软件
正版软件
正版软件
正版软件
正版软件
1
2
3
7
8