重构 wbi golang 实现

This commit is contained in:
Miuzarte
2025-04-04 21:26:25 +08:00
parent 2e203db62e
commit 724e618bab

View File

@@ -380,178 +380,185 @@ bar=514&baz=1919810&foo=114&wts=1684805578&w_rid=bb97e15f28edf445a0e4420d36f0157
### Golang ### Golang
需要 `github.com/tidwall/gjson` 作为依赖 无第三方库
```go ```go
package main package main
import ( import (
"crypto/md5" "bytes"
"encoding/hex" "crypto/md5"
"fmt" "encoding/hex"
"io" "encoding/json"
"net/http" "fmt"
"net/url" "io"
"sort" "net/http"
"strconv" "net/url"
"strings" "strconv"
"sync" "strings"
"time" "time"
"github.com/tidwall/gjson"
)
var (
mixinKeyEncTab = []int{
46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49,
33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40,
61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11,
36, 20, 34, 44, 52,
}
cache sync.Map
lastUpdateTime time.Time
) )
func main() { func main() {
urlStr := "https://api.bilibili.com/x/space/wbi/acc/info?mid=1850091" u, err := url.Parse("https://api.bilibili.com/x/space/wbi/acc/info?mid=1850091")
newUrlStr, err := signAndGenerateURL(urlStr) if err != nil {
if err != nil { panic(err)
fmt.Printf("Error: %s", err) }
return fmt.Printf("orig: %s\n", u.String())
} err = Sign(u)
req, err := http.NewRequest("GET", newUrlStr, nil) if err != nil {
if err != nil { panic(err)
fmt.Printf("Error: %s", err) }
return fmt.Printf("signed: %s\n", u.String())
}
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36") // 获取 wbi 时未修改 header
req.Header.Set("Referer", "https://www.bilibili.com/") // 但实际使用签名后的 url 时发现风控较为严重
response, err := http.DefaultClient.Do(req)
if err != nil {
fmt.Printf("Request failed: %s", err)
return
}
defer response.Body.Close()
body, err := io.ReadAll(response.Body)
if err != nil {
fmt.Printf("Failed to read response: %s", err)
return
}
fmt.Println(string(body))
} }
func signAndGenerateURL(urlStr string) (string, error) { // Sign 为链接签名
urlObj, err := url.Parse(urlStr) func Sign(u *url.URL) error {
if err != nil { return wbiKeys.Sign(u)
return "", err
}
imgKey, subKey := getWbiKeysCached()
query := urlObj.Query()
params := map[string]string{}
for k, v := range query {
params[k] = v[0]
}
newParams := encWbi(params, imgKey, subKey)
for k, v := range newParams {
query.Set(k, v)
}
urlObj.RawQuery = query.Encode()
newUrlStr := urlObj.String()
return newUrlStr, nil
} }
func encWbi(params map[string]string, imgKey, subKey string) map[string]string { // Update 无视过期时间更新
mixinKey := getMixinKey(imgKey + subKey) func Update() error {
currTime := strconv.FormatInt(time.Now().Unix(), 10) return wbiKeys.Update()
params["wts"] = currTime
// Sort keys
keys := make([]string, 0, len(params))
for k := range params {
keys = append(keys, k)
}
sort.Strings(keys)
// Remove unwanted characters
for k, v := range params {
v = sanitizeString(v)
params[k] = v
}
// Build URL parameters
query := url.Values{}
for _, k := range keys {
query.Set(k, params[k])
}
queryStr := query.Encode()
// Calculate w_rid
hash := md5.Sum([]byte(queryStr + mixinKey))
params["w_rid"] = hex.EncodeToString(hash[:])
return params
} }
func getMixinKey(orig string) string { func Get() (wk WbiKeys, err error) {
var str strings.Builder if err = wk.update(false); err != nil {
for _, v := range mixinKeyEncTab { return WbiKeys{}, err
if v < len(orig) { }
str.WriteByte(orig[v]) return wbiKeys, nil
}
}
return str.String()[:32]
} }
func sanitizeString(s string) string { var wbiKeys WbiKeys
unwantedChars := []string{"!", "'", "(", ")", "*"}
for _, char := range unwantedChars { type WbiKeys struct {
s = strings.ReplaceAll(s, char, "") Img string
} Sub string
return s Mixin string
lastUpdateTime time.Time
} }
func updateCache() { // Sign 为链接签名
if time.Since(lastUpdateTime).Minutes() < 10 { func (wk *WbiKeys) Sign(u *url.URL) (err error) {
return if err = wk.update(false); err != nil {
} return err
imgKey, subKey := getWbiKeys() }
cache.Store("imgKey", imgKey)
cache.Store("subKey", subKey) values := u.Query()
lastUpdateTime = time.Now()
values = removeUnwantedChars(values, '!', '\'', '(', ')', '*') // 必要性存疑?
values.Set("wts", strconv.FormatInt(time.Now().Unix(), 10))
// [url.Values.Encode] 内会对参数排序,
// 且遍历 map 时本身就是无序的
hash := md5.Sum([]byte(values.Encode() + wk.Mixin)) // Calculate w_rid
values.Set("w_rid", hex.EncodeToString(hash[:]))
u.RawQuery = values.Encode()
return nil
} }
func getWbiKeysCached() (string, string) { // Update 无视过期时间更新
updateCache() func (wk *WbiKeys) Update() (err error) {
imgKeyI, _ := cache.Load("imgKey") return wk.update(true)
subKeyI, _ := cache.Load("subKey")
return imgKeyI.(string), subKeyI.(string)
} }
func getWbiKeys() (string, string) { // update 按需更新
client := &http.Client{} func (wk *WbiKeys) update(purge bool) error {
req, err := http.NewRequest("GET", "https://api.bilibili.com/x/web-interface/nav", nil) if !purge && time.Since(wk.lastUpdateTime) < time.Hour {
if err != nil { return nil
fmt.Printf("Error creating request: %s", err) }
return "", ""
} // 测试下来不用修改 header 也能过
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36") resp, err := http.Get("https://api.bilibili.com/x/web-interface/nav")
req.Header.Set("Referer", "https://www.bilibili.com/") if err != nil {
resp, err := client.Do(req) return err
if err != nil { }
fmt.Printf("Error sending request: %s", err) defer resp.Body.Close()
return "", "" body, err := io.ReadAll(resp.Body)
} if err != nil {
defer resp.Body.Close() return err
body, err := io.ReadAll(resp.Body) }
if err != nil {
fmt.Printf("Error reading response: %s", err) nav := Nav{}
return "", "" err = json.Unmarshal(body, &nav)
} if err != nil {
json := string(body) return err
imgURL := gjson.Get(json, "data.wbi_img.img_url").String() }
subURL := gjson.Get(json, "data.wbi_img.sub_url").String()
imgKey := strings.Split(strings.Split(imgURL, "/")[len(strings.Split(imgURL, "/"))-1], ".")[0] if nav.Code != 0 && nav.Code != -101 { // -101 未登录时也会返回两个 key
subKey := strings.Split(strings.Split(subURL, "/")[len(strings.Split(subURL, "/"))-1], ".")[0] return fmt.Errorf("unexpected code: %d, message: %s", nav.Code, nav.Message)
return imgKey, subKey }
img := nav.Data.WbiImg.ImgUrl
sub := nav.Data.WbiImg.SubUrl
if img == "" || sub == "" {
return fmt.Errorf("empty image or sub url: %s", body)
}
// https://i0.hdslb.com/bfs/wbi/7cd084941338484aae1ad9425b84077c.png
imgParts := strings.Split(img, "/")
subParts := strings.Split(sub, "/")
// 7cd084941338484aae1ad9425b84077c.png
imgPng := imgParts[len(imgParts)-1]
subPng := subParts[len(subParts)-1]
// 7cd084941338484aae1ad9425b84077c
wbiKeys.Img = strings.TrimSuffix(imgPng, ".png")
wbiKeys.Sub = strings.TrimSuffix(subPng, ".png")
wbiKeys.mixin()
wbiKeys.lastUpdateTime = time.Now()
return nil
}
func (wk *WbiKeys) mixin() {
var mixin [32]byte
wbi := wk.Img + wk.Sub
for i := range mixin { // for i := 0; i < len(mixin); i++ {
mixin[i] = wbi[mixinKeyEncTab[i]]
}
wk.Mixin = string(mixin[:])
}
var mixinKeyEncTab = [...]int{
46, 47, 18, 2, 53, 8, 23, 32,
15, 50, 10, 31, 58, 3, 45, 35,
27, 43, 5, 49, 33, 9, 42, 19,
29, 28, 14, 39, 12, 38, 41, 13,
37, 48, 7, 16, 24, 55, 40, 61,
26, 17, 0, 1, 60, 51, 30, 4,
22, 25, 54, 21, 56, 59, 6, 63,
57, 62, 11, 36, 20, 34, 44, 52,
}
func removeUnwantedChars(v url.Values, chars ...byte) url.Values {
b := []byte(v.Encode())
for _, c := range chars {
b = bytes.ReplaceAll(b, []byte{c}, nil)
}
s, err := url.ParseQuery(string(b))
if err != nil {
panic(err)
}
return s
}
type Nav struct {
Code int `json:"code"`
Message string `json:"message"`
Ttl int `json:"ttl"`
Data struct {
WbiImg struct {
ImgUrl string `json:"img_url"`
SubUrl string `json:"sub_url"`
} `json:"wbi_img"`
// ......
} `json:"data"`
} }
``` ```