Go 语言实现安全的分享链接:AES 加密 + SHA256 签名 + 过期防重放

问题描述

业务线有PC端、H5、小程序、App四种类型,有五种类型的测评,因为时间的关系,App和小程序要使用iframe方式嵌入的都是H5,现在要重构之前的分享报告,主要解决的问题是多端通信的兼容性和安全性。

画流程图理清思路,思路对一个程序员解决问题尤为重要,一个问题从理解,理清思路,在分层次的表达出来这个问题已经解决了一大半了,所以针对架构问题先画流程图,流程图如下:

理清思路后,下面是给AI的提示词:

bash 复制代码
我是一名Go语言后端开发工程师,现在要实现PC端 、H5 、小程序 、App 有分享测评报告的功能 ,要求是对分享的url链接进行加密  要求不能篡改 分享在加密串在答题提交后失效 生成新的加密串查看报告详情 并且兼容之前的用户校验逻辑 被分享人使用分享人的token  
需要注意的点:
1、各端 Http Header头信息 是不同的 
2、App中使用iframe方式嵌入的都是H5
3、报告信息分属于2个不同的服务项
帮我出一套程序架构的设计思路

AI巴拉巴拉给我出了一堆方案,我把符合我实际业务和需要的思路整理提取出来,进行编码和程序设计。

核心需求梳理

  1. 多端差异:PC/H5 / 小程序 / App Header 不同,App 内嵌 H5 共享一套逻辑
  2. 安全要求:URL 加密 + 防篡改,禁止篡改报告 ID、用户 ID 等核心参数
  3. 业务规则:分享签名使用一次,答题后查看报告生成新加密串
  4. 兼容要求:不破坏原有用户校验逻辑,复用分享人 Token
  5. 业务拆分:整合之前多服务痛点,需统一分享、答题、查看报告入口

设计原则

  1. 无侵入兼容:原有鉴权、业务逻辑 0 修改,仅新增分享模块
  2. 统一中间层:屏蔽多端差异,所有端共用一套分享加密 / 解密逻辑
  3. 安全闭环:加密 + 签名 + 过期 + 防重放 + 防篡改,全链路防护
  4. 服务解耦:双服务报告通过统一标识区分,不耦合业务
  5. 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/{加密字符串}.{签名}
相关推荐
MeAT ITEM2 小时前
ShardingSphere-jdbc 5.5.0 + spring boot 基础配置 - 实战篇
java·spring boot·后端
snakeshe10102 小时前
SpringBoot 应用入门与 Docker 化部署实战
后端
mldlds2 小时前
Spring Boot 集成 MyBatis 全面讲解
spring boot·后端·mybatis
阿祖zu3 小时前
OpenClaw 入门到放弃:私人 AI 的初代原型机
前端·后端·aigc
jieyucx3 小时前
Go 语言运算符与控制台输入输出详解
开发语言·后端·golang
代码N年归来仍是新手村成员3 小时前
OTel - DataDog Observability踩坑
后端·python
pupudawang3 小时前
Spring Boot 热部署
java·spring boot·后端
下地种菜小叶3 小时前
Spring Boot 2.x 升级 3.x / 4.x 怎么做?一次讲清 JDK、Jakarta、依赖兼容与上线策略
java·spring boot·后端
代码羊羊4 小时前
Rust方法速览:从self到impl
开发语言·后端·rust