Files
fanqie-go/internal/fqapi/register_key.go
T
chenxin 84bd3238ab ✨ feat(api): 新增榜单书架历史与搜索续传并重构正文缓存链路
- 新增 /rank 免签名榜单接口(多域名容错)、/bookshelf 与 /read-history(sessionid 登录态,书架支持分组过滤)
- 搜索重写:翻页透传上游 passback/search_id/next_offset/corrected_query 并按 book_id 去重,15~20 位纯数字关键词直查详情(ID 直搜)
- 删除 /login/check 登录校验端点(login.go、路由与文档同步移除,sessionid 说明改为直接供书架/历史使用)
- 正文链路重构:singleflight 合并同章节并发回源防击穿,缓存与解析解耦统一在出口处理,decodeContentBody 按 crypt_status 分支并兜底 500003 明文
- 缓存层:SQL 持久层 TTL 与业务 TTL 解耦(不可变内容过期后由 SQL 回填),动态短缓存只走 Redis,withCache 合并并发 miss
- 密钥池:KeyTs 随正文请求上传降低密钥不匹配重试,密钥超 24 小时异步刷新并防刷新风暴,last_used_at 批量落库
- 签名补 X-Helios(与 X-Ladon 同值,对齐书源实现);设备注册版本号统一走 ClientVersionCode/Name
- 分类 landing 透传新版标签 selected_items;multi-detail 批量详情供书架/历史复用
- README 与 openapi.yaml 补充新接口文档
2026-10-12 14:49:14 +08:00

257 lines
6.1 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 fqapi
import (
"bytes"
"compress/gzip"
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fanqie/internal/common"
"fmt"
"math/rand"
"regexp"
)
// KeyInfo 密钥信息
type KeyInfo struct {
Key string `json:"key"`
Ver int `json:"ver"`
Ts int64 `json:"ts"` // 密钥注册时间戳(key_register_ts),随正文请求上传可降低密钥不匹配重试
}
var (
keyInfo *KeyInfo
keyLogger = common.NewLogger("register_key", "INFO", "")
)
// rHex 将数字转换为反转的16进制字符串
func rHex(num int64) string {
hexStr := fmt.Sprintf("%032x", num)
result := ""
for i := len(hexStr); i > 0; i -= 2 {
result += hexStr[i-2 : i]
}
return result
}
// createSymmetricCrypto 创建对称加密器
func createSymmetricCrypto(algorithm string, key, iv []byte) (func([]byte) ([]byte, error), func([]byte) ([]byte, error), error) {
if algorithm != "AES/CBC/PKCS5Padding" {
return nil, nil, fmt.Errorf("不支持的加密算法")
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, nil, err
}
encrypt := func(data []byte) ([]byte, error) {
// PKCS7填充
padding := aes.BlockSize - len(data)%aes.BlockSize
padText := make([]byte, padding)
for i := range padText {
padText[i] = byte(padding)
}
paddedData := append(data, padText...)
ciphertext := make([]byte, len(paddedData))
mode := cipher.NewCBCEncrypter(block, iv)
mode.CryptBlocks(ciphertext, paddedData)
return ciphertext, nil
}
decrypt := func(data []byte) ([]byte, error) {
mode := cipher.NewCBCDecrypter(block, iv)
decrypted := make([]byte, len(data))
mode.CryptBlocks(decrypted, data)
// 移除PKCS7填充(完整校验:密钥错误时尾部为随机字节,
// 弱校验会让乱码静默通过并返回 200)
padding := int(decrypted[len(decrypted)-1])
if padding < 1 || padding > len(decrypted) || padding > aes.BlockSize {
return nil, fmt.Errorf("无效的填充")
}
for _, b := range decrypted[len(decrypted)-padding:] {
if int(b) != padding {
return nil, fmt.Errorf("无效的填充")
}
}
return decrypted[:len(decrypted)-padding], nil
}
return encrypt, decrypt, nil
}
// En AES加密
func En(data, key, iv []byte) ([]byte, error) {
encrypt, _, err := createSymmetricCrypto("AES/CBC/PKCS5Padding", key, iv)
if err != nil {
return nil, err
}
return encrypt(data)
}
// De AES解密
func De(data, key, iv []byte) ([]byte, error) {
_, decrypt, err := createSymmetricCrypto("AES/CBC/PKCS5Padding", key, iv)
if err != nil {
return nil, err
}
return decrypt(data)
}
// randomStringKey 生成随机字符串(密钥用)
func randomStringKey(length int) string {
const charset = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
b := make([]byte, length)
for i := range b {
b[i] = charset[rand.Intn(len(charset))]
}
return string(b)
}
// GetKey 获取密钥
func GetKey(header string) (*KeyInfo, error) {
if header == "" {
keyLogger.Error("请先注册设备")
return nil, fmt.Errorf("请先注册设备")
}
key, _ := base64.StdEncoding.DecodeString("rCXGfd2POMGzeiNIgo4iLg==")
iv := []byte(randomStringKey(16))
// 提取device_id
re := regexp.MustCompile(`device_id=(\d+)`)
matches := re.FindStringSubmatch(header)
if len(matches) < 2 {
return nil, fmt.Errorf("无法从header中提取device_id")
}
var deviceIDInt int64
fmt.Sscanf(matches[1], "%d", &deviceIDInt)
// 转换device_id
hexStr := rHex(deviceIDInt)
data, _ := hex.DecodeString(hexStr)
keyLogger.Debug(fmt.Sprintf("rKey: %v", iv))
// 加密
encrypted, err := En(data, key, iv)
if err != nil {
return nil, err
}
// 组合iv和加密数据
newArr := append(iv, encrypted...)
content := base64.StdEncoding.EncodeToString(newArr)
keyLogger.Info("正在获取密钥...")
// 构建参数(form 编码:机型含空格等字符时 URL 与签名一致才不会注册失败)
params := []string{
"aid=1967",
"app_name=novelapp",
"channel=0",
"device_platform=android",
"os_version=0",
"version_code=" + ClientVersionCode,
header,
}
sortedParams := buildSortedQuery(params...)
// 准备请求体
requestBody := map[string]string{
"content": content,
"keyver": "1",
}
bodyJSON, _ := json.Marshal(requestBody)
// gzip压缩
var gzipBuf bytes.Buffer
gzipWriter := gzip.NewWriter(&gzipBuf)
gzipWriter.Write(bodyJSON)
gzipWriter.Close()
reqURL := "https://reading.snssdk.com/reading/crypt/registerkey?" + sortedParams
resp, err := GorgonRequest(reqURL, map[string]interface{}{
"method": "POST",
"body": gzipBuf.Bytes(),
"headers": map[string]string{
"User-Agent": "com.dragon.read",
"Content-Encoding": "gzip",
},
})
if err != nil {
keyLogger.Error("获取密钥失败: " + err.Error())
return nil, err
}
if resp.Status == 200 {
var respData map[string]interface{}
json.Unmarshal(resp.Content, &respData)
// 上游判定该设备无需解密(正文明文下发),返回空密钥信息由调用方分支处理
if code, ok := respData["code"].(float64); ok && int(code) == 500003 {
keyLogger.Info("上游标记无需解密(code=500003)")
return &KeyInfo{}, nil
}
dataObj, ok := respData["data"].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("响应格式错误")
}
keyStr, ok := dataObj["key"].(string)
if !ok {
return nil, fmt.Errorf("密钥字段不存在")
}
// 解码返回的密钥
res, _ := base64.StdEncoding.DecodeString(keyStr)
if len(res) < 32 {
return nil, fmt.Errorf("密钥长度不足")
}
data := res[16:]
ivResp := res[0:16]
// 解密密钥
decryptedKey, err := De(data, key, ivResp)
if err != nil {
return nil, err
}
finalKey := base64.StdEncoding.EncodeToString(decryptedKey)
keyVer, _ := dataObj["keyver"].(float64)
keyTs, _ := dataObj["key_register_ts"].(float64)
if keyTs == 0 {
keyTs, _ = dataObj["register_time"].(float64)
}
keyInfo = &KeyInfo{
Key: finalKey,
Ver: int(keyVer),
Ts: int64(keyTs),
}
keyLogger.Info("密钥获取成功!")
return keyInfo, nil
}
return nil, fmt.Errorf("获取密钥失败")
}
// GetKeyInfo 获取当前密钥信息
func GetKeyInfo() *KeyInfo {
return keyInfo
}