开源地址 https://github.com/vapehacker/bili-auth
使用演示 https://www.bilibili.com/video/BV1TX4y1c727/

为何做这个项目?

之前自己做一个Openid的小项目,想着接入第三方登录。Github的第三方接入倒是很方便,OAuth接入的教程网上很多,但是Bilibili的接入网上一点相关都没有。然而我是见到过接入B站第三方接入的实例的。没有官方文档,我还闲着无聊去问下客服(不得不说B站客服真的很好,每次答复基本都很快),回复是说现在不开放API。看到这幅图,说明图床已配置好了,嘿嘿

思路

行吧。所以我的想法是通过私信的方式鉴权。待鉴权的用户给机器人帐户发送一条特殊的私信,以证明用户对其B站帐号的所有权,很简单吧。

可能有朋友会不解,为什么不直接通过机器人帐号发送消息,就像短信验证码一样呢?其实这样实现起来会更加简单,还不用消息轮询。只需要实现消息发送就可以。但是这样容易被系统判定机器人帐户频繁地向多个用户主动发起消息,虽然我不知B站有没有私信滥发广告的惩罚判定机制,不过还是免除为好。

功能

实现

经过专业的架构,使用Python作为Bot,轮询B站的私信API(这厮不提供WebSocket,只好轮询),发现新消息后放到消息列表处理,若是特殊格式的验证请求则进一步处理。Bot提供一个HTTP API,将验证都封装好了,只要调用就行。同时我用PHP和JavaScript写了个小的Demo,演示这个Bot的用法。为了快点完成并没有完全前后端分离。另外用JS的原生接口操作DOM,这是第一次,以前我只能用Vue。另外这个Demo并没有完全展示Bot提供的功能,也不是调用这个Bot的标准实现,比如说Bot其实提供了一个撤回验证请求的功能,但是为了方便并没有在Demo中实现。

用到的API

用开发者工具或者其他的什么方式,可以获得B站的私信API。没有混淆加密什么的,这点还是挺良心的。

以下的API如无特殊说明,应带上Cookie,从浏览器复制即可。最好带上正常的User-Agent, OriginReferer(sic)。

GET https://api.vc.bilibili.com/session_svr/v1/session_svr/new_sessions?begin_ts={}&build=0&mobi_app=web

begin_ts: 毫秒时间戳

返回最后消息发送时间begin_ts之前的会话。

Response Body

{
    "code": 0,
    "msg": "ok",
    "message": "ok",
    "data": {
        "session_list": [
            {
                "talker_id": 362062895,
                "session_type": 1,
                "top_ts": 0,
                "is_follow": 0,
                "is_dnd": 0,
                "ack_seqno": 已接收的最后一条消息的编号,
                "ack_ts": 毫秒级时间戳,
                "session_ts": 毫秒级时间戳,
                "unread_count": 未读消息数目,
                "last_msg": {
                    "sender_uid": 字面意思,
                    "receiver_type": 1,
                    "receiver_id": 字面意思,
                    "msg_type": 整数,其中1是纯文本消息,格式如下,
                    "content": "{\"content\":\"纯文本消息内容\"}",
                    "msg_seqno": 消息的顺序编号,
                    "timestamp": 秒级时间戳,
                    "at_uids": [
                        0
                    ],
                    "msg_key": 消息的唯一索引,
                    "msg_status": 0,
                    "notify_code": "",
                    "new_face_version": 0
                },
                "can_fold": 1,
                "status": 0,
                "max_seqno": 会话中最新消息的编号,
                "new_push_msg": 0,
                "setting": 0,
                "is_guardian": 0
            }
        ],
        "has_more": 0,
        "_gt_": 0
    }
}

GET https://api.vc.bilibili.com/session_svr/v1/session_svr/ack_sessions?begin_ts={}&build=0&mobi_app=web

GET
https://api.vc.bilibili.com/svr_sync/v1/svr_sync/fetch_session_msgs?sender_device_id=1&talker_id={}&session_type=1&size={}&begin_seqno={}&build=0&mobi_app=web

talker_id: 会话中另一个用户的uid size: 获取消息列表的最大长度 begin_seqno: 起始消息的编号。如果要获取未读消息,应该与第1个API中的ack_seqno相同。

Response Body

{
    "code": 0,
    "msg": "0",
    "message": "0",
    "ttl": 1,
    "data": {
        "messages": [
            {
                "sender_uid": 见上文,
                "receiver_type": 1,
                "receiver_id": 见上文,
                "msg_type": 见上文,
                "content": "{\"content\":\"纯文本消息\"}",
                "msg_seqno": 见上文,
                "timestamp": 见上文,
                "at_uids": [
                    0
                ],
                "msg_key": 见上文,
                "msg_status": 0,
                "notify_code": ""
            }
        ],
        "has_more": 0,
        "min_seqno": 消息列表中,最旧消息的编号,
        "max_seqno": 最新一条消息的编号
    }
}

POST https://api.vc.bilibili.com/web_im/v1/web_im/send_msg Content-Type: application/x-www-form-urlencoded

Form Data msg[sender_uid]: 发信者uid msg[receiver_id]: 收信者uid msg[receiver_type]: 1 msg[msg_type]: 见上文 msg[msg_status]: 0 msg[content]: {"content":"纯文本内容"} msg[timestamp]: 秒级时间戳 msg[new_face_version]: 0 msg[dev_id]: 设备id,从浏览器发出一条消息后通过开发者工具获取 from_firework: 0 build: 0 mobi_app: web csrf_token: Cookie中"bili_jct"对应的值 csrf: 同上

偷个懒,响应Body不像写了,基本上没什么新的,只是多一个msg_key,消息的唯一索引,暂时不知道有什么用

GET https://api.bilibili.com/x/space/acc/info?mid={}&jsonp=jsonp

mid: 被查询用户的uid

此API可以无鉴权访问,不需要特殊的Header。

Response Body

{
    "code": 0,
    "message": "0",
    "ttl": 1,
    "data": {
        "mid": 用户uid,
        "name": 昵称,
        "sex": 性别(中文),
        "face": 头像URL(坑点:无法跨域访问),
        "sign": 个性签名,
        "rank": 10000,
        "level": 6,
        "jointime": 0,
        "moral": 0,
        "silence": 0,
        "birthday": "07-07",
        "coins": 0,
        "fans_badge": false,
        "fans_medal": {
            "show": true,
            "wear": true,
            "medal": {
                "uid": 用户uid,
                "target_id": 不知道,
                "medal_id": 396868,
                "level": 5,
                "medal_name": 大概是粉丝头衔的名称,
                "medal_color": 6126494,
                "intimacy": 358,
                "next_intimacy": 1000,
                "day_limit": 1500,
                "medal_color_start": 6126494,
                "medal_color_end": 6126494,
                "medal_color_border": 6126494,
                "is_lighted": 1,
                "light_status": 1,
                "wearing_status": 1,
                "score": 2059
            }
        },
        "official": {
            "role": 0,
            "title": "",
            "desc": "",
            "type": -1
        },
        "vip": {
            "type": 2,
            "status": 1,
            "due_date": 1629129600000,
            "vip_pay_type": 1,
            "theme_type": 0,
            "label": {
                "path": "",
                "text": "年度大会员",
                "label_theme": "annual_vip",
                "text_color": "#FFFFFF",
                "bg_style": 1,
                "bg_color": "#FB7299",
                "border_color": ""
            },
            "avatar_subscript": 1,
            "nickname_color": "#FB7299",
            "role": 3,
            "avatar_subscript_url": 头像角标(比如大会员)图片的URL
        },
        "pendant": {
            "pid": 0,
            "name": "",
            "image": "",
            "expire": 0,
            "image_enhance": "",
            "image_enhance_frame": ""
        },
        "nameplate": {
            "nid": 0,
            "name": "",
            "image": "",
            "image_small": "",
            "level": "",
            "condition": ""
        },
        "user_honour_info": {
            "mid": 0,
            "colour": null,
            "tags": null
        },
        "is_followed": bool,可能与关注有关,
        "top_photo": 用户首页顶部展示的图片URL,
        "theme": {},
        "sys_notice": {},
        "live_room": {
            "roomStatus": 1,
            "liveStatus": 0,
            "url": 直播间的URL,
            "title": 直播间标题,
            "cover": 不知道,没查证,可以自己试试,
            "online": 5,
            "roomid": 13015,
            "roundStatus": 0,
            "broadcast_type": 0
        }
    }
}

最后

在前端获取用户信息和头像时遇到了跨域问题(连头像都做防盗链...),纯前端没法解决,无奈用PHP写了个API代理。至于头像,受限于带宽,算了:)(好像可以用Service Worker?) 调试的时候经常出问题,无奈下载了 Pycharm 社区版(有IDE真是不错),搞完了发现只是一些我自己脑瘫至极导致的小问题而已。 捣鼓得好累。。。这个项目就这样了吧,以后再完善了。