From 8fd1efd8bf328e3ee68ac2c568c0c4553c511ff9 Mon Sep 17 00:00:00 2001 From: guozhigq Date: Fri, 14 Jul 2023 13:38:23 +0800 Subject: [PATCH] feat: Wbi sign --- lib/http/api.dart | 5 + lib/http/member.dart | 34 +++++ lib/models/member/info.dart | 29 +++++ lib/pages/dynamics/widgets/author_panel.dart | 6 +- lib/pages/dynamics/widgets/video_panel.dart | 6 +- lib/pages/member/controller.dart | 28 ++++ lib/pages/member/index.dart | 4 + lib/pages/member/view.dart | 20 +++ lib/router/app_pages.dart | 3 + lib/utils/wbi_sign.dart | 130 +++++++++++++++++++ pubspec.lock | 2 +- pubspec.yaml | 1 + 12 files changed, 260 insertions(+), 8 deletions(-) create mode 100644 lib/http/member.dart create mode 100644 lib/models/member/info.dart create mode 100644 lib/pages/member/controller.dart create mode 100644 lib/pages/member/index.dart create mode 100644 lib/pages/member/view.dart create mode 100644 lib/utils/wbi_sign.dart diff --git a/lib/http/api.dart b/lib/http/api.dart index 3301c1dc..9ea957e3 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -196,4 +196,9 @@ class Api { // qn 80:流畅,150:高清,400:蓝光,10000:原画,20000:4K, 30000:杜比 static const String liveRoomInfo = 'https://api.live.bilibili.com/xlive/web-room/v2/index/getRoomPlayInfo'; + + // 用户信息 需要Wbi签名 + // https://api.bilibili.com/x/space/wbi/acc/info?mid=503427686&token=&platform=web&web_location=1550101&w_rid=d709892496ce93e3d94d6d37c95bde91&wts=1689301482 + static const String memberInfo = + 'https://api.bilibili.com/x/space/wbi/acc/info'; } diff --git a/lib/http/member.dart b/lib/http/member.dart new file mode 100644 index 00000000..40b1c4c4 --- /dev/null +++ b/lib/http/member.dart @@ -0,0 +1,34 @@ +import 'package:pilipala/http/index.dart'; +import 'package:pilipala/models/member/info.dart'; + +class MemberHttp { + static Future memberInfo({String? params}) async { + var res = await Request().get(Api.memberInfo + params!); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': MemberInfoModel.fromJson(res.data['data']) + }; + } else { + return { + 'status': false, + 'data': [], + 'msg': res.data['message'], + }; + } + } + + static Future memberStat({int? mid}) async { + var res = await Request().get(Api.userStat, data: {mid: mid}); + if (res.data['code'] == 0) { + print(res.data['data']); + // return {'status': true, 'data': FansDataModel.fromJson(res.data['data'])}; + } else { + return { + 'status': false, + 'data': [], + 'msg': res.data['message'], + }; + } + } +} diff --git a/lib/models/member/info.dart b/lib/models/member/info.dart new file mode 100644 index 00000000..9b7b9453 --- /dev/null +++ b/lib/models/member/info.dart @@ -0,0 +1,29 @@ +class MemberInfoModel { + MemberInfoModel({ + this.mid, + this.name, + this.sex, + this.face, + this.sign, + this.level, + this.isFollowed, + }); + + int? mid; + String? name; + String? sex; + String? face; + String? sign; + int? level; + bool? isFollowed; + + MemberInfoModel.fromJson(Map json) { + mid = json['mid']; + name = json['name']; + sex = json['sex']; + face = json['face']; + sign = json['sign']; + level = json['level']; + isFollowed = json['is_followed']; + } +} diff --git a/lib/pages/dynamics/widgets/author_panel.dart b/lib/pages/dynamics/widgets/author_panel.dart index f1d86067..8aa33487 100644 --- a/lib/pages/dynamics/widgets/author_panel.dart +++ b/lib/pages/dynamics/widgets/author_panel.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; Widget author(item, context) { @@ -7,9 +8,8 @@ Widget author(item, context) { child: Row( children: [ GestureDetector( - onTap: () { - print('个人主页'); - }, + onTap: () => + Get.toNamed('/member?mid=${item.modules.moduleAuthor.mid}'), child: NetworkImgLayer( width: 40, height: 40, diff --git a/lib/pages/dynamics/widgets/video_panel.dart b/lib/pages/dynamics/widgets/video_panel.dart index 0ae3aa49..63acd375 100644 --- a/lib/pages/dynamics/widgets/video_panel.dart +++ b/lib/pages/dynamics/widgets/video_panel.dart @@ -94,7 +94,7 @@ Widget videoSeasonWidget(item, context, type, {floor = 1}) { clipBehavior: Clip.hardEdge, decoration: BoxDecoration( gradient: const LinearGradient( - begin: Alignment.center, + begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.transparent, @@ -115,9 +115,7 @@ Widget videoSeasonWidget(item, context, type, {floor = 1}) { color: Colors.white), child: Row( children: [ - pBadge(content.durationText ?? '', context, null, - null, 0, 0, - type: 'gray'), + Text(content.durationText ?? ''), if (content.durationText != null) const SizedBox(width: 10), Text(content.stat.play + '次围观'), diff --git a/lib/pages/member/controller.dart b/lib/pages/member/controller.dart new file mode 100644 index 00000000..dc662f6d --- /dev/null +++ b/lib/pages/member/controller.dart @@ -0,0 +1,28 @@ +import 'package:get/get.dart'; +import 'package:pilipala/http/member.dart'; +import 'package:pilipala/utils/wbi_sign.dart'; + +class MemberController extends GetxController { + late int mid; + + @override + void onInit() { + super.onInit(); + mid = int.parse(Get.parameters['mid']!); + getInfo(); + } + + getInfo() async { + String params = await WbiSign().makSign({ + 'mid': mid, + 'token': '', + 'platform': 'web', + 'web_location': 1550101, + }); + params = '?$params'; + var res = await MemberHttp.memberInfo(params: params); + if (res['status']) { + print(res['data']); + } + } +} diff --git a/lib/pages/member/index.dart b/lib/pages/member/index.dart new file mode 100644 index 00000000..c422fb09 --- /dev/null +++ b/lib/pages/member/index.dart @@ -0,0 +1,4 @@ +library member; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/member/view.dart b/lib/pages/member/view.dart new file mode 100644 index 00000000..0cd02c55 --- /dev/null +++ b/lib/pages/member/view.dart @@ -0,0 +1,20 @@ +import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/pages/member/index.dart'; + +class MemberPage extends StatefulWidget { + const MemberPage({super.key}); + + @override + State createState() => _MemberPageState(); +} + +class _MemberPageState extends State { + final MemberController _memberController = Get.put(MemberController()); + + @override + Widget build(BuildContext context) { + return Scaffold(); + } +} diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index 899aa4ca..5474607c 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -10,6 +10,7 @@ import 'package:pilipala/pages/home/index.dart'; import 'package:pilipala/pages/hot/index.dart'; import 'package:pilipala/pages/later/index.dart'; import 'package:pilipala/pages/liveRoom/view.dart'; +import 'package:pilipala/pages/member/index.dart'; import 'package:pilipala/pages/preview/index.dart'; import 'package:pilipala/pages/search/index.dart'; import 'package:pilipala/pages/searchResult/index.dart'; @@ -62,5 +63,7 @@ class Routes { GetPage(name: '/fan', page: () => const FansPage()), // 直播详情 GetPage(name: '/liveRoom', page: () => const LiveRoomPage()), + // 用户中心 + GetPage(name: '/member', page: () => const MemberPage()), ]; } diff --git a/lib/utils/wbi_sign.dart b/lib/utils/wbi_sign.dart new file mode 100644 index 00000000..419884ce --- /dev/null +++ b/lib/utils/wbi_sign.dart @@ -0,0 +1,130 @@ +// Wbi签名 用于生成 REST API 请求中的 w_rid 和 wts 字段 +// https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/misc/sign/wbi.md +// import md5 from 'md5' +// import axios from 'axios' +import 'package:pilipala/http/index.dart'; +import 'package:crypto/crypto.dart'; +import 'dart:convert'; + +class WbiSign { + List mixinKeyEncTab = [ + 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 + ]; + // 对 imgKey 和 subKey 进行字符顺序打乱编码 + String getMixinKey(orig) { + String temp = ''; + for (int i = 0; i < mixinKeyEncTab.length; i++) { + temp += orig.split('')[mixinKeyEncTab[i]]; + } + return temp.substring(0, 32); + } + + // 为请求参数进行 wbi 签名 + String encWbi(params, imgKey, subKey) { + String mixinKey = getMixinKey(imgKey + subKey); + DateTime now = DateTime.now(); + int currTime = (now.millisecondsSinceEpoch / 1000).round(); + RegExp chrFilter = RegExp(r"[!\'\(\)*]"); + List query = []; + Map newParams = Map.from(params)..addAll({"wts": currTime}); // 添加 wts 字段 + // 按照 key 重排参数 + List keys = newParams.keys.toList()..sort(); + for (var i in keys) { + query.add( + '${Uri.encodeComponent(i)}=${Uri.encodeComponent(newParams[i].toString().replaceAll(chrFilter, ''))}'); + } + String queryStr = query.join('&'); + String wbiSign = + md5.convert(utf8.encode(queryStr + mixinKey)).toString(); // 计算 w_rid + print('w_rid: $wbiSign'); + return '$queryStr&w_rid=$wbiSign'; + } + + // 获取最新的 img_key 和 sub_key + static Future> getWbiKeys() async { + var resp = + await Request().get('https://api.bilibili.com/x/web-interface/nav'); + var jsonContent = resp.data['data']; + + String imgUrl = jsonContent['wbi_img']['img_url']; + String subUrl = jsonContent['wbi_img']['sub_url']; + return { + 'imgKey': imgUrl + .substring(imgUrl.lastIndexOf('/') + 1, imgUrl.length) + .split('.')[0], + 'subKey': subUrl + .substring(subUrl.lastIndexOf('/') + 1, subUrl.length) + .split('.')[0] + }; + } + + makSign(Map params) async { + // params 为需要加密的请求参数 + Map wbiKeys = await getWbiKeys(); + String query = encWbi(params, wbiKeys['imgKey'], wbiKeys['subKey']); + return query; + } +} diff --git a/pubspec.lock b/pubspec.lock index 6820dbc5..dc53572e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -210,7 +210,7 @@ packages: source: hosted version: "0.3.3+4" crypto: - dependency: transitive + dependency: "direct main" description: name: crypto sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 diff --git a/pubspec.yaml b/pubspec.yaml index 6af9ebb0..17d711cd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -86,6 +86,7 @@ dependencies: path: package custom_sliding_segmented_control: ^1.7.5 loading_more_list: ^5.0.3 + crypto: any dev_dependencies: flutter_test: