Skip to content

Commit e3121ec

Browse files
authored
Merge pull request #1223 from Miuzarte/wbiGoRefactor
重构 wbi golang 实现
2 parents 1b41f63 + 724e618 commit e3121ec

File tree

1 file changed

+157
-150
lines changed

1 file changed

+157
-150
lines changed

docs/misc/sign/wbi.md

Lines changed: 157 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -380,178 +380,185 @@ bar=514&baz=1919810&foo=114&wts=1684805578&w_rid=bb97e15f28edf445a0e4420d36f0157
380380

381381
### Golang
382382

383-
需要 `github.com/tidwall/gjson` 作为依赖
383+
无第三方库
384384

385385
```go
386386
package main
387387

388388
import (
389-
"crypto/md5"
390-
"encoding/hex"
391-
"fmt"
392-
"io"
393-
"net/http"
394-
"net/url"
395-
"sort"
396-
"strconv"
397-
"strings"
398-
"sync"
399-
"time"
400-
401-
"github.com/tidwall/gjson"
402-
)
403-
404-
var (
405-
mixinKeyEncTab = []int{
406-
46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49,
407-
33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40,
408-
61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11,
409-
36, 20, 34, 44, 52,
410-
}
411-
cache sync.Map
412-
lastUpdateTime time.Time
389+
"bytes"
390+
"crypto/md5"
391+
"encoding/hex"
392+
"encoding/json"
393+
"fmt"
394+
"io"
395+
"net/http"
396+
"net/url"
397+
"strconv"
398+
"strings"
399+
"time"
413400
)
414401

415402
func main() {
416-
urlStr := "https://api.bilibili.com/x/space/wbi/acc/info?mid=1850091"
417-
newUrlStr, err := signAndGenerateURL(urlStr)
418-
if err != nil {
419-
fmt.Printf("Error: %s", err)
420-
return
421-
}
422-
req, err := http.NewRequest("GET", newUrlStr, nil)
423-
if err != nil {
424-
fmt.Printf("Error: %s", err)
425-
return
426-
}
427-
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")
428-
req.Header.Set("Referer", "https://www.bilibili.com/")
429-
response, err := http.DefaultClient.Do(req)
430-
if err != nil {
431-
fmt.Printf("Request failed: %s", err)
432-
return
433-
}
434-
defer response.Body.Close()
435-
body, err := io.ReadAll(response.Body)
436-
if err != nil {
437-
fmt.Printf("Failed to read response: %s", err)
438-
return
439-
}
440-
fmt.Println(string(body))
403+
u, err := url.Parse("https://api.bilibili.com/x/space/wbi/acc/info?mid=1850091")
404+
if err != nil {
405+
panic(err)
406+
}
407+
fmt.Printf("orig: %s\n", u.String())
408+
err = Sign(u)
409+
if err != nil {
410+
panic(err)
411+
}
412+
fmt.Printf("signed: %s\n", u.String())
413+
414+
// 获取 wbi 时未修改 header
415+
// 但实际使用签名后的 url 时发现风控较为严重
416+
}
417+
418+
// Sign 为链接签名
419+
func Sign(u *url.URL) error {
420+
return wbiKeys.Sign(u)
421+
}
422+
423+
// Update 无视过期时间更新
424+
func Update() error {
425+
return wbiKeys.Update()
441426
}
442427

443-
func signAndGenerateURL(urlStr string) (string, error) {
444-
urlObj, err := url.Parse(urlStr)
445-
if err != nil {
446-
return "", err
447-
}
448-
imgKey, subKey := getWbiKeysCached()
449-
query := urlObj.Query()
450-
params := map[string]string{}
451-
for k, v := range query {
452-
params[k] = v[0]
453-
}
454-
newParams := encWbi(params, imgKey, subKey)
455-
for k, v := range newParams {
456-
query.Set(k, v)
457-
}
458-
urlObj.RawQuery = query.Encode()
459-
newUrlStr := urlObj.String()
460-
return newUrlStr, nil
428+
func Get() (wk WbiKeys, err error) {
429+
if err = wk.update(false); err != nil {
430+
return WbiKeys{}, err
431+
}
432+
return wbiKeys, nil
433+
}
434+
435+
var wbiKeys WbiKeys
436+
437+
type WbiKeys struct {
438+
Img string
439+
Sub string
440+
Mixin string
441+
lastUpdateTime time.Time
442+
}
443+
444+
// Sign 为链接签名
445+
func (wk *WbiKeys) Sign(u *url.URL) (err error) {
446+
if err = wk.update(false); err != nil {
447+
return err
448+
}
449+
450+
values := u.Query()
451+
452+
values = removeUnwantedChars(values, '!', '\'', '(', ')', '*') // 必要性存疑?
453+
454+
values.Set("wts", strconv.FormatInt(time.Now().Unix(), 10))
455+
456+
// [url.Values.Encode] 内会对参数排序,
457+
// 且遍历 map 时本身就是无序的
458+
hash := md5.Sum([]byte(values.Encode() + wk.Mixin)) // Calculate w_rid
459+
values.Set("w_rid", hex.EncodeToString(hash[:]))
460+
u.RawQuery = values.Encode()
461+
return nil
461462
}
462463

463-
func encWbi(params map[string]string, imgKey, subKey string) map[string]string {
464-
mixinKey := getMixinKey(imgKey + subKey)
465-
currTime := strconv.FormatInt(time.Now().Unix(), 10)
466-
params["wts"] = currTime
467-
468-
// Sort keys
469-
keys := make([]string, 0, len(params))
470-
for k := range params {
471-
keys = append(keys, k)
472-
}
473-
sort.Strings(keys)
474-
475-
// Remove unwanted characters
476-
for k, v := range params {
477-
v = sanitizeString(v)
478-
params[k] = v
479-
}
480-
481-
// Build URL parameters
482-
query := url.Values{}
483-
for _, k := range keys {
484-
query.Set(k, params[k])
485-
}
486-
queryStr := query.Encode()
487-
488-
// Calculate w_rid
489-
hash := md5.Sum([]byte(queryStr + mixinKey))
490-
params["w_rid"] = hex.EncodeToString(hash[:])
491-
return params
464+
// Update 无视过期时间更新
465+
func (wk *WbiKeys) Update() (err error) {
466+
return wk.update(true)
492467
}
493468

494-
func getMixinKey(orig string) string {
495-
var str strings.Builder
496-
for _, v := range mixinKeyEncTab {
497-
if v < len(orig) {
498-
str.WriteByte(orig[v])
499-
}
500-
}
501-
return str.String()[:32]
469+
// update 按需更新
470+
func (wk *WbiKeys) update(purge bool) error {
471+
if !purge && time.Since(wk.lastUpdateTime) < time.Hour {
472+
return nil
473+
}
474+
475+
// 测试下来不用修改 header 也能过
476+
resp, err := http.Get("https://api.bilibili.com/x/web-interface/nav")
477+
if err != nil {
478+
return err
479+
}
480+
defer resp.Body.Close()
481+
body, err := io.ReadAll(resp.Body)
482+
if err != nil {
483+
return err
484+
}
485+
486+
nav := Nav{}
487+
err = json.Unmarshal(body, &nav)
488+
if err != nil {
489+
return err
490+
}
491+
492+
if nav.Code != 0 && nav.Code != -101 { // -101 未登录时也会返回两个 key
493+
return fmt.Errorf("unexpected code: %d, message: %s", nav.Code, nav.Message)
494+
}
495+
img := nav.Data.WbiImg.ImgUrl
496+
sub := nav.Data.WbiImg.SubUrl
497+
if img == "" || sub == "" {
498+
return fmt.Errorf("empty image or sub url: %s", body)
499+
}
500+
501+
// https://i0.hdslb.com/bfs/wbi/7cd084941338484aae1ad9425b84077c.png
502+
imgParts := strings.Split(img, "/")
503+
subParts := strings.Split(sub, "/")
504+
505+
// 7cd084941338484aae1ad9425b84077c.png
506+
imgPng := imgParts[len(imgParts)-1]
507+
subPng := subParts[len(subParts)-1]
508+
509+
// 7cd084941338484aae1ad9425b84077c
510+
wbiKeys.Img = strings.TrimSuffix(imgPng, ".png")
511+
wbiKeys.Sub = strings.TrimSuffix(subPng, ".png")
512+
513+
wbiKeys.mixin()
514+
wbiKeys.lastUpdateTime = time.Now()
515+
return nil
502516
}
503517

504-
func sanitizeString(s string) string {
505-
unwantedChars := []string{"!", "'", "(", ")", "*"}
506-
for _, char := range unwantedChars {
507-
s = strings.ReplaceAll(s, char, "")
508-
}
509-
return s
518+
func (wk *WbiKeys) mixin() {
519+
var mixin [32]byte
520+
wbi := wk.Img + wk.Sub
521+
for i := range mixin { // for i := 0; i < len(mixin); i++ {
522+
mixin[i] = wbi[mixinKeyEncTab[i]]
523+
}
524+
wk.Mixin = string(mixin[:])
510525
}
511526

512-
func updateCache() {
513-
if time.Since(lastUpdateTime).Minutes() < 10 {
514-
return
515-
}
516-
imgKey, subKey := getWbiKeys()
517-
cache.Store("imgKey", imgKey)
518-
cache.Store("subKey", subKey)
519-
lastUpdateTime = time.Now()
527+
var mixinKeyEncTab = [...]int{
528+
46, 47, 18, 2, 53, 8, 23, 32,
529+
15, 50, 10, 31, 58, 3, 45, 35,
530+
27, 43, 5, 49, 33, 9, 42, 19,
531+
29, 28, 14, 39, 12, 38, 41, 13,
532+
37, 48, 7, 16, 24, 55, 40, 61,
533+
26, 17, 0, 1, 60, 51, 30, 4,
534+
22, 25, 54, 21, 56, 59, 6, 63,
535+
57, 62, 11, 36, 20, 34, 44, 52,
520536
}
521537

522-
func getWbiKeysCached() (string, string) {
523-
updateCache()
524-
imgKeyI, _ := cache.Load("imgKey")
525-
subKeyI, _ := cache.Load("subKey")
526-
return imgKeyI.(string), subKeyI.(string)
538+
func removeUnwantedChars(v url.Values, chars ...byte) url.Values {
539+
b := []byte(v.Encode())
540+
for _, c := range chars {
541+
b = bytes.ReplaceAll(b, []byte{c}, nil)
542+
}
543+
s, err := url.ParseQuery(string(b))
544+
if err != nil {
545+
panic(err)
546+
}
547+
return s
527548
}
528549

529-
func getWbiKeys() (string, string) {
530-
client := &http.Client{}
531-
req, err := http.NewRequest("GET", "https://api.bilibili.com/x/web-interface/nav", nil)
532-
if err != nil {
533-
fmt.Printf("Error creating request: %s", err)
534-
return "", ""
535-
}
536-
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")
537-
req.Header.Set("Referer", "https://www.bilibili.com/")
538-
resp, err := client.Do(req)
539-
if err != nil {
540-
fmt.Printf("Error sending request: %s", err)
541-
return "", ""
542-
}
543-
defer resp.Body.Close()
544-
body, err := io.ReadAll(resp.Body)
545-
if err != nil {
546-
fmt.Printf("Error reading response: %s", err)
547-
return "", ""
548-
}
549-
json := string(body)
550-
imgURL := gjson.Get(json, "data.wbi_img.img_url").String()
551-
subURL := gjson.Get(json, "data.wbi_img.sub_url").String()
552-
imgKey := strings.Split(strings.Split(imgURL, "/")[len(strings.Split(imgURL, "/"))-1], ".")[0]
553-
subKey := strings.Split(strings.Split(subURL, "/")[len(strings.Split(subURL, "/"))-1], ".")[0]
554-
return imgKey, subKey
550+
type Nav struct {
551+
Code int `json:"code"`
552+
Message string `json:"message"`
553+
Ttl int `json:"ttl"`
554+
Data struct {
555+
WbiImg struct {
556+
ImgUrl string `json:"img_url"`
557+
SubUrl string `json:"sub_url"`
558+
} `json:"wbi_img"`
559+
560+
// ......
561+
} `json:"data"`
555562
}
556563
```
557564

0 commit comments

Comments
 (0)