第三方登陆的整体思路是获取第三方中的openid,然后与用户关联(存到数据库),进行登陆。
现在需求是:两个(或多个)一级域名,如maxiye.cn
和yexima.com
,同时每个域名下有多个二级域名分布,如:app.maxiye.cn,new.maxiye.cn,old.maxiye.cn,app.yexima.com,new.yexima.com,old.yexima.com
...等,但是这些域名下使用了同一份代码(对,就是马甲),共享数据库和session。同时旗下每个域名均可能包含pc,ios,Android端,如何全部接入第三方登陆?
qq,微信,微博接入要点:
1.申请入口:QQ是QQ互联,微信是微信开放平台,微博是微博开放平台;
2.回调域设置:QQ可以设置一级域名且可以有多个,必须;微信只能设置到二级域名且只能一个,格式为:http://
开头,;
结束,如http://maxiye.cn;http://yexima.com;
app.maxiye.cn
;微博也可以设置一级域名且是一个:maxiye.cn
;
注:由于QQ互联开启了回调地址强校验,现在必须写完整的回调路径,多个以“;”分隔,如https://account.maxiye.cn/connect/callback/qq;https://account.yexima.com/connect/qc
。(2018.3.26)
3.unionid获取:多个app(同一开发者账号下
)共享数据库,故需要使用unionid确认身份。QQ获取unionid需要额外申请权限,具体参考相同开发者账号下的不同appid应用如何打通;微信已有unionid获取接口;微博中的uid即unionid;
4.本地也可以调试:比如要本地调试app.maxiye.cn的接入,只需要配置本地域名为local.maxiye.cn
(local可以改,但一级域名不能动,适用于QQ,微博;微信则必须是回调设置好的二级域名,可以先把回调地址设置为local.maxiye.cn,本地调试好了,再改),就可以正常接收第三方的回调了;
坑:
1.QQ接口返回的结果好多是jsonp格式,需要手动剥离callback()
;
2.微博获取access_token竟然必须使用post
,惊了;
3.微信不支持回调地址填写一级域名,所以需要使用统一域名(代理)作为回调地址,然后内部再重定向到发申请的域名;
下边是一个写好的工具类:
<?php
/* PHP SDK 第三方登录授权验证* @version 1.0.0* @author Maxiye* @copyright*/namespace app\helpers;use yii\helpers\Url;
use Yii;class OauthLogin
{public $server = '';//接入第三方的域名public $proxy = 'app.maxiye.cn';//微信登录代理中转站点private $type = '';//第三方类型:qq,weixin,weiboprivate $app_id = '';//分配给网站的appid。private $app_secret = '';//分配给网站的appkey。private $state = '';//client端的状态值。用于第三方应用防止CSRF攻击,成功授权后回调时会原样带回。请务必严格按照流程检查用户与state参数状态的绑定。private $code = '';//用户成功登录并授权,则会跳转到指定的回调地址,并在URL中带上Authorization Code。private $access_token;private $config = [//配置多个网站的appid&appkey'maxiye.cn' => ['qq' => ['app_id' => '100000000','app_secret' => 'f9038c3d07c*******7884edf3e31708',],'weixin' => ['app_id' => 'wxee7c90a7744c2002','app_secret' => '13e649627894*******7a85a0e2f50e7',],'weibo' => ['app_id' => '1200000000','app_secret' => 'e074de8*******d3818d0df9ca28c459',],],'yexima.com' => ['qq' => ['app_id' => '101111244','app_secret' => '6ca59c6a1b1*******77e636a10ac334',],'weixin' => ['app_id' => 'wx0b822222ea9ee323','app_secret' => '7f9cbd*******f37ce7b4c267bdde029',],'weibo' => ['app_id' => '911111998','app_secret' => '5b21c452f88e2982*******1722d8fcd',],],];function __construct($params = []){$this->type = $params['type'];$this->server = $_SERVER['SERVER_NAME'];foreach ($this->config as $k => $v) {if (stristr($this->server, $k) && isset($v[$this->type])) {$this->app_id = $v[$this->type]['app_id'];$this->app_secret = $v[$this->type]['app_secret'];}}if (isset($params['code'])) {$this->code = $params['code'];}}/*** 获取用户授权验证的链接* @return string*/public function getOauthUrl(){$this->state = md5(uniqid(rand(), TRUE));Yii::$app->session->setFlash('oauth_state', $this->state);$redirect_uri = urlencode(Url::to(['login-by-openid', 'type' => $this->type, true));if ($this->type == 'weixin' && $this->server != $this->proxy) {//微信回调多域名代理处理$redirect_uri = str_replace($this->server, $this->proxy, $redirect_uri) . '%26redirect%3D' . $this->server;}$url = '';switch ($this->type) {case 'qq'://qq回调域填写一级域名并以“;”结束:http://maxiye.cn;http://yexima.com;$url = "https://graph.qq.com/oauth/show?which=Login&display=pc&response_type=code&client_id={$this->app_id}&state={$this->state}&display=web&redirect_uri={$redirect_uri}";break;case 'weixin'://app.maxiye.cn不支持只填写一级域名$url = "https://open.weixin.qq.com/connect/qrconnect?response_type=code&appid={$this->app_id}&state={$this->state}&scope=snsapi_login&redirect_uri={$redirect_uri}#wechat_redirect";break;case 'weibo'://微博设置安全域名:maxiye.cn$url = "https://api.weibo.com/oauth2/authorize?response_type=code&client_id={$this->app_id}&state={$this->state}&display=web&redirect_uri={$redirect_uri}";break;default:break;}return $url;}/*** 获取针对开发者账号的惟一uuid* @return string unionid或uid*/public function getUuid(){$openid = '';if ($this->type == 'qq') {$this->getAccessToken();} else {$openid = $this->getOpenid();}$access_token = $this->access_token;$uuid = '';if ($access_token) {switch ($this->type) {case 'qq':$url = "https://graph.qq.com/oauth2.0/me?access_token={$access_token}&unionid=1";// 返回示例.../*callback({"client_id":"YOUR_APPID","openid":"YOUR_OPENID","unionid":"YOUR_UNIONID"});*/$result = $this->get_contents($url);if (strpos($result, "callback") !== false) {$lpos = strpos($result, "(");$rpos = strrpos($result, ")");$result = json_decode(substr($result, $lpos + 1, $rpos - $lpos - 1), true);$uuid = isset($result['unionid']) ? $result['unionid'] : '';}return $uuid;// return $openid;break;case 'weixin':$url = "https://api.weixin.qq.com/sns/userinfo?access_token={$access_token}&openid={$openid}";// 返回示例/*{"openid":"OPENID","nickname":"NICKNAME","sex":1,"province":"PROVINCE","city":"CITY","country":"COUNTRY","headimgurl": "http://wx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/0","privilege":["PRIVILEGE1","PRIVILEGE2"],"unionid": " o6_bmasdasdsad6_2sgVt7hMZOPfL"}*/$result = json_decode($this->get_contents($url), true);return isset($result['unionid']) ? $result['unionid'] : '';break;case 'weibo':return $openid;break;default:break;}}return $uuid;}/*** 获取access_token* @param boolean $true false表示获取原始结果,true获取真正的access_token* @return string json包|string*/public function getAccessToken($true = true){//验证stateif (Yii::$app->request->get('state', '') != Yii::$app->session->getFlash('oauth_state')) {return '';}$redirect_uri = urlencode(Url::to(['login-by-openid', 'type' => $this->type], true));$url = '';switch ($this->type) {case 'qq':$url = "https://graph.qq.com/oauth2.0/token?grant_type=authorization_code&code={$this->code}&client_id={$this->app_id}&client_secret={$this->app_secret}&redirect_uri={$redirect_uri}";//返回示例...//access_token=15C0CE01C0311240F9091A7DB6828E62&expires_in=7776000&refresh_token=7BFCE2E5B773D4F5531561A10E1C2B2Dbreak;case 'weixin':$url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid={$this->app_id}&secret={$this->app_secret}&code={$this->code}&grant_type=authorization_code";//返回示例/*{"access_token":"ACCESS_TOKEN","expires_in":7200,"refresh_token":"REFRESH_TOKEN","openid":"OPENID","scope":"SCOPE"}*/break;case 'weibo':$url = "https://api.weibo.com/oauth2/access_token?client_id={$this->app_id}&client_secret={$this->app_secret}&grant_type=authorization_code&redirect_uri={$redirect_uri}&code={$this->code}";//新浪微博$post_data = [];return $this->post($url, $post_data);//撒币制杖//返回示例/*{"access_token": "SlAV32hkKG","remind_in": 3600,"expires_in": 3600,"uid":"12341234"}*/break;default:break;}if ($true) {$res_access_token = $this->get_contents($url);if ($this->type == 'qq' && strpos($res_access_token, "access_token") !== false) {$token_result = ['access_token' => explode('=', explode('&', $res_access_token)[0])[1]];} else {$token_result = json_decode($res_access_token ?: '{}', true);}$access_token = !empty($token_result['access_token']) ? $token_result['access_token'] : '';$this->access_token = $access_token;return $access_token;} else {return $this->get_contents($url);}}/*** post* post方式请求资源* @param string $url 基于的baseUrl* @param array $keysArr 请求的参数列表* @param int $flag 标志位* @return string 返回的资源内容*/public function post($url, $keysArr, $flag = 0){$ch = curl_init();if (!$flag) curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);curl_setopt($ch, CURLOPT_POST, TRUE);curl_setopt($ch, CURLOPT_POSTFIELDS, $keysArr);curl_setopt($ch, CURLOPT_URL, $url);$ret = curl_exec($ch);curl_close($ch);return $ret;}/*** get_contents* 服务器通过get请求获得内容* @param string $url 请求的url,拼接后的* @return string 请求返回的内容*/public function get_contents($url){$ch = curl_init();curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);curl_setopt($ch, CURLOPT_URL, $url);$response = curl_exec($ch);curl_close($ch);//-------请求为空if (empty($response)) {return '{}';}return $response;}/*** 获取openid* @return string openid*/public function getOpenid(){$res_access_token = $this->getAccessToken(false);if ($this->type == 'qq' && strpos($res_access_token, "access_token") !== false) {$access_token = ['access_token' => explode('=', explode('&', $res_access_token)[0])[1]];} else {$access_token = json_decode($res_access_token ?: '{}', true);}$openid = '';if (isset($access_token['access_token'])) {$this->access_token = $access_token['access_token'];switch ($this->type) {case 'qq':$url = "https://graph.qq.com/oauth2.0/me?access_token={$access_token['access_token']}";// 返回示例...// callback( {"client_id":"101406183","openid":"6C611CBE0C72F765572AE2472C9B59A4"} );$result = $this->get_contents($url);if (strpos($result, "callback") !== false) {$lpos = strpos($result, "(");$rpos = strrpos($result, ")");$result = json_decode(substr($result, $lpos + 1, $rpos - $lpos - 1), true);$openid = isset($result['openid']) ? $result['openid'] : '';}break;case 'weixin':return $access_token['openid'];break;case 'weibo':return $access_token['uid'];/*$url = "https://api.weibo.com/oauth2/get_token_info?access_token={$access_token['access_token']}";//返回示例{"uid": 1073880650,"appkey": 1352222456,"scope": null,"create_at": 1352267591,"expire_in": 157679471}$result = $this->get_contents($url);$openid = isset($result['uid'])?$result['uid']:'';*/break;default:break;}}return $openid;}
}
使用方法如下:
//第三方登录界面处理
public function actionLoginByOpenid(){Yii::$app->response->format= Response::FORMAT_HTML;$params = Yii::$app->request->get();$oauth = new OauthLogin($params);if(empty($params['code'])){$url = $oauth->getOauthUrl();return $this->redirect($url);}else{//微信代理跳转处理if(isset($params['redirect']) && $params['redirect'] != $oauth->server){$proxy = $oauth->proxy;$server = $params['redirect'];return $this->redirect(str_replace($proxy, $server, Url::current(['redirect'=>null], 'http')));}$openid = $oauth->getUuid();if (!$openid) {//失败处理TODO} else {//成功处理TODO}}
}
具体流程(qq为例)如下:
1.用户点击QQ登陆图标,访问链接http://app.maxiye.cn/site/login-by-openid?type=qq
;
2.服务器处理获取QQ的授权链接,重定向到https://graph.qq.com/oauth/show?which=Login&display=pc&response_type=code&client_id=1**83&state=acc19**b&display=web&redirect_uri=http%3A%2F%2Fapp.maxiye.cn%2Fsite%2Flogin-by-openid%3Ftype%3Dqq
;
3.用户点击QQ头像或扫一扫授权通过,请求QQ服务器处理;
4.QQ在回调地址中添加code参数(Authorization Code),回调http://app.maxiye.cn/site/login-by-openid?type=qq&state=4a78***&code=1CA8DF***
;
5.服务器验证state,根据code参数获取access_token,然后获取unionid(uid),处理结果。