mirror of
https://github.com/lkeme/BiliHelper-personal.git
synced 2025-12-19 09:30:10 +08:00
519 lines
18 KiB
PHP
519 lines
18 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Website: https://mudew.com/
|
|
* Author: Lkeme
|
|
* License: The MIT License
|
|
* Updated: 2021 ~ 2022
|
|
*/
|
|
|
|
namespace BiliHelper\Plugin;
|
|
|
|
use BiliHelper\Core\Log;
|
|
use BiliHelper\Core\Curl;
|
|
use BiliHelper\Util\TimeLock;
|
|
use BiliHelper\Tool\Common;
|
|
|
|
class Login
|
|
{
|
|
use TimeLock;
|
|
|
|
// 账密
|
|
private static $username;
|
|
private static $password;
|
|
|
|
public static function run()
|
|
{
|
|
if (self::getLock()) {
|
|
self::keepAuth();
|
|
return;
|
|
}
|
|
Log::info('启动登录程序');
|
|
if (getAccessToken() == '') {
|
|
Log::info('准备载入登录令牌');
|
|
self::login();
|
|
}
|
|
Log::info('检查登录令牌有效性');
|
|
if (!self::checkToken()) {
|
|
Log::warning('登录令牌失效或即将过期');
|
|
Log::info('申请更换登录令牌中');
|
|
if (!self::refreshToken()) {
|
|
Log::warning('无效的登录令牌,尝试重新申请');
|
|
self::login();
|
|
}
|
|
}
|
|
self::setLock(3600);
|
|
}
|
|
|
|
/**
|
|
* @use 登录控制中心
|
|
*/
|
|
private static function login()
|
|
{
|
|
self::checkLogin();
|
|
switch (getConf('mode', 'login.mode')) {
|
|
case 1:
|
|
// 账密模式
|
|
self::accountLogin();
|
|
break;
|
|
case 2:
|
|
// 短信验证码模式
|
|
self::smsLogin();
|
|
break;
|
|
case 3:
|
|
// 行为验证码模式(暂未开放)
|
|
// self::captchaLogin();
|
|
Log::error('此登录模式暂未开放');
|
|
die();
|
|
default:
|
|
Log::error('登录模式配置错误');
|
|
die();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @use 检查登录
|
|
*/
|
|
private static function checkLogin()
|
|
{
|
|
$username = getConf('username', 'login.account');
|
|
$password = getConf('password', 'login.account');
|
|
if (empty($username) || empty($password)) {
|
|
Log::error('空白的帐号和口令');
|
|
die();
|
|
}
|
|
self::clearAccount();
|
|
self::$username = $username;
|
|
self::$password = self::publicKeyEnc($password);
|
|
}
|
|
|
|
/**
|
|
* @use 保持认证
|
|
* @return bool
|
|
*/
|
|
private static function keepAuth(): bool
|
|
{
|
|
if (self::getLock() > time()) {
|
|
return true;
|
|
}
|
|
self::setLock(7200);
|
|
if (!self::checkToken()) {
|
|
Log::warning('令牌即将过期');
|
|
Log::info('申请更换令牌中...');
|
|
if (!self::refreshToken()) {
|
|
Log::warning('无效令牌,正在重新申请...');
|
|
self::accountLogin();
|
|
}
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @use 获取令牌信息
|
|
* @return bool
|
|
*/
|
|
private static function checkToken(): bool
|
|
{
|
|
$url = 'https://passport.bilibili.com/api/v2/oauth2/info';
|
|
$payload = [
|
|
'access_token' => getAccessToken(),
|
|
];
|
|
$data = Curl::get('app', $url, Sign::common($payload));
|
|
// {"ts":1234,"code":0,"data":{"mid":1234,"access_token":"1234","expires_in":7759292}}
|
|
$data = json_decode($data, true);
|
|
if (isset($data['code']) && $data['code']) {
|
|
Log::error('检查令牌失败', ['msg' => $data['message']]);
|
|
return false;
|
|
}
|
|
Log::notice('令牌有效期: ' . date('Y-m-d H:i:s', $data['ts'] + $data['data']['expires_in']));
|
|
return $data['data']['expires_in'] > 14400;
|
|
}
|
|
|
|
/**
|
|
* @use 刷新Token
|
|
*/
|
|
private static function refreshToken(): bool
|
|
{
|
|
$url = 'https://passport.bilibili.com/api/v2/oauth2/refresh_token';
|
|
$payload = [
|
|
'access_token' => getAccessToken(),
|
|
'refresh_token' => getRefreshToken(),
|
|
];
|
|
$raw = Curl::post('app', $url, Sign::common($payload));
|
|
$de_raw = json_decode($raw, true);
|
|
// {"message":"user not login","ts":1593111694,"code":-101}
|
|
if (isset($de_raw['code']) && $de_raw['code']) {
|
|
Log::error('重新生成令牌失败', ['msg' => $de_raw['message']]);
|
|
return false;
|
|
}
|
|
self::refreshSuccess($de_raw);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @use 公钥加密
|
|
* @param $plaintext
|
|
* @return string
|
|
*/
|
|
private static function publicKeyEnc($plaintext): string
|
|
{
|
|
Log::info('正在载入公钥');
|
|
// $url = 'https://passport.bilibili.com/api/oauth2/getKey';
|
|
$url = 'https://passport.bilibili.com/x/passport-login/web/key';
|
|
$payload = [];
|
|
$data = Curl::get('app', $url, Sign::login($payload));
|
|
$data = json_decode($data, true);
|
|
if (isset($data['code']) && $data['code']) {
|
|
Log::error('公钥载入失败', ['msg' => $data['message']]);
|
|
die();
|
|
} else {
|
|
Log::info('公钥载入完毕');
|
|
}
|
|
// print_r($data);
|
|
$public_key = $data['data']['key'];
|
|
$hash = $data['data']['hash'];
|
|
openssl_public_encrypt($hash . $plaintext, $crypt, $public_key);
|
|
return base64_encode($crypt);
|
|
}
|
|
|
|
/**
|
|
* @use 获取验证码
|
|
* @return array|string[]
|
|
*/
|
|
private static function getCaptcha(): array
|
|
{
|
|
$url = 'https://passport.bilibili.com/web/captcha/combine';
|
|
$payload = [
|
|
'plat' => 3
|
|
];
|
|
$raw = Curl::get('other', $url, $payload);
|
|
$de_raw = json_decode($raw, true);
|
|
// {"code":0,"data":{"result":{"success":1,"gt":"b6e5b7fad7ecd37f465838689732e788","challenge":"88148a764f94e5923564b356a69277fc","key":"230509df5ce048ca9bf29e1ee323af30"},"type":1}}
|
|
Log::info('正在获取验证码 ' . $de_raw['code']);
|
|
if ($de_raw['code'] == 0 && isset($de_raw['data']['result'])) {
|
|
return [
|
|
'gt' => $de_raw['data']['result']['gt'],
|
|
'challenge' => $de_raw['data']['result']['challenge'],
|
|
'key' => $de_raw['data']['result']['key'],
|
|
];
|
|
}
|
|
return [
|
|
'gt' => '',
|
|
'challenge' => '',
|
|
'key' => ''
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @use 识别验证码
|
|
* @param array $captcha
|
|
* @return array
|
|
*/
|
|
private static function ocrCaptcha(array $captcha): array
|
|
{
|
|
$url = 'https://captcha-v1.mudew.com:19951/';
|
|
$payload = [
|
|
'type' => 'gt3',
|
|
'gt' => $captcha['gt'],
|
|
"challenge" => $captcha['challenge'],
|
|
"referer" => "https://passport.bilibili.com/"
|
|
];
|
|
$headers = [
|
|
'Content-Type' => 'application/json',
|
|
];
|
|
$data = Curl::post('other', $url, $payload, $headers);
|
|
$de_raw = json_decode($data, true);
|
|
Log::info('正在获取验证码 ' . $de_raw['code']);
|
|
return [
|
|
'validate' => $de_raw['data']['validate'],
|
|
'challenge' => $de_raw['data']['challenge']
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @use 账密登录
|
|
* @param string $validate
|
|
* @param string $challenge
|
|
* @param string $mode
|
|
*/
|
|
private static function accountLogin(string $validate = '', string $challenge = '', string $mode = '账密模式')
|
|
{
|
|
Log::info("尝试 {$mode} 登录");
|
|
// $url = 'https://passport.bilibili.com/api/v3/oauth2/login';
|
|
$url = 'https://passport.bilibili.com/x/passport-login/oauth2/login';
|
|
$payload = [
|
|
'seccode' => $validate ? "{$validate}|jordan" : '',
|
|
'validate' => $validate,
|
|
'challenge' => $challenge,
|
|
'permission' => 'ALL',
|
|
'username' => self::$username,
|
|
'password' => self::$password,
|
|
'captcha' => '',
|
|
'subid' => 1,
|
|
'cookies' => ''
|
|
];
|
|
$raw = Curl::post('app', $url, Sign::login($payload));
|
|
$de_raw = json_decode($raw, true);
|
|
// {"ts":1593079322,"code":-629,"message":"账号或者密码错误"}
|
|
// {"ts":1593082268,"code":-105,"data":{"url":"https://passport.bilibili.com/register/verification.html?success=1>=b6e5b7fad7ecd37f465838689732e788&challenge=7efb4020b22c0a9ac124aea624e11ad7&ct=1&hash=7fa8282ad93047a4d6fe6111c93b308a"},"message":"验证码错误"}
|
|
// {"ts":1593082432,"code":0,"data":{"status":0,"token_info":{"mid":123456,"access_token":"123123","refresh_token":"123123","expires_in":2592000},"cookie_info":{"cookies":[{"name":"bili_jct","value":"123123","http_only":0,"expires":1595674432},{"name":"DedeUserID","value":"123456","http_only":0,"expires":1595674432},{"name":"DedeUserID__ckMd5","value":"123123","http_only":0,"expires":1595674432},{"name":"sid","value":"bd6aagp7","http_only":0,"expires":1595674432},{"name":"SESSDATA","value":"6d74d850%123%2Cf0e36b61","http_only":1,"expires":1595674432}],"domains":[".bilibili.com",".biligame.com",".bigfunapp.cn"]},"sso":["https://passport.bilibili.com/api/v2/sso","https://passport.biligame.com/api/v2/sso","https://passport.bigfunapp.cn/api/v2/sso"]}}
|
|
// {"ts":1610254019,"code":0,"data":{"status":2,"url":"https://passport.bilibili.com/account/mobile/security/managephone/phone/verify?tmp_token=2bc5dd260df7158xx860565fxx0d5311&requestId=dffcfxx052fe11xxa9c8e2667739c15c&source=risk","message":"您的账号存在高危异常行为,为了您的账号安全,请验证手机号后登录帐号"}}
|
|
// https://passport.bilibili.com/mobile/verifytel_h5.html
|
|
self::loginAfter($mode, $de_raw['code'], $de_raw);
|
|
}
|
|
|
|
/**
|
|
* @use 短信登录
|
|
* @param string $mode
|
|
*/
|
|
private static function smsLogin(string $mode = '短信模式')
|
|
{
|
|
Log::info("尝试 {$mode} 登录");
|
|
if (getConf('phone', 'login.check')) {
|
|
self::checkPhone(self::$username);
|
|
}
|
|
$captcha = self::sendSms(self::$username);
|
|
$url = 'https://passport.bilibili.com/x/passport-login/login/sms';
|
|
$payload = [
|
|
'captcha_key' => $captcha['captcha_key'],
|
|
'cid' => $captcha['cid'],
|
|
'tel' => $captcha['tel'],
|
|
'statistics' => $captcha['statistics'],
|
|
'code' => self::cliInput('请输入收到的短信验证码: '),
|
|
];
|
|
$raw = Curl::post('app', $url, Sign::login($payload));
|
|
$de_raw = json_decode($raw, true);
|
|
self::loginAfter($mode, $de_raw['code'], $de_raw);
|
|
}
|
|
|
|
/**
|
|
* @use 输入短信验证码
|
|
* @param string $msg
|
|
* @param int $max_char
|
|
* @return string
|
|
*/
|
|
private static function cliInput(string $msg, $max_char = 100): string
|
|
{
|
|
$stdin = fopen('php://stdin', 'r');
|
|
echo '# ' . $msg;
|
|
$input = fread($stdin, $max_char);
|
|
fclose($stdin);
|
|
return str_replace(PHP_EOL, '', $input);
|
|
}
|
|
|
|
/**
|
|
* @use 发送短信验证码
|
|
* @param string $phone
|
|
* @return array
|
|
*/
|
|
private static function sendSms(string $phone): array
|
|
{
|
|
$url = 'https://passport.bilibili.com//x/passport-login/sms/send';
|
|
$payload = [
|
|
'cid' => getConf('country_code', 'login.country') ,
|
|
'tel' => $phone,
|
|
'statistics' => '{"appId":1,"platform":3,"version":"6.32.0","abtest":""}',
|
|
];
|
|
$raw = Curl::post('app', $url, Sign::login($payload));
|
|
$de_raw = json_decode($raw, true);
|
|
// {"code":0,"message":"0","ttl":1,"data":{"is_new":false,"captcha_key":"4e292933816755442c1568e2043b8e41","recaptcha_url":""}}
|
|
// {"code":0,"message":"0","ttl":1,"data":{"is_new":false,"captcha_key":"","recaptcha_url":"https://www.bilibili.com/h5/project-msg-auth/verify?ct=geetest\u0026recaptcha_token=ad520c3a4a3c46e29b1974d85efd2c4b\u0026gee_gt=1c0ea7c7d47d8126dda19ee3431a5f38\u0026gee_challenge=c772673050dce482b9f63ff45b681ceb\u0026hash=ea2850a43cc6b4f1f7b925d601098e5e"}}
|
|
if ($de_raw['code'] == 0 && isset($de_raw['data']['captcha_key']) && $de_raw['data']['recaptcha_url'] == '') {
|
|
Log::info("短信验证码发送成功 {$de_raw['data']['captcha_key']}");
|
|
$payload['captcha_key'] = $de_raw['data']['captcha_key'];
|
|
return $payload;
|
|
}
|
|
Log::error("短信验证码发送失败 {$raw}");
|
|
die();
|
|
}
|
|
|
|
/**
|
|
* @use 登录之后
|
|
* @param $mode
|
|
* @param $code
|
|
* @param $data
|
|
*/
|
|
private static function loginAfter($mode, $code, $data)
|
|
{
|
|
switch ($code) {
|
|
case 0:
|
|
// data->data->status number
|
|
if (array_key_exists('status', $data['data'])) {
|
|
// 二次判断
|
|
switch ($data['data']['status']) {
|
|
case 0:
|
|
// 正常登录
|
|
self::loginSuccess($mode, $data);
|
|
break;
|
|
case 2:
|
|
// 异常高危
|
|
self::loginFail($mode, $data['data']['message']);
|
|
break;
|
|
default:
|
|
// 未知错误
|
|
self::loginFail($mode, '未知错误: ' . $data['data']['message']);
|
|
break;
|
|
}
|
|
} else {
|
|
// 正常登录
|
|
self::loginSuccess($mode, $data);
|
|
}
|
|
break;
|
|
case -105:
|
|
// 需要验证码
|
|
self::loginFail($mode, '此次登录需要验证码或' . $data['message']);
|
|
break;
|
|
case -629:
|
|
// 密码错误
|
|
self::loginFail($mode, $data['message']);
|
|
break;
|
|
case -2100:
|
|
// 验证手机号
|
|
self::loginFail($mode, '账号启用了设备锁或异地登录需验证手机号');
|
|
break;
|
|
default:
|
|
// 未知错误
|
|
self::loginFail($mode, '未知错误: ' . $data['message']);
|
|
break;
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* @use 登录成功
|
|
* @param $mode
|
|
* @param $data
|
|
*/
|
|
private static function loginSuccess($mode, $data)
|
|
{
|
|
Log::info("{$mode} 登录成功");
|
|
self::successHandle($data);
|
|
Log::info('生成信息配置完毕');
|
|
}
|
|
|
|
/**
|
|
* @use 刷新成功
|
|
* @param $data
|
|
*/
|
|
private static function refreshSuccess($data)
|
|
{
|
|
Log::info('重新令牌生成完毕');
|
|
self::successHandle($data);
|
|
Log::info('重置信息配置完毕');
|
|
}
|
|
|
|
/**
|
|
* @use 成功处理
|
|
* @param $data
|
|
*/
|
|
private static function successHandle($data)
|
|
{
|
|
$access_token = $data['data']['token_info']['access_token'];
|
|
$refresh_token = $data['data']['token_info']['refresh_token'];
|
|
self::saveConfig('access_token', $access_token, 'login.auth');
|
|
self::saveConfig('refresh_token', $refresh_token, 'login.auth');
|
|
self::saveConfig('cookie', self::formatCookie($data), 'login.auth');
|
|
$user = User::parseCookies();
|
|
self::saveConfig('uid', $user['uid'], 'login.auth', false);
|
|
self::saveConfig('csrf', $user['csrf'], 'login.auth', false);
|
|
}
|
|
|
|
/**
|
|
* @use 登录失败
|
|
* @param $mode
|
|
* @param $data
|
|
*/
|
|
private static function loginFail($mode, $data)
|
|
{
|
|
Log::error("{$mode} 登录失败", ['msg' => $data]);
|
|
die();
|
|
}
|
|
|
|
/**
|
|
* @use 检查手机号格式
|
|
* @param string $phone
|
|
*/
|
|
private static function checkPhone(string $phone)
|
|
{
|
|
if (!preg_match("/^1[3456789]{1}\d{9}$/", $phone)) {
|
|
Log::error("当前用户名不是有效手机号格式");
|
|
die();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @use 保存配置
|
|
* @param string $key
|
|
* @param string $value
|
|
* @param string $section
|
|
* @param bool $print
|
|
* @param bool $hide
|
|
*/
|
|
private static function saveConfig(string $key, string $value, string $section, $print = true, $hide = true)
|
|
{
|
|
setConf($key, $value, $section);
|
|
if ($print) {
|
|
Log::info(" > {$key}: " . ($hide ? Common::replaceStar($value, 6, 6) : $value));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @use @use 格式化COOKIE
|
|
* @param array $data
|
|
* @return string
|
|
*/
|
|
private static function formatCookie(array $data): string
|
|
{
|
|
$c = '';
|
|
$cookies = $data['data']['cookie_info']['cookies'];
|
|
foreach ($cookies as $cookie) {
|
|
$c .= $cookie['name'] . '=' . $cookie['value'] . ';';
|
|
}
|
|
return $c;
|
|
}
|
|
|
|
/**
|
|
* @use 清除已有
|
|
*/
|
|
private static function clearAccount()
|
|
{
|
|
$variables = ['cookie', 'access_token', 'refresh_token'];
|
|
foreach ($variables as $variable) {
|
|
setConf($variable, '', 'login.auth');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @use 刷新COOKIE
|
|
* @return string
|
|
*/
|
|
private static function refreshCookie(): string
|
|
{
|
|
$url = 'https://passport.bilibili.com/api/login/sso';
|
|
$payload = [
|
|
'gourl' => 'https%3A%2F%2Faccount.bilibili.com%2Faccount%2Fhome'
|
|
];
|
|
$response = Curl::headers('app', $url, Sign::common($payload));
|
|
$headers = $response['Set-Cookie'];
|
|
$cookies = [];
|
|
foreach ($headers as $header) {
|
|
preg_match_all('/^(.*);/iU', $header, $cookie);
|
|
array_push($cookies, $cookie[0][0]);
|
|
}
|
|
return implode("", array_reverse($cookies));
|
|
}
|
|
|
|
/**
|
|
* @use 验证码登录
|
|
* @param string $mode
|
|
*/
|
|
private static function captchaLogin(string $mode = '验证码模式')
|
|
{
|
|
$captcha_ori = self::getCaptcha();
|
|
$captcha = self::ocrCaptcha($captcha_ori);
|
|
self::accountLogin($captcha['validate'], $captcha['challenge'], $mode);
|
|
}
|
|
|
|
|
|
} |