文档地址: 网站应用的方式进行扫码登录:[https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html](https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html) 公众号的方式进行扫码登录:[https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_event_pushes.html](https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_event_pushes.html) 思路: 1:网站应用的方式直接参考微信文档即可明白登录流程,详细调用方法如下: 2:公众号方式的思路为:通过关注事件进行的推送,即用户扫描二维码后微信后台会向开发者推送关注或已关注的事件消息,开发者通过解析事件消息来对扫码的用户进行账号绑定或者获取账号信息,能获取到账号信息的即可以登录,获取不到则通知用户进行绑定。详细调用方法如下: 控制器层: ``` <?php namespace Controllers; use Controllers\BaseController; use Util\SendSms; use \Common\Tools; class OauthController extends BaseController { public function getBindAction() { $auth = $this->request->get('auth'); if (empty($auth)) { $this->outputJsonError('auth参数不允许为空'); } $auth = Tools::authcode($auth, 'DECODE'); if (empty($auth)) { $this->outputJsonError('非法的auth数据'); } $oauthInfo = $this->local->call(array('service' => 'Services\Oauth', 'method' => 'getOauth', 'args' => array(['site' => 2, 'sid' => $auth], "itemid,nickname,userid"))); if (!$oauthInfo['data']) { $this->outputJsonSuccess(['type' => 1]); } elseif ($oauthInfo['data']['userid'] != $this->userinfo['userid']) { $this->outputJsonSuccess(['type' => 2]); } else { $oauthInfo['data']['type'] = 3; $this->outputJsonSuccess($oauthInfo['data']); } } /** * @Author zhp * @DateTime 2020-04-22 * @desc [微信回调通知] * @version [version] * @return [type] [description] */ public function wxchatNotifyAction() { $checkSignature = $this->local->call(array('service' => 'Services\Oauth', 'method' => 'checkSignature', 'args' => array())); if ($checkSignature['code'] != 200) { exit('非法请求'); } $HTTP_RAW_POST_DATA or $HTTP_RAW_POST_DATA = file_get_contents('php://input'); if (!$HTTP_RAW_POST_DATA) { exit($_GET["echostr"]); } $this->local->call(array('service' => 'Services\Oauth', 'method' => 'wxchatNotify', 'args' => array($HTTP_RAW_POST_DATA))); } public function wxchatQrcodeBindAction() { $userid = $this->userinfo['userid']; $sid = Tools::random(9, '123456789'); $auth = Tools::authcode($sid, ''); $scene_str = "wxbind_{$userid}_{$sid}"; $res = $this->local->call(array('service' => 'Services\Oauth', 'method' => 'wxchatQrcode', 'args' => array($scene_str))); if ($res['code'] != 200) { $this->outputJsonError($res['msg']); } else { $this->outputJsonSuccess(['auth' => $auth, 'url' => $res['data']['url']]); } } /** * @Author zhp * @DateTime 2020-04-21 * @desc [公众号方式-生成二维码] * @version [version] * @return [type] [description] */ public function wxchatQrcodeAction() { $sid = Tools::random(9, '123456789'); $scene_str = "wxlogin_{$sid}"; $auth = Tools::authcode($sid, ''); $res = $this->local->call(array('service' => 'Services\Oauth', 'method' => 'wxchatQrcode', 'args' => array($scene_str))); if ($res['code'] != 200) { $this->outputJsonError($res['msg']); } else { $this->outputJsonSuccess(['auth' => $auth, 'url' => $res['data']['url']]); } } /** * @Author zhp * @DateTime 2020-04-21 * @desc [公众号方式-扫码关注后登录] * @version [version] * @return [type] [description] */ public function wxchatLoginAction() { $header = []; $auth = $this->request->get('auth'); if (empty($auth)) { $this->outputJsonError('auth参数不允许为空'); } $auth = Tools::authcode($auth, 'DECODE'); if (empty($auth)) { $this->outputJsonError('非法的auth数据'); } $header['HTTP_USER_AGENT'] = $this->request->getHeader("HTTP_USER_AGENT"); $result = $this->local->call(array('service' => 'Services\Oauth', 'method' => 'wxchatLogin', 'args' => array($auth, $header))); $this->outputJsonSuccess($result['data']); } /** * @Author zhp * @DateTime 2020-04-21 * @desc [网站应用方式-扫码登录] * @version [version] * @return [type] [description] */ public function wxLoginAction() { $header = []; $code = $this->request->getPost('code'); if (!$code) { $this->outputJsonError('参数不允许为空'); } $header['HTTP_USER_AGENT'] = $this->request->getHeader("HTTP_USER_AGENT"); $result = $this->local->call(array('service' => 'Services\Oauth', 'method' => 'wxLogin', 'args' => array($code, $header))); if ($result['code'] != 200) { $this->outputJsonError($result['msg'], 600, $result['data']); } else { $this->outputJsonSuccess($result['data']); } } /** * @Author zhp * @DateTime 2020-04-21 * @desc [网站应用方式-微信绑定] * @version [version] * @return [type] [description] */ public function wxBindAction() { $code = $this->request->getPost('code'); if (!$code) { $this->outputJsonError('参数不允许为空'); } $wxUser = $this->local->call(array('service' => 'Services\Oauth', 'method' => 'getWx', 'args' => array($code))); if ($wxUser['code'] != 200) { $this->outputJsonError($wxUser['msg']); } $result = $this->local->call(array('service' => 'Services\Oauth', 'method' => 'wxBind', 'args' => array($wxUser['data'], $this->userinfo))); if ($result['code'] != 200) { $this->outputJsonError($result['msg']); } else { $this->outputJsonSuccess($result['data']); } } /** * @Author zhp * @DateTime 2020-04-21 * @desc [微信解绑] * @version [version] * @return [type] [description] */ public function wxDelBingAction() { $this->local->call(array('service' => 'Services\Oauth', 'method' => 'wxDelBind', 'args' => array($this->userinfo))); $this->outputJsonSuccess([]); } public function wxchatDelBindAction() { $data = $this->request->getPost(); if (empty($data['itemid'])) { $this->outputJsonError('参数不允许为空'); } $this->local->call(array('service' => 'Services\Oauth', 'method' => 'editOauth', 'args' => array(['itemid' => $data['itemid'], 'userid' => $this->userinfo['userid']], ['userid' => 0]))); $this->outputJsonSuccess([]); } /** * @Author zhp * @DateTime 2020-04-21 * @desc [网站应用方式or公众号方式扫码 后手机号绑定] * @version [version] * @return [type] [description] */ public function wxLoginMobileAction() { $data = $this->request->getPost(); if (empty($data['id']) || empty($data['site']) || empty($data['mobile']) || empty($data['mobilecode'])) { $this->outputJsonError('参数不允许为空'); } if (!Tools::isMobile($data['mobile'])) { $this->outputJsonError('请输入正确的手机号'); } if (!Tools::isCode($data['mobilecode'])) { $this->outputJsonError('验证码格式错误'); } if (!in_array($data['site'], [1, 2])) { $this->outputJsonError('site参数错误'); } //验证码 if ($data['mobilecode'] != $this->memcache->get('bind:mobile:code:' . $data['mobile'])) { $this->outputJsonError('验证码错误'); } $oauthInfo = $this->local->call(array('service' => 'Services\Oauth', 'method' => 'getOauth', 'args' => array(['itemid' => $data['id']], "itemid"))); if (empty($oauthInfo['data'])) { $this->outputJsonError('不存在该微信用户信息'); } $oauthInfo = $oauthInfo['data']; $userInfo = $this->local->call(array('service' => 'Services\Member', 'method' => 'get', 'args' => array('mobile', ['mobile' => $data['mobile']]))); if ($userInfo['code'] != 200) { //注册用户 $password = Tools::random(8); $result = $this->local->call(array('service' => 'Services\Member', 'method' => 'add', 'args' => array('data' => ['mobile' => $data['mobile'], 'username' => $data['mobile'], 'password' => $password]))); if ($result['code'] != 200) { $this->outputJsonError('注册失败'); } $userInfo = $this->local->call(array('service' => 'Services\Member', 'method' => 'get', 'args' => array('id', ['userid' => $result['data']]))); //短信发送密码 $smsObj = new SendSms(); $smsObj->sendSms($data['mobile'], "您的登录密码为{$password},请牢记保存。【星空译站】"); } else { //检测用户是否已绑定 $checkBind = $this->local->call(array('service' => 'Services\Oauth', 'method' => 'getOauth', 'args' => array(['userid' => $userInfo['data']['userid']], "itemid"))); if ($checkBind['data']) { $this->outputJsonError('该手机号已绑定其它微信,请更换手机号'); } } $result = $this->local->call(array('service' => 'Services\Oauth', 'method' => 'wxBind', 'args' => array($oauthInfo, $userInfo['data'], $data['site']))); if ($result['code'] != 200) { $this->outputJsonError($result['msg']); } else { $header['HTTP_USER_AGENT'] = $this->request->getHeader("HTTP_USER_AGENT"); $jwt = $this->local->call(array('service' => 'Services\Oauth', 'method' => 'logAndJwt', 'args' => array($userInfo['data'], $header))); $userInfo['data']['other'] = json_decode($userInfo['data']['other'], true); $this->outputJsonSuccess(['userinfo' => $userInfo['data'], 'jwt' => $jwt['data']['jwt']]); } } } ``` Service逻辑处理层 ``` <?php namespace Services; use Firebase\JWT\JWT; use \Common\ReqHelp; use \Common\Tools; use \Models\Member; use \Models\Oauth; use \Services\BaseService; class OauthService extends BaseService { private $WX_TOKEN_URL = 'https://api.weixin.qq.com/sns/oauth2/access_token'; private $WX_USERINFO_URL = 'https://api.weixin.qq.com/sns/userinfo'; public function wxchatQrcode($scene_str = '') { $access_token = $this->getAccesstoken(); if ($access_token['code'] != 200) { return $this->outputData([], 600, $access_token['msg']); } $url = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token={$access_token['data']}"; $param = '{"expire_seconds": 3600, "action_name": "QR_STR_SCENE", "action_info": {"scene": {"scene_str": "' . $scene_str . '"}}}'; $res = ReqHelp::postjson($url, $param); if ($res['0'] != 200) { return $this->outputData([], 600, '二维码获取失败'); } $res = json_decode($res['1'], true); return $this->outputData(['url' => $res['url']]); } public function wxchatNotify($HTTP_RAW_POST_DATA) { $nowtime = time(); if (function_exists('libxml_disable_entity_loader')) { libxml_disable_entity_loader(true); } $x = (array) simplexml_load_string($HTTP_RAW_POST_DATA, 'SimpleXMLElement', LIBXML_NOCDATA); if ($x['MsgType'] == 'event') { //事件 switch ($x['Event']) { case 'CLICK': //点击菜单拉取消息时的事件推送 switch ($x['EventKey']) { } break; case 'subscribe': //订阅 $user = $this->getOauth(['openid' => $x['FromUserName']]); $info = $this->wxchatGetWx($x['FromUserName'])['data']; $param = [ 'site' => 2, 'openid' => $x['FromUserName'], 'nickname' => $info['nickname'], 'avatar' => $info['headimgurl'], 'addtime' => $nowtime, 'unionid' => !empty($info['unionid']) ? $info['unionid'] : '', 'subscribe' => 1, 'sex' => $info['sex'], 'city' => $info['city'], 'province' => $info['province'], 'country' => $info['country'], 'language' => $info['language'], ]; if ($user['data']) { $this->editOauth(['itemid' => $user['data']['itemid']], $param); } else { $this->addOauth($param); } if (strpos($x['EventKey'], 'qrscene_') !== false) { $sid = substr($x['EventKey'], 8); if (substr($sid, 0, 7) == 'wxlogin') { $sid = substr($sid, 8); $this->editOauth(['openid' => $x['FromUserName']], ['sid' => $sid]); } if (substr($sid, 0, 6) == 'wxbind') { $sid = explode("_", substr($sid, 7)); $oauthInfo = $this->getOauth(['site' => 2, 'openid' => $x['FromUserName']], "itemid,userid"); if ($oauthInfo['data']['userid']) { $this->editOauth(['openid' => $x['FromUserName']], ['sid' => $sid['1']]); } else { $this->editOauth(['openid' => $x['FromUserName']], ['userid' => $sid['0'], 'sid' => $sid['1']]); } } } break; case 'unsubscribe': //取消订阅 break; case 'SCAN': //扫描二维码[已关注] $user = $this->getOauth(['openid' => $x['FromUserName']]); if ($user['data']) { if (substr($x['EventKey'], 0, 7) == 'wxlogin') { $sid = substr($x['EventKey'], 8); $this->editOauth(['openid' => $x['FromUserName']], ['sid' => $sid]); } if (substr($x['EventKey'], 0, 6) == 'wxbind') { $sid = explode("_", substr($x['EventKey'], 7)); $oauthInfo = $this->getOauth(['site' => 2, 'openid' => $x['FromUserName']], "itemid,userid"); if ($oauthInfo['data']['userid']) { $this->editOauth(['openid' => $x['FromUserName']], ['sid' => $sid['1']]); } else { $this->editOauth(['openid' => $x['FromUserName']], ['userid' => $sid['0'], 'sid' => $sid['1']]); } } } break; case 'LOCATION': //上报地理位置事件 break; case 'VIEW': //点击菜单跳转链接时的事件推送 break; case 'TEMPLATESENDJOBFINISH': //模板消息结果 break; default: break; } } else { //消息 switch ($MsgType) { case 'text': //文本消息 break; case 'image': //图片消息 break; case 'voice': //语音消息 break; case 'video': //视频消息 break; case 'location': //地理位置消息 break; case 'link': //链接消息 break; default: break; } } return $this->outputData([]); } /** * @Author zhp * @DateTime 2020-04-21 * @desc [解绑微信用户] * @version [version] * @param [type] $userInfo [description] * @return [type] [description] */ public function wxDelBind($userInfo) { $oauthInfo = Oauth::findFirst(array( 'conditions' => "userid = {$userInfo['userid']}", )); $oauthInfo->delete(); return $this->outputData([]); } public function addOauth($data) { $obj = new Oauth(); if ($obj->save($data)) { return $this->outputData($obj->itemid); } else { return $this->outputData(false, 600, '添加失败!'); } } public function editOauth($search, $data) { $condition = ''; $bind = []; if ($search) { foreach ($search as $k => $v) { $condition .= "{$k} = :{$k}: and "; $bind[$k] = $v; } } if (empty($condition)) { return $this->outputData([]); } $condition = rtrim($condition, ' and '); $oauthInfo = Oauth::findFirst(array( 'conditions' => $condition, 'bind' => $bind, )); if (!$oauthInfo) { return $this->outputData(false, 600, '不存在该信息'); } $oauthInfo->save($data); return $this->outputData([]); } /** * @Author zhp * @DateTime 2020-04-21 * @desc [获取绑定信息] * @version [version] * @param [type] $userid [description] * @param string $fields [description] * @return [type] [description] */ public function getOauth($search, $fields = "*") { $condition = ''; $bind = []; if ($search) { foreach ($search as $k => $v) { $condition .= "{$k} = :{$k}: and "; $bind[$k] = $v; } } if (empty($condition)) { return $this->outputData([]); } $condition = rtrim($condition, ' and '); $oauthInfo = Oauth::findFirst(array( 'conditions' => $condition, 'bind' => $bind, 'columns' => "{$fields}", )); if ($oauthInfo) { return $this->outputData($oauthInfo->toArray()); } else { return $this->outputData([]); } } /** * @Author zhp * @DateTime 2020-04-21 * @desc [绑定微信用户] * @version [version] * @param [type] $wxUser [description] * @param [type] $userInfo [description] * @return [type] [description] */ public function wxBind($wxUser, $userInfo, $site) { //账户是否已绑定 $oauthInfo = Oauth::findFirst(array( 'conditions' => "userid = {$userInfo['userid']} and site = {$site}", 'columns' => 'itemid', )); if ($oauthInfo) { return $this->outputData([], 600, '该账户已绑定微信'); } //微信是否绑定 $oauthInfo = Oauth::findFirst(array( 'conditions' => "itemid = {$wxUser['itemid']}", )); if (!$oauthInfo) { return $this->outputData([], 600, '不存在该微信信息'); } if ($oauthInfo->userid) { return $this->outputData([], 600, '该微信已绑定账户'); } $oauthInfo->save(['userid' => $userInfo['userid']]); return $this->outputData([]); } public function wxchatLogin($sid, $header = []) { $oauthInfo = Oauth::findFirst(array( 'conditions' => "site=2 and sid={$sid}", 'columns' => 'itemid,userid,openid', )); if (!$oauthInfo) { return $this->outputData(['type' => 1], 200, '未关注公众号'); } $oauthInfo = $oauthInfo->toArray(); if ($oauthInfo['userid']) { //登录 $userInfo = Member::findFirst(array( "conditions" => "userid = {$oauthInfo['userid']}", ))->toArray(); $userInfo['other'] = json_decode($userInfo['other'], true); $jwt = $this->logAndJwt($userInfo, $header); return $this->outputData(['type' => 3, 'userinfo' => $userInfo, 'jwt' => $jwt['data']['jwt']]); } else { return $this->outputData(['type' => 2, 'id' => $oauthInfo['itemid'], 'site' => 2], 200, '请绑定账户'); } } /** * @Author zhp * @DateTime 2020-04-21 * @desc [网站应用方式-扫码登录] * @version [version] * @param [type] $code [description] * @param array $header [description] * @return [type] [description] */ public function wxLogin($code, $header = []) { $wxUser = $this->getWx($code); if ($wxUser['code'] != 200) { return $wxUser; } //该微信用户是否已绑定账号 $oauthInfo = Oauth::findFirst(array( 'conditions' => "openid = {$wxUser['openid']}", 'columns' => 'userid', )); if ($oauthInfo) { //登录 $userInfo = Member::findFirst(array( "conditions" => "userid = {$oauthInfo->userid}", ))->toArray(); $jwt = $this->logAndJwt($userInfo, $header); return $this->outputData(['userinfo' => $userInfo, 'jwt' => $jwt['data']['jwt']]); } else { //未登录手动绑定输入手机号 $this->di['memcache']->set("wx_login_{$wxUser['openid']}", json_encode($wxUser), 1800); return $this->outputData(['id' => $res['openid'], 'site' => 1], 600, '请绑定账户'); } } /** * @Author zhp * @DateTime 2020-04-21 * @desc [网站应用的方式-扫码登录-获取用户信息] * @version [version] * @param [type] $code [description] * @return [type] [description] */ public function getWx($code) { $appid = $this->di['config']->wx->appid; $secret = $this->di['config']->wx->secret; $url = $this->WX_TOKEN_URL . "?appid={$appid}&secret={$secret}&code=CODE&grant_type=authorization_code"; $res = ReqHelp::get($url); $res = json_decode($res, true); if (!empty($res['errcode'])) { return $this->outputData([], 600, $res['errmsg']); } //获取微信用户信息 $url = $this->WX_USERINFO_URL . "?access_token={$res['access_token']}&openid={$res['openid']}"; $wxUser = ReqHelp::get($url); $wxUser = json_decode($wxUser, true); if (!empty($wxUser['errcode'])) { return $this->outputData([], 600, $wxUser['errmsg']); } $wxUser['openid'] = $res['openid']; return $this->outputData(['wxUser' => $wxUser]); } /** * @Author zhp * @DateTime 2020-04-21 * @desc 写登录日志和创建jwt * @version [version] * @param [type] $userInfo [description] * @param array $header [description] * @return [type] [description] */ public function logAndJwt($userInfo, $header = []) { $log['userid'] = $userInfo['userid']; $log['username'] = $userInfo['username']; $log['loginip'] = Tools::getip(); $log['agent'] = $header["HTTP_USER_AGENT"] ?? ''; // $result = Tools::getAddressByIp($log['loginip']); // if ($result['status'] == 0) { // $location = $result['data'][0]['location']; // if (strstr($location, ' ')) { // $loca = explode(' ', $location); // $log['location'] = $loca[0] ?? ''; // $log['operator'] = $loca[1] ?? ''; // } else { // $log['location'] = strstr($location, '本地局域网') ? '' : $location; // $log['operator'] = ''; // } // } $this->di['local']->call(array('service' => 'Services\Login', 'method' => 'add', 'args' => array('data' => $log))); //jwt token $jwtdata = [ 'userid' => $userInfo['userid'] ?? 0, 'expire' => strtotime(date('Ymd')) + 97200, //第二天凌晨3点 ]; $jwt = JWT::encode($jwtdata, $this->di['config']->jwt->secret); return $this->outputData(['jwt' => $jwt]); } /** * @Author zhp * @DateTime 2020-04-21 * @desc [公众号方式-验证签名] * @version [version] * @return [type] [description] */ public function checkSignature() { if (empty($_GET["signature"]) || empty($_GET["timestamp"]) || empty($_GET["nonce"])) { return $this->outputData([], 600, '签名错误'); } $signature = $_GET["signature"]; $timestamp = $_GET["timestamp"]; $nonce = $_GET["nonce"]; $token = $this->di['config']->wxPublicChat->token; $tmpArr = array($token, $timestamp, $nonce); sort($tmpArr, SORT_STRING); $tmpStr = implode($tmpArr); $tmpStr = sha1($tmpStr); if ($tmpStr == $signature) { return $this->outputData([]); } else { return $this->outputData([], 600, '签名错误'); } } /** * @Author zhp * @DateTime 2020-04-22 * @desc [公众号方式-获取微信用户信息] * @version [version] * @param [type] $openid [description] * @return [type] [description] */ public function wxchatGetWx($openid) { $access_token = $this->getAccesstoken(); if (!$access_token['data']) { return $this->outputData([], 600, $access_token['msg']); } $url = 'https://api.weixin.qq.com/cgi-bin/user/info?access_token=' . $access_token['data'] . '&openid=' . $openid; $info = json_decode(ReqHelp::get($url), true); return $this->outputData($info); } /** * @Author zhp * @DateTime 2020-04-21 * @desc [公众号方式] * @version [version] * @return [type] [description] */ public function getAccesstoken() { $appid = $this->di['config']->wxPublicChat->appid; $secret = $this->di['config']->wxPublicChat->secret; $url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={$appid}&secret={$secret}"; $memKey = "{$appid}_accesstoken"; $mem = $this->di['memcache']->get($memKey); if ($mem) { return $this->outputData($mem); } $res = ReqHelp::get($url); $res = json_decode($res, true); if (!empty($res['errcode'])) { return $this->outputData([], 600, $res['errmsg']); } $this->di['memcache']->set($memKey, $res['access_token'], $res['expires_in'] - 1800); return $this->outputData($res['access_token']); } } ```