第三方登陆的整体思路是获取第三方中的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,weibo
private $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)
{
//验证state
if (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=7BFCE2E5B773D4F5531561A10E1C2B2D
break;
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),处理结果。