返回提交历史
Modified
docs/misc/sign/wbi.md
+157
-150
XFEstudio/bilibili-API-collect
重构 wbi golang 实现
724e618
代码差异
1 个文件
+157
-150
@@ -380,178 +380,185 @@ bar=514&baz=1919810&foo=114&wts=1684805578&w_rid=bb97e15f28edf445a0e4420d36f0157
380
380
381
381
### Golang
382
382
383
需要 `github.com/tidwall/gjson` 作为依赖
383
无第三方库
384
384
385
385
```go
386
386
package main
387
387
388
388
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"
413
400
)
414
401
415
402
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()
441
426
}
442
427
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
461
462
}
462
463
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)
492
467
}
493
468
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
502
516
}
503
517
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[:])
510
525
}
511
526
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,
520
536
}
521
537
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
527
548
}
528
549
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"`
555
562
}
556
563
```
557
564