Skip to content

Commit

Permalink
修复 issue #3 - 验证码问题
Browse files Browse the repository at this point in the history
增加 cli 下输入验证码示例代码
  • Loading branch information
consatan committed Apr 21, 2017
1 parent afddd4c commit 91d681d
Show file tree
Hide file tree
Showing 5 changed files with 192 additions and 44 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ echo $url . PHP_EOL;

构造函数可传递 `\Psr\Cache\CacheItemPoolInterface``\GuzzleHttp\ClientInterface`,默认情况下使用文件缓存 cookie 信息,存储在项目根目录的 cache/weibo 文件夹下,缓存的 `key` 使用 `md5` 后的微博用户名,可根据需求将缓存保存到其他适配器中,具体参见 `\Symfony\Cache\Adapter`

> 关于验证码问题([issue #3](https://github.com/consatan/weibo_image_uploader/issues/3)),可查看 [example/cli.php](https://github.com/consatan/weibo_image_uploader/tree/master/example/cli.php) 示例代码
`Client::upload` 方法的第四个参数允许传递 `Guzzle request` 的参数数组,具体见 [Request Options](http://docs.guzzlephp.org/en/latest/request-options.html),通过该参数可实现切换代理等操作,如下例:

```php
Expand Down Expand Up @@ -73,6 +75,7 @@ $url4 = $weibo->upload(\GuzzleHttp\Psr7\stream_for(file_get_contents('./example.
'proxy' => 'http://192.168.1.250:9080'
]);
```

##### 水印选项
```php
// 开启水印
Expand Down Expand Up @@ -132,7 +135,7 @@ $url = $weibo->upload('./example.jpg', '微博帐号', '密码', [
- [ ] 单元测试
- [x] 获取其他规格的图片 URL(如,small, thumbnail...)
- [x] 添加水印选项
- [ ] 实现验证码输入(用户输入)
- [x] 实现验证码输入(用户输入)

#### 参考

Expand Down
23 changes: 23 additions & 0 deletions example/cli.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php
/**
* 在 CLI 下当出现验证码时,要求用户输入验证码
*/

require '../vendor/autoload.php';

$client = new Consatan\Weibo\ImageUploader\Client();
$username = 'weibo_username';
$password = 'password';

while (true) {
try {
echo $client->upload('./example.png', $username, $password);
break;
} catch (Consatan\Weibo\ImageUploader\Exception\RequirePinException $e) {
echo '验证码图片位置:' . $e->getMessage() . PHP_EOL . '输入验证码以继续:';
if (!$client->login($username, $password, stream_get_line(STDIN, 1024, PHP_EOL))) {
echo '登入失败';
break;
}
}
}
Binary file added example/example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
192 changes: 149 additions & 43 deletions src/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use Consatan\Weibo\ImageUploader\Exception\RequestException;
use Consatan\Weibo\ImageUploader\Exception\BadResponseException;
use Consatan\Weibo\ImageUploader\Exception\RuntimeException;
use Consatan\Weibo\ImageUploader\Exception\RequirePinException;
use Consatan\Weibo\ImageUploader\Exception\ImageUploaderException;

/**
Expand Down Expand Up @@ -600,64 +601,106 @@ public function upload(
*
* @param string $username 微博帐号,微博帐号的 md5 值将作为缓存 key
* @param string $password 微博密码
* @param bool $cache (true) 是否使用缓存的cookie进行登入,如果缓存不存在则创建
* @param bool|string $cache (true) 是否使用缓存的cookie进行登入,如果缓存不存在则创建;
* 当传入的是字符串时,该参数为验证码,不使用缓存登入。
*
* @return bool 登入成功与否
*
* @throws \Consatan\Weibo\ImageUploader\Exception\RequirePinException 需要输入验证码时,
* Exception message 为验证码图片的本地路径
* @throws \Consatan\Weibo\ImageUploader\Exception\IOException 缓存持久化失败时
*/
public function login(string $username, string $password, bool $cache = true): bool
public function login(string $username, string $password, $cache = true): bool
{
$this->password = $password;
$this->username = trim($username);
$cacheKey = md5($this->username);

if (is_string($cache)) {
$pin = $cache;
$cache = false;
} else {
$pin = '';
$cache = (bool)$cache;
}

// 如果使用缓存登入且缓存里有对应用户名的缓存cookie的话,则不需要登入操作
if ($cache && ($cookie = $this->cache->getItem(md5($this->username))->get()) instanceof CookieJarInterface) {
if ($cache && ($cookie = $this->cache->getItem($cacheKey)->get()) instanceof CookieJarInterface) {
$this->cookie = $cookie;
$this->setNickname();
return true;
}

return $this->request(
$this->ssoLogin(),
function (string $content) {
if (1 === preg_match('/"\s*result\s*["\']\s*:\s*true\s*/i', $content)) {
// 登入成功,删除旧缓存 cookie
$this->cache->deleteItem(md5($this->username));
// 新建 或 获取 CacheItemInterface 实例
$cache = $this->cache->getItem(md5($this->username));
// 设置 cookie 信息
$cache->set($this->cookie);
// 缓存持久化
if (!$this->cache->save($cache)) {
throw new IOException('持久化缓存失败');
}
$this->setNickname();
return true;
}
return $this->request($this->ssoLogin($pin), function (string $content) use ($cacheKey) {
if (1 === preg_match('/"\s*result\s*["\']\s*:\s*true\s*/i', $content)) {
$this->persistenceCache($cacheKey, $this->cookie);
$this->setNickname();
return true;
}

return false;
},
'GET',
[
// 该请求会返回 302 重定向,所以开启 allow_redirects
'allow_redirects' => true,
'headers' => [
'Referer' => 'http://login.sina.com.cn/sso/login.php?client=ssologin.js(v1.4.18)',
],
]
);
return false;
}, [
// 该请求会返回 302 重定向,所以开启 allow_redirects
'allow_redirects' => true,
'headers' => [
'Referer' => 'http://login.sina.com.cn/sso/login.php?client=ssologin.js(v1.4.18)',
],
]);
}

/**
* 获取 SSO 登入信息
*
* @param string $pin ('') 验证码
*
* @return string 返回登入结果的重定向的 URL
*
* @throws \Consatan\Weibo\ImageUploader\Exception\BadResponseException 响应非预期或要求输入验证码时
* @throws \Consatan\Weibo\ImageUploader\Exception\RequirePinException 需要输入验证码时,
* Exception message 为验证码图片的本地路径
* @throws \Consatan\Weibo\ImageUploader\Exception\BadResponseException 响应非预期或未输入验证码时
*/
protected function ssoLogin(): string
protected function ssoLogin(string $pin = ''): string
{
$data = $this->preLogin();
$params = [];
$pin = trim($pin);
$cacheKey = md5($this->username) . '_preLogin';
if ($this->cache->hasItem($cacheKey)) {
// 从缓存中获取上次 preLogin 的数据
$data = $this->cache->getItem($cacheKey)->get();
if (is_array($data) && isset(
$data['pcid'],
$data['servertime'],
$data['nonce'],
$data['pubkey'],
$data['rsakv'],
$data['pinImgPath']
)) {
if ($pin !== '') {
$params['pcid'] = $data['pcid'];
$params['door'] = $pin;
} else {
if (file_exists($data['pinImgPath'])) {
// 如果已经缓存过验证码图片,就不需要重复获取
throw new RequirePinException($data['pinImgPath']);
}
}
// 删除本地验证码图片
@unlink($data['pinImgPath']);
}
// 删除 prelogin 缓存,如果提供了验证码,则验证码都是一次性的,
// 不管验证成功与否,都没有必要继续缓存;如果没提供验证码,则会
// 抛出 RequirePinException 异常,也就不会执行删除缓存的代码。
$this->cache->deleteItem($cacheKey);
}

if (empty($params)) {
$data = $this->preLogin();
if (isset($data['showpin']) && (int)$data['showpin']) {
// 要求输入验证码
throw new RequirePinException($this->getPin($data));
}
}

$msg = "{$data['servertime']}\t{$data['nonce']}\n{$this->password}";

return $this->request(
Expand All @@ -673,10 +716,9 @@ function (string $content) {
throw new BadResponseException("登入响应非预期结果: $content");
}
},
'POST',
[
'headers' => ['Referer' => 'http://weibo.com/login.php'],
'form_params' => [
'form_params' => $params + [
'entry' => 'weibo',
'gateway' => '1',
'from' => '',
Expand All @@ -694,12 +736,15 @@ function (string $content) {
'sp' => bin2hex(rsa_encrypt($msg, '010001', $data['pubkey'])),
'sr' => '1440*900',
'encoding' => 'UTF-8',
'prelt' => '287',
// 该参数为加载 preLogin 页面到提交登入表单的间隔时间
// 此处使用 float 是为了兼容 32 位系统
'prelt' => (int)round((microtime(true) - $data['preloginTime']) * 1000),
'url' => 'http://weibo.com/ajaxlogin.php?'
. 'framelogin=1&callback=parent.sinaSSOController.feedBackUrlCallBack',
'returntype' => 'META'
],
]
],
'POST'
);
}

Expand All @@ -712,34 +757,72 @@ function (string $content) {
*/
protected function preLogin(): array
{
$ts = microtime(true);
return $this->request(
'http://login.sina.com.cn/sso/prelogin.php?entry=weibo&callback=sinaSSOController.preloginCallBack&su='
. urlencode(base64_encode(urlencode($this->username)))
. '&rsakt=mod&checkpin=1&client=ssologin.js(v1.4.18)&_='
. substr(strval(microtime(true) * 1000), 0, 13),
function (string $content) {
. substr(strval($ts * 1000), 0, 13),
function (string $content) use ($ts) {
if (1 === preg_match('/^sinaSSOController.preloginCallBack\s*\((.*)\)\s*$/', $content, $match)) {
$json = json_decode($match[1], true);
if (isset($json['nonce'], $json['rsakv'], $json['servertime'], $json['pubkey'])) {
// 记录访问时间戳,登入时 prelt 参数需要用到
$json['preloginTime'] = $ts;
return $json;
}
throw new BadResponseException("PreLogin 响应非预期结果: $match[1]");
} else {
throw new BadResponseException("PreLogin 响应非预期结果: $content");
}
},
'GET',
['headers' => ['Referer' => 'http://weibo.com/login.php']]
);
}

/**
* 获取验证码图片
*
* @param string $pcid preLogin 阶段获取到的 pcid
*
* @return string 验证码图片的本地路径
*
* @throws \Consatan\Weibo\ImageUploader\Exception\IOException 创建或保存验证码图片失败时,
* 或持久化缓存失败时
*/
protected function getPin(array $data): string
{
$url = 'http://login.sina.com.cn/cgi/pin.php?r=' . rand(100000000, 99999999) . '&s=0&p=' . $data['pcid'];
$this->request($url, function ($content) use (&$data) {
if (false === ($path = tempnam(sys_get_temp_dir(), 'WEIBO'))) {
throw new IOException('创建验证码图片文件失败');
}

if (false === file_put_contents($path, $content)) {
throw new IOException('保存验证码图片失败');
}
$data['pinImgPath'] = $path;
}, ['headers' => [
'Accept' => 'image/png, image/svg+xml, image/*;q=0.8, */*;q=0.5',
'Referer' => 'http://www.weibo.com/login.php',
]]);

$cacheKey = md5($this->username);
// 持久化 preLogin 获取的数据
$this->persistenceCache($cacheKey . '_preLogin', $data);
// 持久化 cookie 保存当前状态
$this->persistenceCache($cacheKey, $this->cookie);

return $data['pinImgPath'];
}

/**
* 封装的 HTTP 请求方法
*
* @param string $url 请求 URL
* @param callable $fn 回调函数
* @param string $method ('GET') 请求方法
* @param array $option ([]) 请求参数,具体见 Guzzle request 的请求参数说明
* @param string $method ('GET') 请求方法
*
* @return mixed 返回 `$fn` 回调函数的调用结果
*
Expand All @@ -748,7 +831,7 @@ function (string $content) {
*
* @see http://docs.guzzlephp.org/en/latest/request-options.html
*/
protected function request(string $url, callable $fn, string $method = 'GET', array $option = [])
protected function request(string $url, callable $fn, array $option = [], string $method = 'GET')
{
$this->applyOption($option);

Expand Down Expand Up @@ -790,4 +873,27 @@ private function applyOption(array &$option)
$option['cookies'] = $this->cookie;
}
}

/**
* 持久化缓存
*
* @param string $key 缓存key
* @param mixed $value 缓存数据
*
* @return void
*
* @throws Consatan\Weibo\ImageUploader\Exception\IOException 持久化失败时
*/
private function persistenceCache(string $key, $value)
{
$this->cache->deleteItem($key);
// 新建 或 获取 CacheItemInterface 实例
$cache = $this->cache->getItem($key);
// 设置 cookie 信息
$cache->set($value);
// 缓存持久化
if (!$this->cache->save($cache)) {
throw new IOException('持久化缓存失败');
}
}
}
16 changes: 16 additions & 0 deletions src/Exception/RequirePinException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php declare(strict_types=1);

/*
* This file is part of the Consatan\Weibo\ImageUploader package.
*
* (c) Chopin Ngo <consatan@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Consatan\Weibo\ImageUploader\Exception;

class RequirePinException extends RuntimeException
{
}

0 comments on commit 91d681d

Please sign in to comment.