bindbox-game/internal/api/wechat/miniprogram_login.go
邹方成 2e86f8ae42 feat(wechat): 增加微信小程序用户数据解密功能
添加对微信小程序加密用户数据的解密支持,包括签名验证和解密用户信息
更新swagger文档以反映新的API字段和数据结构
2025-10-18 23:08:55 +08:00

173 lines
5.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package wechat
import (
"encoding/json"
"fmt"
"net/http"
"mini-chat/internal/code"
"mini-chat/internal/pkg/core"
"mini-chat/internal/pkg/httpclient"
"mini-chat/internal/pkg/validation"
"mini-chat/internal/pkg/wechat"
)
type miniprogramLoginRequest struct {
AppID string `json:"app_id" binding:"required"` // 小程序AppID
JSCode string `json:"js_code" binding:"required"` // 登录时获取的code
EncryptedData string `json:"encrypted_data,omitempty"` // 加密数据(可选)
IV string `json:"iv,omitempty"` // 初始向量(可选)
RawData string `json:"raw_data,omitempty"` // 原始数据(可选,用于签名验证)
Signature string `json:"signature,omitempty"` // 签名(可选,用于验证数据完整性)
}
type miniprogramLoginResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
OpenID string `json:"openid,omitempty"` // 用户唯一标识
SessionKey string `json:"session_key,omitempty"` // 会话密钥
UnionID string `json:"unionid,omitempty"` // 用户在开放平台的唯一标识符
DecryptedData *wechat.DecryptedUserInfo `json:"decrypted_data,omitempty"` // 解密后的用户信息(可选)
}
// Code2SessionResponse 微信code2Session接口响应
type Code2SessionResponse struct {
OpenID string `json:"openid"` // 用户唯一标识
SessionKey string `json:"session_key"` // 会话密钥
UnionID string `json:"unionid"` // 用户在开放平台的唯一标识符
ErrCode int `json:"errcode"` // 错误码
ErrMsg string `json:"errmsg"` // 错误信息
}
// MiniprogramLogin 小程序登录
// @Summary 小程序登录
// @Description 通过AppID和code获取用户的openid和session_key
// @Tags 微信
// @Accept json
// @Produce json
// @Param request body miniprogramLoginRequest true "请求参数"
// @Success 200 {object} miniprogramLoginResponse
// @Failure 400 {object} code.Failure
// @Failure 500 {object} code.Failure
// @Router /api/wechat/miniprogram/login [post]
func (h *handler) MiniprogramLogin() core.HandlerFunc {
return func(ctx core.Context) {
req := new(miniprogramLoginRequest)
res := new(miniprogramLoginResponse)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ParamBindError,
validation.Error(err),
))
return
}
// 根据AppID查询小程序信息获取AppSecret
miniProgram, err := h.readDB.MiniProgram.WithContext(ctx.RequestContext()).
Where(h.readDB.MiniProgram.AppID.Eq(req.AppID)).
First()
if err != nil {
h.logger.Error(fmt.Sprintf("查询小程序信息失败: %s", err.Error()))
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ServerError,
"小程序不存在或查询失败",
))
return
}
// 调用微信code2Session接口
openID, sessionKey, unionID, err := h.callCode2Session(ctx, req.AppID, miniProgram.AppSecret, req.JSCode)
if err != nil {
h.logger.Error(fmt.Sprintf("调用微信code2Session接口失败: %s", err.Error()))
ctx.AbortWithError(core.Error(
http.StatusInternalServerError,
code.ServerError,
err.Error(),
))
return
}
res.Success = true
res.Message = "登录成功"
res.OpenID = openID
res.SessionKey = sessionKey
res.UnionID = unionID
// 如果提供了加密数据,则进行解密
if req.EncryptedData != "" && req.IV != "" {
// 如果提供了签名验证数据,先验证签名
if req.RawData != "" && req.Signature != "" {
if !wechat.VerifySignature(req.RawData, req.Signature, sessionKey) {
h.logger.Warn("数据签名验证失败")
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ParamBindError,
"数据签名验证失败",
))
return
}
}
// 解密用户数据
decryptedUserInfo, err := wechat.DecryptUserInfo(sessionKey, req.EncryptedData, req.IV)
if err != nil {
h.logger.Error(fmt.Sprintf("解密用户数据失败: %s", err.Error()))
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ServerError,
"解密用户数据失败",
))
return
}
res.DecryptedData = decryptedUserInfo
}
ctx.Payload(res)
}
}
// callCode2Session 调用微信code2Session接口
func (h *handler) callCode2Session(ctx core.Context, appID, appSecret, jsCode string) (string, string, string, error) {
// 构建请求URL
url := "https://api.weixin.qq.com/sns/jscode2session"
// 发送HTTP请求
client := httpclient.GetHttpClientWithContext(ctx.RequestContext())
resp, err := client.R().
SetQueryParams(map[string]string{
"appid": appID,
"secret": appSecret,
"js_code": jsCode,
"grant_type": "authorization_code",
}).
Get(url)
if err != nil {
return "", "", "", fmt.Errorf("HTTP请求失败: %v", err)
}
if resp.StatusCode() != http.StatusOK {
return "", "", "", fmt.Errorf("HTTP请求失败状态码: %d", resp.StatusCode())
}
var result Code2SessionResponse
if err := json.Unmarshal(resp.Body(), &result); err != nil {
return "", "", "", fmt.Errorf("解析响应失败: %v", err)
}
// 检查微信API返回的错误码
if result.ErrCode != 0 {
return "", "", "", fmt.Errorf("微信API错误: errcode=%d, errmsg=%s", result.ErrCode, result.ErrMsg)
}
if result.OpenID == "" {
return "", "", "", fmt.Errorf("获取到的openid为空")
}
return result.OpenID, result.SessionKey, result.UnionID, nil
}