BiliHelper-personal/plugin/Login/Login.php
2022-06-04 15:52:09 +08:00

604 lines
20 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php declare(strict_types=1);
/**
* Website: https://mudew.com/
* Author: Lkeme
* License: The MIT License
* Email: Useri@live.cn
* Updated: 2022 ~ 2023
*
* _____ _ _ _ _ _ _____ _ _____ _____ _____
* | _ \ | | | | | | | | | | | ____| | | | _ \ | ____| | _ \ & l、
* | |_| | | | | | | | | |_| | | |__ | | | |_| | | |__ | |_| | (゚、 。
* | _ { | | | | | | | _ | | __| | | | ___/ | __| | _ /   \、゙ ~ヽ *
* | |_| | | | | |___ | | | | | | | |___ | |___ | | | |___ | | \ \  じしf_, )
* |_____/ |_| |_____| |_| |_| |_| |_____| |_____| |_| |_____| |_| \_\
*/
use Bhp\Api\Passport\ApiCaptcha;
use Bhp\Api\Passport\ApiOauth2;
use Bhp\Api\PassportTv\ApiQrcode;
use Bhp\Cache\Cache;
use Bhp\Log\Log;
use Bhp\Plugin\BasePlugin;
use Bhp\Plugin\Plugin;
use Bhp\TimeLock\TimeLock;
use Bhp\User\User;
use Bhp\Util\Common\Common;
use Bhp\Util\Qrcode\Qrcode;
use JetBrains\PhpStorm\NoReturn;
class Login extends BasePlugin
{
/**
* 插件信息
* @var array|string[]
*/
protected ?array $info = [
'hook' => __CLASS__, // hook
'name' => 'Login', // 插件名称
'version' => '0.0.1', // 插件版本
'desc' => '登录模块', // 插件描述
'author' => 'Lkeme',// 作者
'priority' => 1001, // 插件优先级
'cycle' => '24(小时)', // 运行周期
];
/**
* @var string|null
*/
protected ?string $username = '';
/**
* @var string|null
*/
protected ?string $password = '';
/**
* @param Plugin $plugin
*/
public function __construct(Plugin &$plugin)
{
//
TimeLock::initTimeLock();
//
Cache::initCache();
//
Log::info('加载Login插件');
// $this::class
$plugin->register($this, 'execute');
}
/**
* @return void
*/
public function execute(): void
{
//
if (TimeLock::getTimes() && TimeLock::getTimes() < time()) {
TimeLock::setTimes(7200);
$this->keepLogin();
}
//
if (!TimeLock::getTimes()) {
$this->initLogin();
TimeLock::setTimes(3600);
}
}
/**
* @use 初始化登录
* @return void
*/
protected function initLogin(): void
{
//
$token = getU('access_token');
$r_token = getU('refresh_token');
// Token不存在的情况\直接调用登录
Log::info('启动登录程序');
if (!$token || !$r_token) {
Log::info('准备载入登录令牌');
$this->login();
}
// Token存在\校验有效性\否则调用登录
Log::info('检查登录令牌有效性');
if (!$this->validateToken($token)) {
Log::warning('登录令牌失效或即将过期');
Log::info('申请更换登录令牌中');
if (!$this->refreshToken($token, $r_token)) {
Log::warning('无效的登录令牌,尝试重新申请');
$this->login();
}
}
}
/**
* @use 登录控制中心
* @return void
*/
protected function login(): void
{
$this->checkLogin();
//
switch (getConf('login_mode.mode')) {
case 1:
// 账密模式
$this->accountLogin();
break;
case 2:
// 短信验证码模式
$this->smsLogin();
break;
case 3:
// 二维码模式
$this->qrcodeLogin();
break;
case 4:
// 行为验证码模式(暂未开放)
// self::captchaLogin();
failExit('此登录模式暂未开放');
default:
failExit('登录模式配置错误');
}
}
/**
* @use 保持认证
* @return bool
*/
protected function keepLogin(): bool
{
//
$token = getU('access_token');
$r_token = getU('refresh_token');
//
if ($this->validateToken($token)) {
return true;
}
Log::warning('令牌即将过期');
Log::info('申请更换令牌中...');
if (!$this->refreshToken($token, $r_token)) {
Log::warning('无效令牌,正在重新申请...');
self::accountLogin();
}
return false;
}
/**
* @use 校验令牌信息
* @param string $token
* @return bool
*/
protected function validateToken(string $token): bool
{
// {"ts":1234,"code":0,"data":{"mid":1234,"access_token":"1234","expires_in":7759292}}
$response = ApiOauth2::tokenInfo($token);
//
if (isset($response['code']) && $response['code']) {
Log::error('检查令牌失败', ['msg' => $response['message']]);
return false;
}
Log::notice('令牌有效期: ' . date('Y-m-d H:i:s', $response['ts'] + $response['data']['expires_in']));
return $response['data']['expires_in'] > 14400;
}
/**
* @use 刷新token
* @param string $token
* @param string $r_token
* @return bool
*/
protected function refreshToken(string $token, string $r_token): bool
{
$response = ApiOauth2::tokenRefresh($token, $r_token);
// {"message":"user not login","ts":1593111694,"code":-101}
if (isset($response['code']) && $response['code']) {
Log::error('重新生成令牌失败', ['msg' => $response['message']]);
return false;
}
Log::info('重新令牌生成完毕');
$this->updateLoginInfo($response);
Log::info('重置信息配置完毕');
return true;
}
/**
* @use 更新登录信息
* @param array $data
*/
protected function updateLoginInfo(array $data): void
{
//
$access_token = $data['data']['token_info']['access_token'];
$this->updateInfo('access_token', $access_token);
//
$refresh_token = $data['data']['token_info']['refresh_token'];
$this->updateInfo('refresh_token', $refresh_token);
//
$cookie = $this->formatCookie($data['data']['cookie_info']['cookies']);
$this->updateInfo('cookie', $cookie);
//
$user = User::parseCookie();
$this->updateInfo('uid', $user['uid'], false);
$this->updateInfo('csrf', $user['csrf'], false);
$this->updateInfo('sid', $user['sid'], false);
//
// $this->updateInfo('username',$this->username);
// $this->updateInfo('password',$this->password);
}
/**
* @use 更新Tv登录信息
* @param array $data
*/
protected function updateTvLoginInfo(array $data): void
{
//
$access_token = $data['data']['access_token'];
$this->updateInfo('access_token', $access_token);
//
$refresh_token = $data['data']['refresh_token'];
$this->updateInfo('refresh_token', $refresh_token);
//
//
$cookie = $this->token2Cookie($access_token);
$this->updateInfo('cookie', $cookie);
//
$user = User::parseCookie();
$this->updateInfo('uid', $user['uid'], false);
$this->updateInfo('csrf', $user['csrf'], false);
$this->updateInfo('sid', $user['sid'], false);
//
// $this->updateInfo('username',$this->username);
// $this->updateInfo('password',$this->password);
}
/**
* @use 更新信息
* @param string $key
* @param mixed $value
* @param bool $print
* @param bool $hide
* @return void
*/
protected function updateInfo(string $key, mixed $value, bool $print = true, bool $hide = true): void
{
setU($key, $value);
if ($print) {
Log::info(" > $key: " . ($hide ? Common::replaceStar($value, 6, 6) : $value));
}
}
/**
* @use 格式化Cookie
* @param array $cookies
* @return string
*/
protected function formatCookie(array $cookies): string
{
$c = '';
foreach ($cookies as $cookie) {
$c .= $cookie['name'] . '=' . $cookie['value'] . ';';
}
return $c;
}
/**
* @use 账密登录
* @param string $validate
* @param string $challenge
* @param string $mode
* @return void
*/
protected function accountLogin(string $validate = '', string $challenge = '', string $mode = '账密模式'): void
{
Log::info("尝试 $mode 登录");
// {"ts":1593079322,"code":-629,"message":"账号或者密码错误"}
// {"ts":1593082268,"code":-105,"data":{"url":"https://passport.bilibili.com/register/verification.html?success=1&gt=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
$response = ApiQrcode::passwordLogin($this->username, $this->password, $validate, $challenge);
//
$this->loginAfter($mode, $response['code'], $response);
}
/**
* @use 短信登录
* @param string $mode
* @return void
*/
protected function smsLogin(string $mode = '短信模式'): void
{
Log::info("尝试 $mode 登录");
//
if (getConf('login_check.phone')) {
if (!Common::checkPhone($this->username)) {
failExit('当前用户名不是有效手机号格式');
}
}
//
$captcha = $this->sendSms($this->username, getConf('login_country.code'));
$code = $this->cliInput('请输入收到的短信验证码: ');
$response = ApiQrcode::smsLogin($captcha, $code);
//
$this->loginAfter($mode, $response['code'], $response);
}
/**
* @use 扫码登录
* @param string $mode
* @return void
*/
protected function qrcodeLogin(string $mode = '扫码模式'): void
{
Log::info("尝试 $mode 登录");
//
$this->cliInput("请尝试放大窗口,以确保二维码完整显示,回车继续");
//
$response = $this->fetchQrAuthCode();
$auth_code = $response['auth_code'];
//
Qrcode::show($response['url']);
// max 180 step 3
foreach (range(0, 180, 3) as $_) {
sleep(3);
if ($this->validateQrAuthCode($auth_code)) {
return;
}
}
failExit("扫码失败 二维码已失效");
}
/**
* @use 获取AuthCode
* @return array
*/
protected function fetchQrAuthCode(): array
{
// {"code":0,"message":"0","ttl":1,"data":{"url":"https://passport.bilibili.com/x/passport-tv-login/h5/qrcode/auth?auth_code=xxxx","auth_code":"xxxx"}}
$response = ApiQrcode::authCode();
//
if ($response['code']) {
failExit('获取AuthCode错误', ['msg' => $response['message']]);
}
Log::info("获取到AuthCode: {$response['data']['auth_code']}");
return $response['data'];
}
/**
* @use 验证AuthCode
* @param string $auth_code
* @return bool
*/
protected function validateQrAuthCode(string $auth_code): bool
{
// {"code":0,"message":"0","ttl":1,"data":{"mid":123,"access_token":"xxx","refresh_token":"xxx","expires_in":2592000}}
$response = ApiQrcode::poll($auth_code);
echo json_encode($response, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
switch ($response['code']) {
case 0:
// 登录成功
Log::notice("扫码成功 {$response['message']}");
//
// $this->updateTvLoginInfo($response);
$this->updateLoginInfo($response);
return true;
case -3:
// API校验密匙错误
failExit("扫码失败 {$response['message']}");
case -400:
// 请求错误
failExit("扫码失败 {$response['message']}");
case 86038:
// 二维码已失效
failExit("扫码失败 {$response['message']}");
case 86039:
// 二维码尚未确认
Log::info("等待扫码 {$response['message']}");
return false;
default:
failExit("扫码失败 {$response['message']}");
}
}
/**
* @use 登录后处理
* @param string $mode
* @param int $code
* @param array $data
* @return void
*/
protected function loginAfter(string $mode, int $code, array $data): void
{
switch ($code) {
case 0:
// data->data->status number
if (array_key_exists('status', $data['data'])) {
// 二次判断
switch ($data['data']['status']) {
case 0:
// 正常登录
$this->loginSuccess($mode, $data);
break;
case 2:
// 异常高危
$this->loginFail($mode, $data['data']['message']);
case 3:
// 需要验证手机号
$this->loginFail($mode, "需要验证手机号: {$data['data']['url']}");
default:
// 未知错误
$this->loginFail($mode, '未知错误: ' . json_encode($data));
}
} else {
// 正常登录
$this->loginSuccess($mode, $data);
}
break;
case -105:
// 需要验证码
$this->loginFail($mode, '此次登录需要验证码或' . $data['message']);
case -629:
// 密码错误
$this->loginFail($mode, $data['message']);
case -2100:
// 验证手机号
$this->loginFail($mode, '账号启用了设备锁或异地登录需验证手机号');
default:
// 未知错误
$this->loginFail($mode, '未知错误: ' . $data['message']);
}
}
/**
* @use 登录成功处理
* @param string $mode
* @param array $data
* @return void
*/
protected function loginSuccess(string $mode, array $data): void
{
Log::info("$mode 登录成功");
$this->updateLoginInfo($data);
Log::info('生成信息配置完毕');
}
/**
* @use 登录失败处理
* @param string $mode
* @param string $data
* @return void
*/
#[NoReturn]
protected function loginFail(string $mode, string $data): void
{
failExit("$mode 登录失败", ['msg' => $data]);
}
/**
* @use 检查登录
*/
protected function checkLogin(): void
{
$username = getConf('login_account.username');
$password = getConf('login_account.password');
if (empty($username) || empty($password)) {
failExit('空白的帐号和口令');
}
$this->username = $username;
$this->password = $this->publicKeyEnc($password);
}
/**
* @use 公钥加密
* @param string $plaintext
* @return string
*/
protected function publicKeyEnc(string $plaintext): string
{
Log::info('正在载入公钥');
//
$response = ApiOauth2::getKey();
//
if (isset($response['code']) && $response['code']) {
failExit('公钥载入失败', ['msg' => $response['message']]);
} else {
Log::info('公钥载入完毕');
}
//
$public_key = $response['data']['key'];
$hash = $response['data']['hash'];
openssl_public_encrypt($hash . $plaintext, $crypt, $public_key);
return base64_encode($crypt);
}
/**
* @use 发送短信验证码
* @param string $phone
* @param string $cid
* @return array
*/
protected function sendSms(string $phone, string $cid): array
{
// {"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"}}
$raw = ApiQrcode::sendSms($phone, $cid);
$response = json_decode($raw, true);
//
if ($response['code'] == 0 && isset($response['data']['captcha_key']) && $response['data']['recaptcha_url'] == '') {
Log::info("短信验证码发送成功 {$response['data']['captcha_key']}");
$payload['captcha_key'] = $response['data']['captcha_key'];
return $payload;
}
failExit("短信验证码发送失败 $raw");
}
/**
* @use 输入短信验证码
* @param string $msg
* @param int $max_char
* @return string
*/
protected function cliInput(string $msg, int $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 获取验证码
* @return array
*/
protected function getCaptcha(): array
{
$response = ApiCaptcha::combine();
Log::info('正在获取验证码 ' . $response['code']);
if ($response['code'] == 0 && isset($response['data']['result'])) {
return [
'gt' => $response['data']['result']['gt'],
'challenge' => $response['data']['result']['challenge'],
'key' => $response['data']['result']['key'],
];
}
return [
'gt' => '',
'challenge' => '',
'key' => ''
];
}
/**
* @use 验证码模式
* @param string $mode
* @return void
*/
protected function captchaLogin(string $mode = '验证码模式'): void
{
// $captcha_ori = $this->getCaptcha();
// $captcha = $this->ocrCaptcha($captcha_ori);
// $this->accountLogin($captcha['validate'], $captcha['challenge'], $mode);
}
/**
* @use 转换Cookie
* @param string $token
* @return string
*/
protected function token2Cookie(string $token): string
{
$response = ApiOauth2::token2Cookie($token);
$headers = $response['Set-Cookie'];
$cookies = [];
foreach ($headers as $header) {
preg_match_all('/^(.*);/iU', $header, $cookie);
$cookies[] = $cookie[0][0];
}
return implode("", array_reverse($cookies));
}
}