标签归档:授权

Oauth2授权类 之 1688.com

<?php
/**
 * vfeelit@qq.com
 * 2015-11-30
 * 
 *  Api授权
 */
 
namespace Alibaba;

class Auth
{
    private $appKey;
    private $appSecret;
    private $redirectUrl;
    
    private $getTokenUrl = "https://gw.open.1688.com/openapi/http/1/system.oauth2/getToken";
    
    public function __construct($appKey, $appSecret, $redirectUrl='')
    {
        $this->appKey = $appKey;
        $this->appSecret = $appSecret;
        $this->redirectUrl = $redirectUrl;
    }

    // 一次性code换取access_token 和  refresh_token
    //array(
    //    "aliId" => "8888888888",      编号
    //    "resource_owner" => "xx",     登录名称
    //    "memberId" => "xx",           会员编号
    //    "expires_in" => "36000",      access_token有效时间,10小时
    //    "refresh_token" => "xx",
    //    "access_token" => "xx",
    //    "refresh_token_timeout" => "20121222222222+0800"
    //)
    public function getToken($code, $redirectUrl)
    {
        $params = array(
            'grant_type' => 'authorization_code',
            'need_refresh_token' => 'true',
            'client_id' => $this->appKey,
            'client_secret' => $this->appSecret,
            'redirect_uri' => empty($redirectUrl)?$this->redirectUrl:trim($redirectUrl),
            'code' => $code,
            '_aop_datePattern' => 'yyyy-MM-dd HH:mm:ss',
            '_aop_timeZone' => 'GMT+0800'
        );
        return $this->_token($params);
    }

    // refresh_token换取access_token
    public function refreshToken($refreshToken)
    {
        $params = array(
            'grant_type' => 'refresh_token',
            'client_id' => $this->appKey,
            'client_secret' => $this->appSecret,
            'refresh_token' => $refreshToken,
            //对授权无效
            '_aop_datePattern' => 'yyyy-MM-dd HH:mm:ss',
            '_aop_timeZone' => 'GMT+0800'
        );      
        return $this->_token($params);
    }
    
    // 获取token
    protected function _token($params)
    {
        $baseUrl = $this->getTokenUrl.'/'.$this->appKey;
        
        $result = $this->doRequest($baseUrl,http_build_query($params),true);
    
        if((int)$result['success'] > 0) {
            $data = json_decode($result['data'],true);
            if(!isset($data['access_token'])) {
                $result['success'] = 0;
                $result['err'] = '返回数据有误(没有access_token)';
                unset($result['data']);
            } else {
                $result['data'] = $data;
            }
        }
    
        return $result;
    }
    
    // 获取授权URL
    public function getAuthUrl($redirectUrl='') {
        $baseUrl = "http://auth.1688.com/auth/authorize.htm";
        
        $pramas = array (
            'client_id' => $this->appKey,
            'site' => 'china'
        );
        
        ///
        if(!empty($redirectUrl)) {
            $pramas['redirect_uri'] = $redirectUrl;
        } else {
            $pramas['redirect_uri'] = $this->redirectUrl;
        }
        if(empty($pramas['redirect_uri'])) {
            $pramas['redirect_uri'] = "http://localhost";
        }
        
        ///
        ksort ( $pramas );
        $signStr = '';
        foreach ( $pramas as $key => $val ) {
            $signStr .= $key . $val;
        }
        $sign = strtoupper ( bin2hex ( hash_hmac ( "sha1", $signStr, $this->appSecret, true ) ) );
        
        ///
        $pramas ['_aop_signature'] = $sign;
        
        return $baseUrl . '?' . http_build_query ( $pramas );
    }
    
    // 发起请求
    public function doRequest($url='', $data='', $post=false)
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, trim($url));
        curl_setopt($ch, CURLOPT_HEADER, false);
        curl_setopt($ch, CURLOPT_TIMEOUT, 60);
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 60);
        if($post === false){
            curl_setopt($ch, CURLOPT_POST, false);
        }else{
            curl_setopt($ch, CURLOPT_POST, true);
            if(!empty($data)) {
                curl_setopt($ch, CURLOPT_POSTFIELDS,$data);
            }
        }
        if ((int)preg_match('/^HTTPS/i', $url) > 0) {
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
            curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
        }
    
        curl_setopt($ch,CURLOPT_RETURNTRANSFER,true);
        curl_setopt($ch,CURLOPT_FOLLOWLOCATION,true);
        curl_setopt($ch,CURLOPT_MAXREDIRS,10);
    
        curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:21.0) Gecko/20100101 Firefox/21.0");
        $result = curl_exec($ch);
        $errn = curl_errno($ch);
        curl_close($ch);
    
        $return = ['success' => 0,'err' => '发生错误'];
        if((int)$errn > 0) {
            $return['err'] = curl_strerror((int)$errn);
        }else{
            $return['success'] = 1;
            unset($return['err']);
            $return['data'] = $result;
        }
        return $return;
    }
}

Wish的OAuth 2认证流程

Wish还提供了一个sandbox环境,不像那些个Ali系统,从不提供这种东西,想想都恶心(玛尼,就知道坑钱去了)。

http://sandbox.merchant.wish.com,自己注册个sandbox账户,随意折腾。

其实,OAuth 2认证流程都差不多,首先跳转到认证登录页,要求输入用户名密码进行授权(这个过程可能有点差别),然后重定向到预定的redirect_uri并附上一个一次性的code(这个就是认证码了),然后用这个code去获取access_token和refresh_token,这个时候要把它们保存下来,再然后就可以使用access_token来访问了。如果access_token过期了,可以使用refresh_token来换取新的access_token。

在进行第一步之前,要先获取API相关信息(账户-设置):
wish_api
理论上,这里就是对应一个App,那么不同的店铺应该都可以绑定到这个App。不过,如果别人都可以绑定到你的App,那么你就可以提供服务了,很明显,它不应该不允许你这样干,这个跟恶心的Ali系统一样,如果要这样干,需要得到特别授权,所以,这里的App(Api)跟你的账户一一对应,你的账户只能授权给你自己创建的这个App。(注意以上的信息,后面都要用到)

在基本信息中,还有一个叫商户 ID的,它唯一标识你的账户。这个后面会涉及到。

弄好之后,就可以开始了。详细内容参考:https://merchant.wish.com/documentation/oauth。这里只做一些备注。在第一步时,直接发起https://merchant.wish.com/oauth/authorize?client_id={client_id},参数除了client_id,你无法再提供其它参数了,有些实现方案还可以在这传递redirect_uri和其它参数,Wish不允许,授权码(code)直接附加到你在账户设置里面设置的那个redirect_uri。

在第二步中,你需要使用第一步中获取到的一次性code换取令牌,你需要通过POST方法发送如下参数:

//https://merchant.wish.com/api/v2/oauth/access_token 
client_id	Your app's client ID
client_secret	Your app's client secret
code	        The authorization code you received
grant_type	The string 'authorization_code'
redirect_uri	Your app's redirect uri that you specified when you created the app

这里需要注意的是,redirect_uri必须填写在你账户里设置的那个,否则无法通过验证。在Ali系统中,这个是不验证的,可以随意,玛尼,被它坑死。

这个通过之后会返回一个JSON信息,包含了Token,这个时候要把Token保存起来。不过这里,如果参考文档,你发现,它返回的数据没带有商户Id,那么怎么跟商户关联起来?你要是可以在redirect时携带参数还好,不过Wish你允许啊,所以,千万别被它坑了,真实返回的JSON是带有这个商户ID,这样就可以对应起来。

从真实返回的信息可以知道,它的Access Token有效是30天(玛尼,是不是有点长),另外,refresh_token视乎没有过期概念。在Access Token过期前换一个新的,不知道旧的会不会失效。同样,如果重新对App进行授权,这样就产生新的refresh_token,那么旧的会不会就失效了呢? 答案应该是的。(Ali系貌似这个问题比较隐晦)所以,如果要换一换Token,再来一次授权吧。

最后说说官方提供的PHP SDK吧。地址:https://github.com/ContextLogic/Wish-Merchant-API,可以通过composer引用,不过要注意的是,它至今还是dev版本,所以要这样干:

{
  "minimum-stability": "dev",
  "require":{
      "wish/php-sdk":"*"
  }
}

好的不教,这个简直要害死你。这样其它的包,也下载dev分支的给你。对我来说,我直接下载下来当本地包使用,因为这个包有点问题,需要改。故而:

    "autoload": {
        "psr-4": {
            "Wish\\": "local/Wish/"
        }
    },

然后更新下就完事了。

我第一个需要改这个包的地方就是,它使用了curl去发起请求,而Wish的Api都是https的,玛尼,Windows下curl无法验证https的证书(需要设置,指定CA证书的位置),所以我需要把它给位不验证:

//WishRequest.php 65行
if ((int)preg_match('/^HTTPS/i', $url) > 0) {
    $options[CURLOPT_SSL_VERIFYPEER] = false;
    $options[CURLOPT_SSL_VERIFYHOST] = false;
}

另外,这个鬼佬写得代码是用两个空格作为tab的,简直欠抽的节奏。还好格式化工具比较强大。

然后,打开WishSession.php,兼容问题:

private static $api_key;
private static $session_type;
private static $merchant_id;

#改为

private $api_key;
private $session_type;
private $merchant_id;

在实例方法中,出现和静态变量重名报错。以下是一段实例:

// Laravel 的Model
<?php
namespace App;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Wish\WishAuth;

class WishAccount extends Model
{

    protected $table = 'wish_account';

    protected $guarded = [];

    public function getAccessToken($type='prod')
    {
        $default = ['success'=>0,'message'=>'获取Access Token失败','access_token'=>''];
        // Access Token不为空,判断是否过气
        if (! empty($this->access_token)) {
            $timeOut = trim($this->access_token_timeout_at);
            if(!empty($timeOut) && (strtotime($timeOut) > time())) {
                return ['success'=>1,'message'=>'','access_token'=>$this->access_token];
            }
        } 
        
        // 更换新Token
        if(! empty($this->refresh_token)) {
            $client_id = trim($this->client_id);
            $client_secret = trim($this->secret_id);
            
            if(empty($client_id) || empty($client_secret)) {
                $default['message'] = "Access Token过期,使用Refresh Token换取Access Token失败(Client_id 或 Client_secret为空,无法换取Token)";
                return $default;
            }
            
            // 
            try {
                $auth = new WishAuth($client_id,$client_secret,$type);
                $response = $auth->refreshToken($this->refresh_token);
                if($response->getStatusCode() > 0) {
                    $default['message'] = "Access Token过期,使用Refresh Token换取Access Token失败(状态码:".$response->getStatusCode()." Msg:".$response->getMessage();
                } else {
                    // 返回的Refresh Token,跟之前一样
                    $this->freshToken($response->getData());
                    return ['success'=>1,'message'=>'','access_token'=>$response->getData()->access_token];
                }
            } catch (Exception $e) {
                $default['message'] = "Access Token过期,使用Refresh Token换取Access Token失败(状态码:4000 Msg:Unauthorized access)";
                return $default;
            }
        }
        
        return $default;
    }
    
    // 一次性code换取Token access_token和refresh_token
    public function setToken($code,$type='prod') {

        $client_id = trim($this->client_id);
        $client_secret = trim($this->secret_id);
        $redirect_uri = trim($this->redirect_uri);
        
        if(empty($client_id) || empty($client_secret) || empty($redirect_uri) || empty($code)) {
            return "信息不完整,无法完成授权。";
        }
        
        try {
            $auth = new WishAuth($client_id,$client_secret,$type);
            $response = $auth->getToken($code,$redirect_uri);
            
            $this->freshToken($response->getData());
        } catch (Exception $e) {
            return "4000 或 1016 异常。";
        }
    }
    
    // 保存Token信息
    protected function freshToken($data) {
        if (is_object($data)) {
            $this->refresh_token = $data->refresh_token;
            $this->refresh_token_timeout_at = date("Y-m-d H:i:s", time() + 1 * 12 * 30 * 24 * 3600);
            $this->access_token = $data->access_token;
            $this->access_token_timeout_at = date("Y-m-d H:i:s", time() + $data->expires_in - 2 * 24 * 3600);
            
            $this->save();
        }
    }
}

抓Listing:

use Wish\WishClient;

$access_token = 'an_example_access_token';

$client = new WishClient($access_token,'prod');
$products = $client->getAllProducts();
echo "You have ".count($products)." products!\n";

注意,这段代码中间是可能抛出异常的,比如Access Token过期等,需要注意捕捉。

eBay用户授权流程

ebay-developer
往下翻,点击Customize the eBay User Consent Form,找到Manage Your RuNames部分,然后点击Generate Runame,这样就会产生一个所谓的RuName,长成这个样子”vfeelit-vfeelit1d-65eb–tboemfhvb”,可以点击多次产生多个RuName,不过看起来没有什么必要,那么RuName是什么毛呢,先看看针对它的配置吧,在对应字符串右边点击Show Details,将展示如下表单:
eBay RuName
Display Title和Display Description是展示给用户看的标题和描述,这个信息可以在Application Level Settings中设置Show Application Details为enabled或disabled来设置,还可以上传Logo,这些信息是在用户点击同意授权时展示的出来给用户看的。

Token Return Method设置如何获取Token,Authorization Type为授权类型,Accept Redirect URL为成功授权后跳转到的地址,Reject Redirect URL授权被拒绝时跳转到的地址。实际上这里的值只要默认就可以了,关于Accept Redirect URL和Reject Redirect URL如果要设置,必须是HTTPS的链接地址,实际设置这两个变量毫无意义,因为它不会回传任何信息到这两个URL。所以,RuName实际上是一个Application的标识符(别名),它设置了相关的认证类型,Token获取方式等等,比如当用户授权成功后,就需要用这里设置的Token Return Method来获取Token。

对于一个应用,只要设置一个RuName即可。多个RuName也支持。这个步骤完成后应用就支持多用户授权了。

授权过程,步骤如下:
1 调用GetSessionID获取一个SessionID
这个API调用详细参考:http://developer.ebay.com/Devzone/XML/docs/Reference/ebay/GetSessionID.html,这里面如果是发送XML,只要发送RuName即可:

<?xml version="1.0" encoding="utf-8"?>
<GetSessionIDRequest xmlns="urn:ebay:apis:eBLBaseComponents">
  <!-- Call-specific Input Fields -->
  <RuName> string </RuName>
  <!-- Standard Input Fields -->
  <ErrorLanguage> string </ErrorLanguage>
  <MessageID> string </MessageID>
  <Version> string </Version>
  <WarningLevel> WarningLevelCodeType </WarningLevel>
</GetSessionIDRequest>

从返回提取SessionID即可。

2 用获取到的SessionID构建URL
URL格式:https://signin.ebay.com/ws/eBayISAPI.dll?SignIn&RUName=RUName&SessID=SessionID,SessionID需要经过URL-encoded,然后定位到这个URL,这样将打开用户登录表单,用户登录成功后将跳到一个是否同意授权的页面:
eBay 用户登录
登录后跳到:
授权应用
这个的Grant application access后的名称就是设置RuName是指定的Display Title,这就说明RuName可以看做是Application。最下面展示的是应用的信息,这个可以在开发者账户中进行设置。点击I agree跳转到如下页:
ebay_auth_success
这个也是可以设置的。前面已经论述。这里叫你去关闭这个页。TMD,这就完了,然后接下来要发起获取Token的操作,这个过程明显让我们感觉整个流程被中断了。我们观察一下这个返回的URL:https://signin.ebay.com/ws/eBayISAPI.dll?ThirdPartyAuthSucessFailure&isAuthSuccessful=true&ebaytkn=&tknexp=1970-01-01+00%3A00%3A00&username=testuser_vfeelit,问号之前的是可以设置的(需要HTTPS),后面的数据是固定的,isAuthSuccessful参数表明是否成功,username指出了eBay用户名,应该只要设置一下应用的返回地址,根据这些参数,也是可以自动获取Token的(流程不中断,因为获取到了username)

3 获取Token
以上两个步骤完成后,只是说明用户对应用进行了授权,但是授权码应用程序还没有获取到,这个时候调用FetchToken(传递eBay ID),就能返回针对这个账户的Token。

4 保存这个Token和有效时间
Token保存起来后就可以使用这个Token来访问API操作对应账户数据了。

这个过程看起来并没有比oAuth 2先进多少,这个流程过程中的中断让人产生困惑,虽然可以设置Accept Redirect URL,但是它要求是HTTPS的链接。

速卖通API接入之授权二

之前曾写过“速卖通API接入之授权”,这次是写点有关授权的官方文档中没有的内容。

一般,如果你自己想开发一个程序,这个程序需要用到速卖通的API,那么你首先需要开一个速卖通账户,用这个账户去申请接入的相关信息。

如果你不想开发,那么你可以用第三方的程序,你不需要自己去申请API接入的相关信息,但是你要授权这个第三方程序它通过API来抓取你的店铺的数据。

很简单,一个是要你自己去申请接入的Key和秘钥,然后调用速卖通的API抓取数据;一个是不需要自己去搞这些,委托第三方来通过API来抓取数据。实际上,这里描述的这两种方式,对应于“App授权协议”的自用 和 商用,简单来说就是,如果是自用的,那么你开发的程序就仅仅用来抓取你的店铺的数据,如果是商用的,你的程序可以被其它账户委托,用来抓取别人店铺的数据。

那么你申请了一个自用账户,开发了一个程序,完了之后去接受别人的委托,然后去抓取别人的信息,是否可以呢? 至少从理论上来说,应该是可以的。关键就在于速卖通到底有没有严格执行自用 与 商用的逻辑。比如,如果你是自用的,就限制不可以接受委托,怎么进行呢?

我们知道,为了获取授权,通常有如下:
速卖通授权
在验证用户和密码的同时验证过来的Appkey是否跟这个用户一致,就可以限制自用。

不过,到目前为止,这个逻辑并没有实施。那么也就是说,自用的程序,搞完之后,也可以接受别人的委托抓别人的数据。这样的好处在于,如果你有多个店铺,那么只要拿一个去申请自用的Appkey和秘钥,其它的店铺就不需要了,因为它们可以委托这个已经存在的App去抓取数据。还有一个原因是,如果一个新店铺,如果还没有交易,那么是无法申请自用App的(也就是无法使用API),但是通过委托的方式就可以,所以对于新店铺,就可以在没有交易的情况下,通过自己的程序去铺产品。

速卖通目前只能是自用就自用,说起来是让有资质的第三方公司来做这种专门的开发,实际是卡主这里,和第三方应用进行应用的利润分成。

永久链接:http://blog.ifeeline.com/1071.html
原创文章,装载务必保留出处。