330 lines
11 KiB
Go
330 lines
11 KiB
Go
// go/main.go
|
||
package main
|
||
|
||
import (
|
||
"bytes"
|
||
"crypto/aes"
|
||
"crypto/cipher"
|
||
"crypto/rand"
|
||
"crypto/rsa"
|
||
"crypto/sha256"
|
||
"crypto/x509"
|
||
"encoding/base64"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"log"
|
||
"net/http"
|
||
"net/url"
|
||
"strconv"
|
||
"strings"
|
||
)
|
||
|
||
// 与 Java 保持一致的 RSA 公钥/私钥(Base64)
|
||
var publicKeyStr = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqmdgZjjpySFAd+3Go33tshTOhRcSl6Sl4x8bR5vrEzsvFqQQW+VXLco0E1jy9dIR4NguRIGWOowi/4EU5PEM3ZVrXQCxCnyyqIuuDtY9QNTh5DTn60aDOLEL2X7+mvICgFg+VAKPik+8fBSUfzcGiLqFlx+VhUAkq9hCyd/wtYInuAxPSoCr8F2cmI4/V6sAhVUkHUZhJvWlyDLUpYKOGgYM4rXjCXXKrPO0FNf1iY70AWACSJmXUwBVuIRYWfTRVOvzEPWkp/tuqir/XcvMfKKVU5/eCr8abNVIG99HTF1iKvQPdQUldAyk5z9YPV5IwAbrjlEACmJ5JvuT3bypewIDAQAB"
|
||
var privateKeyStr = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCqZ2BmOOnJIUB37cajfe2yFM6FFxKXpKXjHxtHm+sTOy8WpBBb5VctyjQTWPL10hHg2C5EgZY6jCL/gRTk8QzdlWtdALEKfLKoi64O1j1A1OHkNOfrRoM4sQvZfv6a8gKAWD5UAo+KT7x8FJR/NwaIuoWXH5WFQCSr2ELJ3/C1gie4DE9KgKvwXZyYjj9XqwCFVSQdRmEm9aXIMtSlgo4aBgziteMJdcqs87QU1/WJjvQBYAJImZdTAFW4hFhZ9NFU6/MQ9aSn+26qKv9dy8x8opVTn94Kvxps1Ugb30dMXWIq9A91BSV0DKTnP1g9XkjABuuOUQAKYnkm+5PdvKl7AgMBAAECggEAY0xmYmsb4PadiMVokXEaiEGTrv6o+PEbMeS4ktwK+mPsprboSYS1bpt8CSI2QoUtoeaX35fcITX0VwuzT04gfydJLyLuB/xuZ8Utoru5agQjtkYWN4YZhXm2PAHDACuyxXOmrnHnj2OzpGKhvhgkmJyIqG3hRYsBU5psIRN8Q2gnCarhiB948YDu6EfvFJPv0ET9T+mzQWLmMVz+lorepfcXV1Wwvr0awRPfyS2s7te9AW4GYEzKN9ijZx05XnYOSgh/hD82iqh+poPXiqamhZGcQBQ8CveVbyNatTpbZYca8gXByOIipSEqg3UVQ2rnd/hYAh4VKSagyKXtFt7REQKBgQDgjGtbit3JK8jJuQTWBqgNHWphc1/4tAidjWeEF1NBi0NZuSZiVLoh9J794lO67psAZheZV70gXD9pHF7tiSESTIsHZiOtU9rrEVACc6EHJIeBrmB4oNWvzzix5S0S6RZxLYm5oiEoNjXSfqcFqCHGiT3lnxUhSp3JywlcIx0QZQKBgQDCRYEMt+u0Hp8JbhxhHx5Z87dZRJe0AGRQXnz3blxq+MTIVB6oj6NKBxPN6BmNLGI/xnxO8XTog5JCv9SZRZpY6eZmdCt2sVK2k2i1kxpd3sLjlbFTNnyC9RJFeZMjIlvW03KmDu0VBDf4CW/zSP3aej+vgxvOZesYcgI9isoEXwKBgEmrJ+mflIXUfIpZzhFdm7K5zNXt4TWZ8x2lb6mxcVoWk2ETUll+TJapR6Qppai1cVrfI6zmUSEVwqP8b9RkYdo8DHy/8MKDuVXXlzVGtDTAskhEalgJBDIqvQH4GyKSIA+/jei+HTyxFFVbwfYkI/ibvBfiai9C6KN0njyBNJ7VAoGAVgDZCZ1efmXT+CPD8ocJM78+GwnPswM9ZYr+/bbguQaabyk2TV8RZdNORCiNLz9H233uSDCClfCxTlWIM7ZphxU9R3wERc5olKUbhM6zrHzSgFgjoXgMlRkTVqhkp/gs+iSvq64N7PDqKidbZTOaFh9qlDORmsTp1++Y6E/J8TcCgYEA0cszTR9OuvrQDpiX5PW+HB66GIxuEFBCla0HphtV16i/tzXnIaZ6q2hf1e9qejO3lOIzi3e1PVHOuMIemzl17batonERhBIjDYEtGraFyaHSgkp+zRdjPGj8A0dq7iwdv0M4ravQcF9dVvfEucVhN3XSJXSqdJRSoZzOvRZH4VY="
|
||
|
||
// parsePublicKey 解析 Java Base64 公钥为 rsa.PublicKey
|
||
// 返回 rsa.PublicKey 或错误
|
||
func parsePublicKey(b64 string) (*rsa.PublicKey, error) {
|
||
der, err := base64.StdEncoding.DecodeString(b64)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
pub, err := x509ParsePublicKey(der)
|
||
return pub, err
|
||
}
|
||
|
||
// parsePrivateKey 解析 Java Base64 私钥为 rsa.PrivateKey
|
||
// 返回 rsa.PrivateKey 或错误
|
||
func parsePrivateKey(b64 string) (*rsa.PrivateKey, error) {
|
||
der, err := base64.StdEncoding.DecodeString(b64)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
priv, err := x509ParsePrivateKey(der)
|
||
return priv, err
|
||
}
|
||
|
||
// x509ParsePublicKey X509 公钥解析(PKCS#8)
|
||
func x509ParsePublicKey(der []byte) (*rsa.PublicKey, error) {
|
||
key, err := x509.ParsePKIXPublicKey(der)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
pub, ok := key.(*rsa.PublicKey)
|
||
if !ok {
|
||
return nil, errors.New("公钥类型错误")
|
||
}
|
||
return pub, nil
|
||
}
|
||
|
||
// x509ParsePrivateKey PKCS#8 私钥解析
|
||
func x509ParsePrivateKey(der []byte) (*rsa.PrivateKey, error) {
|
||
key, err := x509.ParsePKCS8PrivateKey(der)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
priv, ok := key.(*rsa.PrivateKey)
|
||
if !ok {
|
||
return nil, errors.New("私钥类型错误")
|
||
}
|
||
return priv, nil
|
||
}
|
||
|
||
// EncryptHybrid 混合加密:随机 AES-256-GCM + RSA-OAEP(SHA-256) 加密 AES 密钥
|
||
// 输出 Base64(iv || rsa(aesKey) || gcmCiphertext) 与 Java 完全一致
|
||
func EncryptHybrid(plaintext []byte) (string, error) {
|
||
pub, err := parsePublicKey(publicKeyStr)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
aesKey := make([]byte, 32)
|
||
if _, err = rand.Read(aesKey); err != nil {
|
||
return "", err
|
||
}
|
||
block, err := aes.NewCipher(aesKey)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
gcm, err := cipher.NewGCM(block)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
iv := make([]byte, 12)
|
||
if _, err = rand.Read(iv); err != nil {
|
||
return "", err
|
||
}
|
||
ciphertext := gcm.Seal(nil, iv, plaintext, nil)
|
||
label := []byte{}
|
||
encKey, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, pub, aesKey, label)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
buf := bytes.Join([][]byte{iv, encKey, ciphertext}, nil)
|
||
return base64.StdEncoding.EncodeToString(buf), nil
|
||
}
|
||
|
||
// DecryptHybrid 混合解密:拆分 iv、RSA 加密的 AES 密钥、GCM 密文
|
||
// 使用 RSA-OAEP(SHA-256) 解密 AES 密钥,再用 AES-256-GCM 解密数据
|
||
func DecryptHybrid(token string) ([]byte, error) {
|
||
log.Println("=== 开始解密 DecryptHybrid ===")
|
||
log.Println("原始 token:", token)
|
||
|
||
// 兼容 Java 的 " "→"+"
|
||
token = strings.ReplaceAll(token, " ", "+")
|
||
log.Println("空格替换后的 token:", token)
|
||
|
||
data, err := base64.StdEncoding.DecodeString(token)
|
||
if err != nil {
|
||
log.Println("Base64 解码失败:", err)
|
||
return nil, err
|
||
}
|
||
log.Println("Base64 解码成功,长度:", len(data))
|
||
|
||
if len(data) < 12+256 {
|
||
log.Println("密文长度非法:", len(data))
|
||
return nil, errors.New("密文长度非法")
|
||
}
|
||
|
||
iv := data[:12]
|
||
encKey := data[12 : 12+256]
|
||
ct := data[12+256:]
|
||
|
||
log.Println("IV 长度:", len(iv))
|
||
log.Println("RSA 加密的 AES Key 长度:", len(encKey))
|
||
log.Println("GCM 密文长度:", len(ct))
|
||
|
||
priv, err := parsePrivateKey(privateKeyStr)
|
||
if err != nil {
|
||
log.Println("解析私钥失败:", err)
|
||
return nil, err
|
||
}
|
||
|
||
// RSA PKCS1Padding 解密
|
||
aesKey, err := rsa.DecryptPKCS1v15(rand.Reader, priv, encKey)
|
||
if err != nil {
|
||
log.Println("RSA PKCS1 解密失败:", err)
|
||
return nil, err
|
||
}
|
||
log.Println("RSA 解密 AES Key 成功,长度:", len(aesKey))
|
||
|
||
block, err := aes.NewCipher(aesKey)
|
||
if err != nil {
|
||
log.Println("AES Cipher 创建失败:", err)
|
||
return nil, err
|
||
}
|
||
|
||
gcm, err := cipher.NewGCM(block)
|
||
if err != nil {
|
||
log.Println("GCM 创建失败:", err)
|
||
return nil, err
|
||
}
|
||
|
||
pt, err := gcm.Open(nil, iv, ct, nil)
|
||
if err != nil {
|
||
log.Println("GCM 解密失败:", err)
|
||
return nil, err
|
||
}
|
||
|
||
log.Println("解密成功,明文长度:", len(pt))
|
||
log.Println("=== 解密结束 ===")
|
||
return pt, nil
|
||
}
|
||
|
||
// PricingLinkHandler 处理 GET /pricingLink
|
||
// 构造与 Java 相同结构的载荷:number(=numbers*100)、total、qureyApiUrl、taskMapList,并进行混合加密
|
||
func PricingLinkHandler(w http.ResponseWriter, r *http.Request) {
|
||
if r.Method != http.MethodGet {
|
||
http.Error(w, "仅支持 GET", http.StatusMethodNotAllowed)
|
||
return
|
||
}
|
||
q := r.URL.Query()
|
||
numbers := atoi(q.Get("numbers"))
|
||
total := atoi(q.Get("total"))
|
||
|
||
res := map[string]any{
|
||
"number": strconv.Itoa(numbers * 100),
|
||
"total": strconv.Itoa(total),
|
||
"qureyApiUrl": buildQueryApiUrl(q),
|
||
"taskMapList": buildTaskMapList(q),
|
||
}
|
||
|
||
body, err := json.Marshal(res)
|
||
if err != nil {
|
||
http.Error(w, "序列化失败", http.StatusInternalServerError)
|
||
return
|
||
}
|
||
token, err := EncryptHybrid(body)
|
||
if err != nil {
|
||
http.Error(w, "加密失败", http.StatusInternalServerError)
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusOK, token)
|
||
}
|
||
|
||
// PricingLinkDecHandler 处理 GET /pricingLinkDec
|
||
// 解密并返回原始 Map,兼容 Java 的 Base64 空格替换逻辑
|
||
func PricingLinkDecHandler(w http.ResponseWriter, r *http.Request) {
|
||
log.Println("进入 /pricingLinkDec")
|
||
|
||
if r.Method != http.MethodGet {
|
||
log.Println("方法非法:", r.Method)
|
||
http.Error(w, "仅支持 GET", http.StatusMethodNotAllowed)
|
||
return
|
||
}
|
||
|
||
link := r.URL.Query().Get("link")
|
||
log.Println("收到 link 参数:", link)
|
||
|
||
if strings.TrimSpace(link) == "" {
|
||
log.Println("缺少 link 参数")
|
||
http.Error(w, "缺少参数 link", http.StatusBadRequest)
|
||
return
|
||
}
|
||
|
||
pt, err := DecryptHybrid(link)
|
||
if err != nil {
|
||
log.Println("解密失败:", err)
|
||
http.Error(w, "解密失败", http.StatusBadRequest)
|
||
return
|
||
}
|
||
|
||
var payload map[string]any
|
||
if err := json.Unmarshal(pt, &payload); err != nil {
|
||
log.Println("JSON 解析失败:", err)
|
||
http.Error(w, "载荷解析失败", http.StatusBadRequest)
|
||
return
|
||
}
|
||
|
||
log.Println("解密成功,返回 payload:", payload)
|
||
writeJSON(w, http.StatusOK, payload)
|
||
}
|
||
|
||
// buildQueryApiUrl 生成 qureyApiUrl,与 Java 的 getUrl 逻辑一致(按字段编码并拼接)
|
||
// 基础路径固定为 https://api.buzhiyushu.cn/zhishu/baseInfo/pricing/list
|
||
func buildQueryApiUrl(q url.Values) string {
|
||
base := "https://api.buzhiyushu.cn/zhishu/baseInfo/pricing/list?"
|
||
add := func(k string) string {
|
||
v := strings.TrimSpace(q.Get(k))
|
||
if v == "" {
|
||
return ""
|
||
}
|
||
return fmt.Sprintf("%s=%s&", k, url.QueryEscape(v))
|
||
}
|
||
var b strings.Builder
|
||
b.WriteString(base)
|
||
// 字段与 Java 对齐
|
||
b.WriteString(add("bookName"))
|
||
b.WriteString(add("bookPic"))
|
||
b.WriteString(add("isbn"))
|
||
b.WriteString(add("author"))
|
||
b.WriteString(add("publisher"))
|
||
if v := q.Get("vio_book"); v != "" {
|
||
b.WriteString(fmt.Sprintf("vio_book=%s&", url.QueryEscape(v)))
|
||
}
|
||
if v := q.Get("book_set"); v != "" {
|
||
b.WriteString(fmt.Sprintf("book_set=%s&", url.QueryEscape(v)))
|
||
}
|
||
if v := q.Get("onenum_mbooks"); v != "" {
|
||
b.WriteString(fmt.Sprintf("onenum_mbooks=%s&", url.QueryEscape(v)))
|
||
}
|
||
if v := q.Get("ill_publisher"); v != "" {
|
||
b.WriteString(fmt.Sprintf("ill_publisher=%s&", url.QueryEscape(v)))
|
||
}
|
||
b.WriteString(add("saleSelect"))
|
||
b.WriteString(add("category"))
|
||
if v := q.Get("ill_author"); v != "" {
|
||
b.WriteString(fmt.Sprintf("ill_author=%s&", url.QueryEscape(v)))
|
||
}
|
||
b.WriteString(add("publiction_times"))
|
||
b.WriteString(add("buy_counts"))
|
||
// sell_counts 最后一个不加 &
|
||
if v := strings.TrimSpace(q.Get("sell_counts")); v != "" {
|
||
b.WriteString(fmt.Sprintf("sell_counts=%s", url.QueryEscape(v)))
|
||
}
|
||
// 去除可能的尾部 &
|
||
u := b.String()
|
||
if strings.HasSuffix(u, "&") {
|
||
u = strings.TrimSuffix(u, "&")
|
||
}
|
||
return u
|
||
}
|
||
|
||
// buildTaskMapList 构造 taskMapList(无数据库,仅透传 shopIds 形成占位)
|
||
// 兼容 Java 的数组元素形态:{ "taskId": "", "shopId": "<id>", "shopType": "<type>" }
|
||
func buildTaskMapList(q url.Values) []map[string]string {
|
||
ids := strings.TrimSpace(q.Get("shopIds"))
|
||
types := q["shopType"] // 可选:与 shopIds 对应
|
||
if ids == "" {
|
||
return []map[string]string{}
|
||
}
|
||
idList := strings.Split(ids, ",")
|
||
out := make([]map[string]string, 0, len(idList))
|
||
for i, id := range idList {
|
||
m := map[string]string{
|
||
"taskId": "", // 无任务系统,留空
|
||
"shopId": strings.TrimSpace(id),
|
||
"shopType": "",
|
||
}
|
||
if i < len(types) {
|
||
m["shopType"] = strings.TrimSpace(types[i])
|
||
}
|
||
out = append(out, m)
|
||
}
|
||
return out
|
||
}
|
||
|
||
// writeJSON 写出 JSON 响应
|
||
func writeJSON(w http.ResponseWriter, status int, v any) {
|
||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||
w.WriteHeader(status)
|
||
_ = json.NewEncoder(w).Encode(v)
|
||
}
|
||
|
||
// atoi 安全转换
|
||
func atoi(s string) int {
|
||
v, _ := strconv.Atoi(strings.TrimSpace(s))
|
||
return v
|
||
}
|