Browse Source

微信三方登录插件

lizhen_gitee 3 years ago
parent
commit
dd0bc84dfa

+ 1 - 0
addons/third/.addonrc

@@ -0,0 +1 @@
+{"files":["application\\admin\\controller\\Third.php","application\\admin\\lang\\zh-cn\\third.php","application\\admin\\model\\Third.php","application\\admin\\validate\\Third.php","application\\admin\\view\\third\\index.html","application\\index\\controller\\Third.php","application\\index\\view\\third\\prepare.html","public\\assets\\js\\backend\\third.js"],"license":"regular","licenseto":"19079","licensekey":"Vzt5wJenAcXkaQ13 \/GynI7msar3hlCb7GoK3rA==","domains":[],"licensecodes":[],"validations":[],"menus":["third","third\/index","third\/del"]}

+ 79 - 0
addons/third/Third.php

@@ -0,0 +1,79 @@
+<?php
+
+namespace addons\third;
+
+use app\common\library\Menu;
+use think\Addons;
+
+/**
+ * 第三方登录
+ */
+class Third extends Addons
+{
+
+    /**
+     * 插件安装方法
+     * @return bool
+     */
+    public function install()
+    {
+        $menu = [
+            [
+                'name'    => 'third',
+                'title'   => '第三方登录管理',
+                'icon'    => 'fa fa-users',
+                'sublist' => [
+                    [
+                        "name"  => "third/index",
+                        "title" => "查看"
+                    ],
+                    [
+                        "name"  => "third/del",
+                        "title" => "删除"
+                    ]
+                ]
+            ]
+        ];
+        Menu::create($menu);
+        return true;
+    }
+
+    /**
+     * 插件卸载方法
+     * @return bool
+     */
+    public function uninstall()
+    {
+        Menu::delete("third");
+        return true;
+    }
+
+    /**
+     * 插件启用方法
+     * @return bool
+     */
+    public function enable()
+    {
+        Menu::enable("third");
+        return true;
+    }
+
+    /**
+     * 插件禁用方法
+     * @return bool
+     */
+    public function disable()
+    {
+        Menu::disable("third");
+        return true;
+    }
+
+    /**
+     * @param $params
+     */
+    public function configInit(&$params)
+    {
+        $config = $this->getConfig();
+        $params['third'] = ['status' => explode(',', $config['status'])];
+    }
+}

+ 13 - 0
addons/third/bootstrap.js

@@ -0,0 +1,13 @@
+if (Config.modulename === 'index' && Config.controllername === 'user' && ['login', 'register'].indexOf(Config.actionname) > -1 && $("#register-form,#login-form").size() > 0) {
+    $('<style>.social-login{display:flex}.social-login a{flex:1;margin:0 2px;}.social-login a:first-child{margin-left:0;}.social-login a:last-child{margin-right:0;}</style>').appendTo("head");
+    $("#register-form,#login-form").append('<div class="form-group social-login"></div>');
+    if (Config.third.status.indexOf("wechat") > -1) {
+        $('<a class="btn btn-success" href="' + Fast.api.fixurl('/third/connect/wechat') + '"><i class="fa fa-wechat"></i> 微信登录</a>').appendTo(".social-login");
+    }
+    if (Config.third.status.indexOf("qq") > -1) {
+        $('<a class="btn btn-info" href="' + Fast.api.fixurl('/third/connect/qq') + '"><i class="fa fa-qq"></i> QQ登录</a>').appendTo(".social-login");
+    }
+    if (Config.third.status.indexOf("weibo") > -1) {
+        $('<a class="btn btn-danger" href="' + Fast.api.fixurl('/third/connect/weibo') + '"><i class="fa fa-weibo"></i> 微博登录</a>').appendTo(".social-login");
+    }
+}

+ 114 - 0
addons/third/config.php

@@ -0,0 +1,114 @@
+<?php
+
+return [
+    [
+        'name' => 'qq',
+        'title' => 'QQ',
+        'type' => 'array',
+        'content' => [
+            'app_id' => '',
+            'app_secret' => '',
+            'scope' => 'get_user_info',
+        ],
+        'value' => [
+            'app_id' => '100000000',
+            'app_secret' => '123456',
+            'scope' => 'get_user_info',
+        ],
+        'rule' => 'required',
+        'msg' => '',
+        'tip' => '',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'wechat',
+        'title' => '微信',
+        'type' => 'array',
+        'content' => [
+            'app_id' => '',
+            'app_secret' => '',
+            'callback' => '',
+            'scope' => 'snsapi_base',
+        ],
+        'value' => [
+            'app_id' => 'wx69fb4525c9156250',
+            'app_secret' => '50672a30e50a0a0af2f4d64eaebedc06',
+            'scope' => 'snsapi_userinfo',
+        ],
+        'rule' => 'required',
+        'msg' => '',
+        'tip' => '',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'weibo',
+        'title' => '微博',
+        'type' => 'array',
+        'content' => [
+            'app_id' => '',
+            'app_secret' => '',
+            'scope' => 'get_user_info',
+        ],
+        'value' => [
+            'app_id' => '100000000',
+            'app_secret' => '123456',
+            'scope' => 'get_user_info',
+        ],
+        'rule' => 'required',
+        'msg' => '',
+        'tip' => '',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'bindaccount',
+        'title' => '账号绑定',
+        'type' => 'radio',
+        'content' => [
+            1 => '开启',
+            0 => '关闭',
+        ],
+        'value' => '1',
+        'rule' => 'required',
+        'msg' => '',
+        'tip' => '',
+        'ok' => '是否开启账号绑定',
+        'extend' => '',
+    ],
+    [
+        'name' => 'status',
+        'title' => '前台第三方登录开关',
+        'type' => 'checkbox',
+        'content' => [
+            'qq' => 'QQ',
+            'wechat' => '微信',
+            'weibo' => '微博',
+        ],
+        'value' => 'qq,wechat,weibo',
+        'rule' => '',
+        'msg' => '',
+        'tip' => '',
+        'ok' => '前台第三方登录的开关',
+        'extend' => '',
+    ],
+    [
+        'name' => 'rewrite',
+        'title' => '伪静态',
+        'type' => 'array',
+        'content' => [],
+        'value' => [
+            'index/index' => '/third$',
+            'index/connect' => '/third/connect/[:platform]',
+            'index/callback' => '/third/callback/[:platform]',
+            'index/bind' => '/third/bind/[:platform]',
+            'index/unbind' => '/third/unbind/[:platform]',
+        ],
+        'rule' => 'required',
+        'msg' => '',
+        'tip' => '',
+        'ok' => '',
+        'extend' => '',
+    ],
+];

+ 142 - 0
addons/third/controller/Api.php

@@ -0,0 +1,142 @@
+<?php
+
+namespace addons\third\controller;
+
+use addons\third\library\Application;
+use app\common\controller\Api as commonApi;
+use addons\third\library\Service;
+use addons\third\model\Third;
+use app\common\library\Sms;
+use fast\Random;
+use think\Lang;
+use think\Config;
+use think\Session;
+use think\Validate;
+
+/**
+ * 第三方登录插件
+ */
+class Api extends commonApi
+{
+    protected $noNeedLogin = ['getAuthUrl', 'callback', 'account']; // 无需登录即可访问的方法,同时也无需鉴权了
+    protected $noNeedRight = ['*']; // 无需鉴权即可访问的方法
+
+    protected $app = null;
+    protected $options = [];
+    protected $config = null;
+
+    public function _initialize()
+    {
+        //跨域检测
+        check_cors_request();
+        //设置session_id
+        Config::set('session.id', $this->request->server("HTTP_SID"));
+
+        parent::_initialize();
+        $this->config = get_addon_config('third');
+        $this->app = new Application($this->config);
+    }
+
+    /**
+     * H5获取授权链接
+     * @return void
+     */
+    public function getAuthUrl()
+    {
+        $url = $this->request->param('url');
+        $platform = $this->request->param('platform');
+        if (!$url || !$platform || !isset($this->config[$platform])) {
+            $this->error('参数错误');
+        }
+        $this->config[$platform]['callback'] = $url;
+        $this->app = new Application($this->config); //
+        if (!$this->app->{$platform}) {
+            $this->error(__('Invalid parameters'));
+        }
+        $this->success('', $this->app->{$platform}->getAuthorizeUrl());
+    }
+
+    /**
+     * 公众号:wechat 授权回调的请求【非第三方,自己的前端请求】
+     * @return void
+     */
+    public function callback()
+    {
+
+        $platform = $this->request->param('platform');
+        if (!$this->app->{$platform}) {
+            $this->error(__('Invalid parameters'));
+        }
+        $userinfo = $this->app->{$platform}->getUserInfo($this->request->param());
+        if (!$userinfo) {
+            $this->error(__('操作失败'));
+        }
+        $userinfo['apptype'] = 'mp';
+        $userinfo['platform'] = $platform;
+
+        $third = [
+            'avatar' => $userinfo['userinfo']['avatar'],
+            'nickname' => $userinfo['userinfo']['nickname']
+        ];
+
+        $user = null;
+        if ($this->auth->isLogin() || Service::isBindThird($userinfo['platform'], $userinfo['openid'], $userinfo['apptype'], $userinfo['unionid'])) {
+            Service::connect($userinfo['platform'], $userinfo);
+            $user = $this->auth->getUserinfo();
+        } else {
+            $user = false;
+            Session::set('third-userinfo', $userinfo);
+        }
+        $this->success("授权成功!", ['user' => $user, 'third' => $third]);
+    }
+
+    /**
+     * 登录或创建账号
+     */
+    public function account()
+    {
+
+        if ($this->request->isPost()) {
+            $params = Session::get('third-userinfo');
+            $mobile = $this->request->post('mobile', '');
+            $code = $this->request->post('code');
+            $token = $this->request->post('__token__');
+            $rule = [
+                'mobile'    => 'require|regex:/^1\d{10}$/',
+                '__token__' => 'require|token',
+            ];
+            $msg = [
+                'mobile'           => 'Mobile is incorrect',
+            ];
+            $data = [
+                'mobile'    => $mobile,
+                '__token__' => $token,
+            ];
+            $ret = Sms::check($mobile, $code, 'bind');
+            if (!$ret) {
+                $this->error(__('验证码错误'));
+            }
+            $validate = new Validate($rule, $msg);
+            $result = $validate->check($data);
+            if (!$result) {
+                $this->error(__($validate->getError()), ['__token__' => $this->request->token()]);
+            }
+
+            $userinfo = \app\common\model\User::where('mobile', $mobile)->find();
+            if ($userinfo) {
+                $result = $this->auth->direct($userinfo->id);
+            } else {
+                $result = $this->auth->register($mobile, Random::alnum(), '', $mobile);
+            }
+
+            if ($result) {
+                Service::connect($params['platform'], $params);
+                $this->success(__('绑定账号成功'), ['userinfo' => $this->auth->getUserinfo()]);
+            } else {
+                $this->error($this->auth->getError(), ['__token__' => $this->request->token()]);
+            }
+        }
+    }
+
+
+}

+ 137 - 0
addons/third/controller/Index.php

@@ -0,0 +1,137 @@
+<?php
+
+namespace addons\third\controller;
+
+use addons\third\library\Application;
+use addons\third\library\Service;
+use addons\third\model\Third;
+use think\addons\Controller;
+use think\Config;
+use think\Cookie;
+use think\Hook;
+use think\Lang;
+use think\Session;
+
+/**
+ * 第三方登录插件
+ */
+class Index extends Controller
+{
+    protected $app = null;
+    protected $options = [];
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $config = get_addon_config('third');
+        $this->app = new Application($config);
+    }
+
+    /**
+     * 插件首页
+     */
+    public function index()
+    {
+        if (!\app\admin\library\Auth::instance()->id) {
+            $this->error('当前插件暂无前台页面');
+        }
+        $platformList = [];
+        if ($this->auth->id) {
+            $platformList = Third::where('user_id', $this->auth->id)->column('platform');
+        }
+        $this->view->assign('platformList', $platformList);
+        return $this->view->fetch();
+    }
+
+    /**
+     * 发起授权
+     */
+    public function connect()
+    {
+        $platform = $this->request->param('platform');
+        $url = $this->request->request('url', $this->request->server('HTTP_REFERER', '/'), 'trim');        
+        if (!$this->app->{$platform}) {
+            $this->error(__('Invalid parameters'));
+        }
+        if ($url) {
+            Session::set("redirecturl", $url);
+        }
+        // 跳转到登录授权页面
+        $this->redirect($this->app->{$platform}->getAuthorizeUrl());
+        return;
+    }
+
+    /**
+     * 通知回调
+     */
+    public function callback()
+    {
+        $auth = $this->auth;
+
+        //监听注册登录注销的事件
+        Hook::add('user_login_successed', function ($user) use ($auth) {
+            $expire = input('post.keeplogin') ? 30 * 86400 : 0;
+            Cookie::set('uid', $user->id, $expire);
+            Cookie::set('token', $auth->getToken(), $expire);
+        });
+        Hook::add('user_register_successed', function ($user) use ($auth) {
+            Cookie::set('uid', $user->id);
+            Cookie::set('token', $auth->getToken());
+        });
+        Hook::add('user_logout_successed', function ($user) use ($auth) {
+            Cookie::delete('uid');
+            Cookie::delete('token');
+        });
+        $platform = $this->request->param('platform');
+
+        // 成功后返回之前页面
+        $url = Session::has("redirecturl") ? Session::pull("redirecturl") : url('index/user/index');
+
+        // 授权成功后的回调
+        $userinfo = $this->app->{$platform}->getUserInfo();
+        if (!$userinfo) {
+            $this->error(__('操作失败'), $url);
+        }
+
+        Session::set("{$platform}-userinfo", $userinfo);
+        //判断是否启用账号绑定
+        $third = Third::get(['platform' => $platform, 'openid' => $userinfo['openid']]);
+        if (!$third) {
+            $config = get_addon_config('third');
+            //要求绑定账号或会员当前是登录状态
+            if ($config['bindaccount'] || $this->auth->id) {
+                $this->redirect(url('index/third/prepare') . "?" . http_build_query(['platform' => $platform, 'url' => $url]));
+            }
+        }
+
+        $loginret = Service::connect($platform, $userinfo);
+        if ($loginret) {
+            $this->redirect($url);
+        }
+    }
+
+    /**
+     * 绑定账号
+     */
+    public function bind()
+    {
+        $platform = $this->request->request('platform', $this->request->param('platform', ''));
+        $url = $this->request->get('url', $this->request->server('HTTP_REFERER'));
+        $redirecturl = url("index/third/bind") . "?" . http_build_query(['platform' => $platform, 'url' => $url]);
+        $this->redirect($redirecturl);
+        return;
+    }
+
+    /**
+     * 解绑账号
+     */
+    public function unbind()
+    {
+        $platform = $this->request->request('platform', $this->request->param('platform', ''));
+        $url = $this->request->get('url', $this->request->server('HTTP_REFERER'));
+        $redirecturl = url("index/third/unbind") . "?" . http_build_query(['platform' => $platform, 'url' => $url]);
+        $this->redirect($redirecturl);
+        return;
+    }
+
+}

+ 10 - 0
addons/third/info.ini

@@ -0,0 +1,10 @@
+name = third
+title = 第三方登录
+intro = 使用微信、QQ、微博登录插件
+author = FastAdmin
+website = https://www.fastadmin.net
+version = 1.2.3
+state = 1
+url = /addons/third
+license = regular
+licenseto = 19079

+ 30 - 0
addons/third/install.sql

@@ -0,0 +1,30 @@
+
+CREATE TABLE IF NOT EXISTS `__PREFIX__third` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
+  `user_id` int(10) unsigned DEFAULT '0' COMMENT '会员ID',
+  `platform` varchar(30) DEFAULT '' COMMENT '第三方应用',
+  `apptype` varchar(50) DEFAULT '' COMMENT '应用类型',
+  `unionid` varchar(100) DEFAULT '' COMMENT '第三方UNIONID',
+  `openid` varchar(100) DEFAULT '' COMMENT '第三方OPENID',
+  `openname` varchar(100) DEFAULT '' COMMENT '第三方会员昵称',
+  `access_token` varchar(255) NULL DEFAULT '' COMMENT 'AccessToken',
+  `refresh_token` varchar(255) DEFAULT 'RefreshToken',
+  `expires_in` int(10) unsigned DEFAULT '0' COMMENT '有效期',
+  `createtime` int(10) unsigned DEFAULT NULL COMMENT '创建时间',
+  `updatetime` int(10) unsigned DEFAULT NULL COMMENT '更新时间',
+  `logintime` int(10) unsigned DEFAULT NULL COMMENT '登录时间',
+  `expiretime` int(10) unsigned DEFAULT NULL COMMENT '过期时间',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `platform` (`platform`,`openid`),
+  KEY `user_id` (`user_id`,`platform`),
+  KEY `unionid` (`platform`,`unionid`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='第三方登录表';
+
+ALTER TABLE `__PREFIX__third` ADD COLUMN `apptype` varchar(50) NULL DEFAULT '' COMMENT '应用类型' AFTER `platform`;
+
+ALTER TABLE `__PREFIX__third` ADD COLUMN `unionid` varchar(100) NULL DEFAULT '' COMMENT '第三方UnionID' AFTER `apptype`;
+ALTER TABLE `__PREFIX__third` ADD INDEX `unionid`(`platform`, `unionid`);
+
+ALTER TABLE `__PREFIX__third` CHARACTER SET = utf8mb4, COLLATE = utf8mb4_general_ci;
+ALTER TABLE `__PREFIX__third` MODIFY COLUMN `openname` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '第三方会员昵称' AFTER `unionid`;
+

+ 68 - 0
addons/third/library/Application.php

@@ -0,0 +1,68 @@
+<?php
+
+namespace addons\third\library;
+
+class Application
+{
+
+    /**
+     * 配置信息
+     * @var array
+     */
+    private $config = [];
+
+    /**
+     * 服务提供者
+     * @var array
+     */
+    private $providers = [
+        'qq'      => 'Qq',
+        'weibo'   => 'Weibo',
+        'wechat'  => 'Wechat',
+    ];
+
+    /**
+     * 服务对象信息
+     * @var array
+     */
+    protected $services = [];
+
+    public function __construct($options = [])
+    {
+        $options = array_intersect_key($options, $this->providers);
+        $options = array_merge($this->config, is_array($options) ? $options : []);
+        foreach ($options as $key => &$option) {
+            $option['app_id'] = isset($option['app_id']) ? $option['app_id'] : '';
+            $option['app_secret'] = isset($option['app_secret']) ? $option['app_secret'] : '';
+            // 如果未定义回调地址则自动生成
+            $option['callback'] = isset($option['callback']) && $option['callback'] ? $option['callback'] : addon_url('third/index/callback', [':platform' => $key], false, true);
+        }
+        $this->config = $options;
+        //注册服务器提供者
+        $this->registerProviders();
+    }
+
+    /**
+     * 注册服务提供者
+     */
+    private function registerProviders()
+    {
+        foreach ($this->providers as $k => $v) {
+            $this->services[$k] = function () use ($k, $v) {
+                $options = $this->config[$k];
+                $objname = __NAMESPACE__ . "\\{$v}";
+                return new $objname($options);
+            };
+        }
+    }
+
+    public function __set($key, $value)
+    {
+        $this->services[$key] = $value;
+    }
+
+    public function __get($key)
+    {
+        return isset($this->services[$key]) ? $this->services[$key]($this) : null;
+    }
+}

+ 141 - 0
addons/third/library/Qq.php

@@ -0,0 +1,141 @@
+<?php
+
+namespace addons\third\library;
+
+use fast\Http;
+use think\Config;
+use think\Session;
+
+/**
+ * QQ
+ */
+class Qq
+{
+    const GET_AUTH_CODE_URL = "https://graph.qq.com/oauth2.0/authorize";
+    const GET_ACCESS_TOKEN_URL = "https://graph.qq.com/oauth2.0/token";
+    const GET_USERINFO_URL = "https://graph.qq.com/user/get_user_info";
+    const GET_OPENID_URL = "https://graph.qq.com/oauth2.0/me";
+
+    /**
+     * 配置信息
+     * @var array
+     */
+    private $config = [];
+
+    public function __construct($options = [])
+    {
+        if ($config = Config::get('third.qq')) {
+            $this->config = array_merge($this->config, $config);
+        }
+        $this->config = array_merge($this->config, is_array($options) ? $options : []);
+    }
+
+    /**
+     * 登陆
+     */
+    public function login()
+    {
+        header("Location:" . $this->getAuthorizeUrl());
+    }
+
+    /**
+     * 获取authorize_url
+     */
+    public function getAuthorizeUrl()
+    {
+        $state = md5(uniqid(rand(), true));
+        Session::set('state', $state);
+        $queryarr = array(
+            "response_type" => "code",
+            "client_id"     => $this->config['app_id'],
+            "redirect_uri"  => $this->config['callback'],
+            "scope"         => $this->config['scope'],
+            "state"         => $state,
+        );
+        request()->isMobile() && $queryarr['display'] = 'mobile';
+        $url = self::GET_AUTH_CODE_URL . '?' . http_build_query($queryarr);
+        return $url;
+    }
+
+    /**
+     * 获取用户信息
+     * @param array $params
+     * @return array
+     */
+    public function getUserInfo($params = [])
+    {
+        $params = $params ? $params : $_GET;
+        if (isset($params['access_token']) || (isset($params['state']) && $params['state'] == Session::get('state') && isset($params['code']))) {
+            //获取access_token
+            $data = isset($params['code']) ? $this->getAccessToken($params['code']) : $params;
+            $access_token = isset($data['access_token']) ? $data['access_token'] : '';
+            $refresh_token = isset($data['refresh_token']) ? $data['refresh_token'] : '';
+            $expires_in = isset($data['expires_in']) ? $data['expires_in'] : 0;
+            if ($access_token) {
+                $openid = $this->getOpenId($access_token);
+                //获取用户信息
+                $queryarr = [
+                    "access_token"       => $access_token,
+                    "oauth_consumer_key" => $this->config['app_id'],
+                    "openid"             => $openid,
+                ];
+                $ret = Http::get(self::GET_USERINFO_URL, $queryarr);
+                $userinfo = (array)json_decode($ret, true);
+                if (!$userinfo || !isset($userinfo['ret']) || $userinfo['ret'] !== 0) {
+                    return [];
+                }
+                $userinfo = $userinfo ? $userinfo : [];
+                $userinfo['avatar'] = isset($userinfo['figureurl_qq_2']) ? $userinfo['figureurl_qq_2'] : '';
+                $data = [
+                    'access_token'  => $access_token,
+                    'refresh_token' => $refresh_token,
+                    'expires_in'    => $expires_in,
+                    'openid'        => $openid,
+                    'userinfo'      => $userinfo
+                ];
+                return $data;
+            }
+        }
+        return [];
+    }
+
+    /**
+     * 获取access_token
+     * @param string $code
+     * @return array
+     */
+    public function getAccessToken($code = '')
+    {
+        if (!$code) {
+            return [];
+        }
+        $queryarr = array(
+            "grant_type"    => "authorization_code",
+            "client_id"     => $this->config['app_id'],
+            "client_secret" => $this->config['app_secret'],
+            "redirect_uri"  => $this->config['callback'],
+            "code"          => $code,
+        );
+        $ret = Http::get(self::GET_ACCESS_TOKEN_URL, $queryarr);
+        $params = [];
+        parse_str($ret, $params);
+        return $params ? $params : [];
+    }
+
+    /**
+     * 获取open_id
+     * @param string $access_token
+     * @return string
+     */
+    private function getOpenId($access_token = '')
+    {
+        $response = Http::get(self::GET_OPENID_URL, ['access_token' => $access_token]);
+        if (strpos($response, "callback") !== false) {
+            $lpos = strpos($response, "(");
+            $rpos = strrpos($response, ")");
+            $response = substr($response, $lpos + 1, $rpos - $lpos - 1);
+        }
+        $user = (array)json_decode($response, true);
+        return isset($user['openid']) ? $user['openid'] : '';
+    }
+}

+ 155 - 0
addons/third/library/Service.php

@@ -0,0 +1,155 @@
+<?php
+
+namespace addons\third\library;
+
+use addons\third\model\Third;
+use app\common\model\User;
+use fast\Random;
+use think\Db;
+use think\Exception;
+
+/**
+ * 第三方登录服务类
+ *
+ */
+class Service
+{
+
+    /**
+     * 第三方登录
+     * @param string $platform 平台
+     * @param array  $params   参数
+     * @param array  $extend   会员扩展信息
+     * @param int    $keeptime 有效时长
+     * @return boolean
+     */
+    public static function connect($platform, $params = [], $extend = [], $keeptime = 0)
+    {
+
+        $time = time();
+        $nickname = $params['nickname'] ?? ($params['userinfo']['nickname'] ?? '');
+        $avatar = $params['avatar'] ?? ($params['userinfo']['avatar'] ?? '');
+        $values = [
+            'platform'      => $platform,
+            'openid'        => $params['openid'],
+            'openname'      => $nickname,
+            'access_token'  => $params['access_token'],
+            'refresh_token' => $params['refresh_token'],
+            'expires_in'    => $params['expires_in'],
+            'logintime'     => $time,
+            'expiretime'    => $time + $params['expires_in'],
+        ];
+        $values = array_merge($values, $params);
+
+        $auth = \app\common\library\Auth::instance();
+
+        $auth->keeptime($keeptime);
+        //是否有自己的
+        $third = Third::get(['platform' => $platform, 'openid' => $params['openid']], 'user');
+        if ($third) {
+            if (!$third->user) {
+                $third->delete();
+            } else {
+                $third->allowField(true)->save($values);
+                // 写入登录Cookies和Token
+                return $auth->direct($third->user_id);
+            }
+        }
+
+        //存在unionid就需要判断是否需要生成新记录
+        if (isset($params['unionid']) && !empty($params['unionid'])) {
+            $third = Third::get(['platform' => $platform, 'unionid' => $params['unionid']], 'user');
+            if ($third) {
+                if (!$third->user) {
+                    $third->delete();
+                } else {
+                    // 保存第三方信息
+                    $values['user_id'] = $third->user_id;
+                    $third = Third::create($values, true);
+                    // 写入登录Cookies和Token
+                    return $auth->direct($third->user_id);
+                }
+            }
+        }
+
+        if ($auth->id) {
+            if (!$third) {
+                $values['user_id'] = $auth->id;
+                Third::create($values, true);
+            }
+            $user = $auth->getUser();
+        } else {
+            // 先随机一个用户名,随后再变更为u+数字id
+            $username = Random::alnum(20);
+            $password = Random::alnum(6);
+            $domain = request()->host();
+
+            Db::startTrans();
+            try {
+                // 默认注册一个会员
+                $result = $auth->register($username, $password, $username . '@' . $domain, '', $extend);
+                if (!$result) {
+                    throw new Exception($auth->getError());
+                }
+                $user = $auth->getUser();
+                $fields = ['username' => 'u' . $user->id, 'email' => 'u' . $user->id . '@' . $domain];
+                if ($nickname) {
+                    $fields['nickname'] = $nickname;
+                }
+                if ($avatar) {
+                    $fields['avatar'] = htmlspecialchars(strip_tags($avatar));
+                }
+
+                // 更新会员资料
+                $user = User::get($user->id);
+                $user->save($fields);
+
+                // 保存第三方信息
+                $values['user_id'] = $user->id;
+                Third::create($values, true);
+                Db::commit();
+            } catch (\Exception $e) {
+                Db::rollback();
+                $auth->logout();
+                return false;
+            }
+        }
+        // 写入登录Cookies和Token
+        return $auth->direct($user->id);
+    }
+
+
+    public static function isBindThird($platform, $openid, $apptype = '', $unionid = '')
+    {
+        $conddtions = [
+            'platform' => $platform,
+            'openid' => $openid
+        ];
+        if ($apptype) {
+            $conddtions['apptype'] = $apptype;
+        }
+        $third = Third::get($conddtions, 'user');
+        //第三方存在
+        if ($third) {
+            //用户失效
+            if (!$third->user) {
+                $third->delete();
+                return false;
+            }
+            return true;
+        }
+        if ($unionid) {
+            $third = Third::get(['platform' => $platform, 'unionid' => $unionid], 'user');
+            if ($third) {
+                //
+                if (!$third->user) {
+                    $third->delete();
+                    return false;
+                }
+                return true;
+            }
+        }
+
+        return false;
+    }
+}

+ 127 - 0
addons/third/library/Wechat.php

@@ -0,0 +1,127 @@
+<?php
+
+namespace addons\third\library;
+
+use fast\Http;
+use think\Config;
+use think\Session;
+
+/**
+ * 微信
+ */
+class Wechat
+{
+    const GET_AUTH_CODE_URL = "https://open.weixin.qq.com/connect/oauth2/authorize";
+    const GET_ACCESS_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/access_token";
+    const GET_USERINFO_URL = "https://api.weixin.qq.com/sns/userinfo";
+
+    /**
+     * 配置信息
+     * @var array
+     */
+    private $config = [];
+
+    public function __construct($options = [])
+    {
+        if ($config = Config::get('third.wechat')) {
+            $this->config = array_merge($this->config, $config);
+        }
+        $this->config = array_merge($this->config, is_array($options) ? $options : []);
+    }
+
+    /**
+     * 登陆
+     */
+    public function login()
+    {
+        header("Location:" . $this->getAuthorizeUrl());
+    }
+
+    /**
+     * 获取authorize_url
+     */
+    public function getAuthorizeUrl()
+    {
+        $state = md5(uniqid(rand(), true));
+        Session::set('state', $state);
+        $queryarr = array(
+            "appid"         => $this->config['app_id'],
+            "redirect_uri"  => $this->config['callback'],
+            "response_type" => "code",
+            "scope"         => $this->config['scope'],
+            "state"         => $state,
+        );
+        request()->isMobile() && $queryarr['display'] = 'mobile';
+        $url = self::GET_AUTH_CODE_URL . '?' . http_build_query($queryarr) . '#wechat_redirect';
+        return $url;
+    }
+
+    /**
+     * 获取用户信息
+     * @param array $params
+     * @return array
+     */
+    public function getUserInfo($params = [])
+    {
+        $params = $params ? $params : request()->get();
+        if (isset($params['access_token']) || (isset($params['state']) && $params['state'] == Session::get('state') && isset($params['code']))) {
+            //获取access_token
+            $data = isset($params['code']) ? $this->getAccessToken($params['code']) : $params;
+            $access_token = isset($data['access_token']) ? $data['access_token'] : '';
+            $refresh_token = isset($data['refresh_token']) ? $data['refresh_token'] : '';
+            $expires_in = isset($data['expires_in']) ? $data['expires_in'] : 0;
+            if ($access_token) {
+                $openid = isset($data['openid']) ? $data['openid'] : '';
+                $unionid = isset($data['unionid']) ? $data['unionid'] : '';
+                if (stripos($this->config['scope'], 'snsapi_userinfo') !== false) {
+                    //获取用户信息
+                    $queryarr = [
+                        "access_token" => $access_token,
+                        "openid"       => $openid,
+                        "lang"         => 'zh_CN'
+                    ];
+                    $ret = Http::get(self::GET_USERINFO_URL, $queryarr);
+                    $userinfo = (array)json_decode($ret, true);
+                    if (!$userinfo || isset($userinfo['errcode'])) {
+                        return [];
+                    }
+                    $userinfo = $userinfo ? $userinfo : [];
+                    $userinfo['avatar'] = isset($userinfo['headimgurl']) ? $userinfo['headimgurl'] : '';
+                } else {
+                    $userinfo = [];
+                }
+                $data = [
+                    'access_token'  => $access_token,
+                    'refresh_token' => $refresh_token,
+                    'expires_in'    => $expires_in,
+                    'openid'        => $openid,
+                    'unionid'       => $unionid,
+                    'userinfo'      => $userinfo
+                ];
+                return $data;
+            }
+        }
+        return [];
+    }
+
+    /**
+     * 获取access_token
+     * @param string code
+     * @return array
+     */
+    public function getAccessToken($code = '')
+    {
+        if (!$code) {
+            return [];
+        }
+        $queryarr = array(
+            "appid"      => $this->config['app_id'],
+            "secret"     => $this->config['app_secret'],
+            "code"       => $code,
+            "grant_type" => "authorization_code",
+        );
+        $response = Http::get(self::GET_ACCESS_TOKEN_URL, $queryarr);
+        $ret = (array)json_decode($response, true);
+        return $ret ? $ret : [];
+    }
+}

+ 121 - 0
addons/third/library/Weibo.php

@@ -0,0 +1,121 @@
+<?php
+
+namespace addons\third\library;
+
+use fast\Http;
+use think\Config;
+use think\Session;
+
+/**
+ * 微博
+ */
+class Weibo
+{
+    const GET_AUTH_CODE_URL = "https://api.weibo.com/oauth2/authorize";
+    const GET_ACCESS_TOKEN_URL = "https://api.weibo.com/oauth2/access_token";
+    const GET_USERINFO_URL = "https://api.weibo.com/2/users/show.json";
+
+    /**
+     * 配置信息
+     * @var array
+     */
+    private $config = [];
+
+    public function __construct($options = [])
+    {
+        if ($config = Config::get('third.weibo')) {
+            $this->config = array_merge($this->config, $config);
+        }
+        $this->config = array_merge($this->config, is_array($options) ? $options : []);
+    }
+
+    /**
+     * 登陆
+     */
+    public function login()
+    {
+        header("Location:" . $this->getAuthorizeUrl());
+    }
+
+    /**
+     * 获取authorize_url
+     */
+    public function getAuthorizeUrl()
+    {
+        $state = md5(uniqid(rand(), true));
+        Session::set('state', $state);
+        $queryarr = array(
+            "response_type" => "code",
+            "client_id"     => $this->config['app_id'],
+            "redirect_uri"  => $this->config['callback'],
+            "state"         => $state,
+        );
+        request()->isMobile() && $queryarr['display'] = 'mobile';
+        $url = self::GET_AUTH_CODE_URL . '?' . http_build_query($queryarr);
+        return $url;
+    }
+
+    /**
+     * 获取用户信息
+     * @param array $params
+     * @return array
+     */
+    public function getUserInfo($params = [])
+    {
+        $params = $params ? $params : $_GET;
+        if (isset($params['access_token']) || (isset($params['state']) && $params['state'] == Session::get('state') && isset($params['code']))) {
+            //获取access_token
+            $data = isset($params['code']) ? $this->getAccessToken($params['code']) : $params;
+            $access_token = isset($data['access_token']) ? $data['access_token'] : '';
+            $refresh_token = isset($data['refresh_token']) ? $data['refresh_token'] : '';
+            $expires_in = isset($data['expires_in']) ? $data['expires_in'] : 0;
+            if ($access_token) {
+                $uid = isset($data['uid']) ? $data['uid'] : '';
+                //获取用户信息
+                $queryarr = [
+                    "access_token" => $access_token,
+                    "uid"          => $uid,
+                ];
+                $ret = Http::get(self::GET_USERINFO_URL, $queryarr);
+                $userinfo = (array)json_decode($ret, true);
+                if (!$userinfo || isset($userinfo['error_code'])) {
+                    return [];
+                }
+                $userinfo = $userinfo ? $userinfo : [];
+                $userinfo['nickname'] = isset($userinfo['screen_name']) ? $userinfo['screen_name'] : '';
+                $userinfo['avatar'] = isset($userinfo['profile_image_url']) ? $userinfo['profile_image_url'] : '';
+                $data = [
+                    'access_token'  => $access_token,
+                    'refresh_token' => $refresh_token,
+                    'expires_in'    => $expires_in,
+                    'openid'        => $uid,
+                    'userinfo'      => $userinfo
+                ];
+                return $data;
+            }
+        }
+        return [];
+    }
+
+    /**
+     * 获取access_token
+     * @param string code
+     * @return array
+     */
+    public function getAccessToken($code = '')
+    {
+        if (!$code) {
+            return '';
+        }
+        $queryarr = array(
+            "grant_type"    => "authorization_code",
+            "client_id"     => $this->config['app_id'],
+            "client_secret" => $this->config['app_secret'],
+            "redirect_uri"  => $this->config['callback'],
+            "code"          => $code,
+        );
+        $response = Http::post(self::GET_ACCESS_TOKEN_URL, $queryarr);
+        $ret = (array)json_decode($response, true);
+        return $ret ? $ret : [];
+    }
+}

+ 26 - 0
addons/third/model/Third.php

@@ -0,0 +1,26 @@
+<?php
+
+namespace addons\third\model;
+
+use think\Model;
+
+/**
+ * 第三方登录模型
+ */
+class Third extends Model
+{
+
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    // 追加属性
+    protected $append = [
+    ];
+
+    public function user()
+    {
+        return $this->belongsTo('\app\common\model\User', 'user_id', 'id', [], 'LEFT');
+    }
+}

+ 139 - 0
addons/third/view/index/index.html

@@ -0,0 +1,139 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <title>第三方登录 - {$site.name}</title>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+
+    <link href="__CDN__/assets/css/frontend.min.css" rel="stylesheet">
+
+    <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
+    <!--[if lt IE 9]>
+    <script src="https://cdn.jsdelivr.net/npm/html5shiv@3.7.3/dist/html5shiv.min.js"></script>
+    <script src="https://cdn.jsdelivr.net/npm/respond.js@1.4.2/dest/respond.min.js"></script>
+    <![endif]-->
+
+</head>
+<body>
+<div class="container">
+    <h2>第三方登录</h2>
+    <hr>
+    <div class="well">
+        <div class="row">
+            <div class="col-xs-4">
+                {if $user && in_array('qq', $platformList)}
+                <a href="{:addon_url('third/index/unbind',[':platform'=>'qq'])}" class="btn btn-block btn-info">
+                    <i class="fa fa-qq"></i> 点击解绑
+                </a>
+                {else/}
+                <a href="{:addon_url('third/index/connect',[':platform'=>'qq'])}" class="btn btn-block btn-info">
+                    <i class="fa fa-qq"></i> QQ登录
+                </a>
+                {/if}
+            </div>
+            <div class="col-xs-4">
+                {if $user && in_array('wechat', $platformList)}
+                <a href="{:addon_url('third/index/unbind',[':platform'=>'wechat'])}" class="btn btn-block btn-success">
+                    <i class="fa fa-wechat"></i> 点击解绑
+                </a>
+                {else/}
+                <a href="{:addon_url('third/index/connect',[':platform'=>'wechat'])}" class="btn btn-block btn-success">
+                    <i class="fa fa-wechat"></i> 微信登录
+                </a>
+                {/if}
+            </div>
+            <div class="col-xs-4">
+                {if $user && in_array('weibo', $platformList)}
+                <a href="{:addon_url('third/index/unbind',[':platform'=>'weibo'])}" class="btn btn-block btn-danger">
+                    <i class="fa fa-weibo"></i> 点击解绑
+                </a>
+                {else/}
+                <a href="{:addon_url('third/index/connect',[':platform'=>'weibo'])}" class="btn btn-block btn-danger">
+                    <i class="fa fa-weibo"></i> 微博登录
+                </a>
+                {/if}
+            </div>
+        </div>
+    </div>
+    <h2>相关链接</h2>
+    <hr>
+    <table class="table table-striped table-hover">
+        <thead>
+        <tr>
+            <th>QQ</th>
+            <th>链接</th>
+        </tr>
+        </thead>
+        <tbody>
+        <tr>
+            <td>QQ 连接</td>
+            <td>{:addon_url('third/index/connect',[':platform'=>'qq'], false, true)}</td>
+        </tr>
+        <tr>
+            <td>QQ 绑定</td>
+            <td>{:addon_url('third/index/bind',[':platform'=>'qq'], false, true)}</td>
+        </tr>
+        <tr>
+            <td>QQ 解绑</td>
+            <td>{:addon_url('third/index/unbind',[':platform'=>'qq'], false, true)}</td>
+        </tr>
+        </tbody>
+    </table>
+    <table class="table table-striped table-hover">
+        <thead>
+        <tr>
+            <th>微信</th>
+            <th>链接</th>
+        </tr>
+        </thead>
+        <tbody>
+        <tr>
+            <td>微信 连接</td>
+            <td>{:addon_url('third/index/connect',[':platform'=>'wechat'], false, true)}</td>
+        </tr>
+        <tr>
+            <td>微信 绑定</td>
+            <td>{:addon_url('third/index/bind',[':platform'=>'wechat'], false, true)}</td>
+        </tr>
+        <tr>
+            <td>微信 解绑</td>
+            <td>{:addon_url('third/index/unbind',[':platform'=>'wechat'], false, true)}</td>
+        </tr>
+        </tbody>
+    </table>
+    <table class="table table-striped table-hover">
+        <thead>
+        <tr>
+            <th>微博</th>
+            <th>链接</th>
+        </tr>
+        </thead>
+        <tbody>
+        <tr>
+            <td>微博 连接</td>
+            <td>{:addon_url('third/index/connect',[':platform'=>'weibo'], false, true)}</td>
+        </tr>
+        <tr>
+            <td>微博 绑定</td>
+            <td>{:addon_url('third/index/bind',[':platform'=>'weibo'], false, true)}</td>
+        </tr>
+        <tr>
+            <td>微博 解绑</td>
+            <td>{:addon_url('third/index/unbind',[':platform'=>'weibo'], false, true)}</td>
+        </tr>
+        </tbody>
+    </table>
+</div>
+<!-- jQuery -->
+<script src="https://cdn.jsdelivr.net/npm/jquery@2.1.4/dist/jquery.min.js"></script>
+
+<!-- Bootstrap Core JavaScript -->
+<script src="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/js/bootstrap.min.js"></script>
+
+<script type="text/javascript">
+    $(function () {
+
+    });
+</script>
+</body>
+</html>

+ 66 - 0
application/admin/controller/Third.php

@@ -0,0 +1,66 @@
+<?php
+
+namespace app\admin\controller;
+
+use app\common\controller\Backend;
+
+/**
+ * 第三方登录管理
+ *
+ * @icon fa fa-circle-o
+ */
+class Third extends Backend
+{
+
+    /**
+     * Third模型对象
+     * @var \app\admin\model\Third
+     */
+    protected $model = null;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new \app\admin\model\Third;
+    }
+
+    /**
+     * 查看
+     */
+    public function index()
+    {
+        $this->relationSearch = true;
+        //设置过滤方法
+        $this->request->filter(['strip_tags']);
+        if ($this->request->isAjax()) {
+            //如果发送的来源是Selectpage,则转发到Selectpage
+            if ($this->request->request('keyField')) {
+                return $this->selectpage();
+            }
+            list($where, $sort, $order, $offset, $limit) = $this->buildparams();
+            $total = $this->model
+                ->with(['user'])
+                ->where($where)
+                ->order($sort, $order)
+                ->count();
+
+            $list = $this->model
+                ->with(['user'])
+                ->where($where)
+                ->order($sort, $order)
+                ->limit($offset, $limit)
+                ->select();
+            foreach ($list as $index => $item) {
+                if ($item->user) {
+                    $item->user->visible(['nickname']);
+                }
+            }
+            $list = collection($list)->toArray();
+            $result = array("total" => $total, "rows" => $list);
+
+            return json($result);
+        }
+        return $this->view->fetch();
+    }
+
+}

+ 16 - 0
application/admin/lang/zh-cn/third.php

@@ -0,0 +1,16 @@
+<?php
+
+return [
+    'Id'           => 'ID',
+    'User_id'      => '会员ID',
+    'Platform'     => '第三方应用',
+    'Unionid'      => '第三方UnionID',
+    'Openid'       => '第三方OpenID',
+    'Openname'     => '第三方会员昵称',
+    'Access_token' => 'AccessToken',
+    'Expires_in'   => '有效期',
+    'Createtime'   => '创建时间',
+    'Updatetime'   => '更新时间',
+    'Logintime'    => '登录时间',
+    'Expiretime'   => '过期时间'
+];

+ 56 - 0
application/admin/model/Third.php

@@ -0,0 +1,56 @@
+<?php
+
+namespace app\admin\model;
+
+use think\Model;
+
+class Third extends Model
+{
+
+
+    // 表名
+    protected $name = 'third';
+
+    // 自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    protected $deleteTime = false;
+
+    // 追加属性
+    protected $append = [
+        'logintime_text',
+        'expiretime_text'
+    ];
+
+
+    public function getLogintimeTextAttr($value, $data)
+    {
+        $value = $value ? $value : (isset($data['logintime']) ? $data['logintime'] : '');
+        return is_numeric($value) ? date("Y-m-d H:i:s", $value) : $value;
+    }
+
+
+    public function getExpiretimeTextAttr($value, $data)
+    {
+        $value = $value ? $value : (isset($data['expiretime']) ? $data['expiretime'] : '');
+        return is_numeric($value) ? date("Y-m-d H:i:s", $value) : $value;
+    }
+
+    protected function setLogintimeAttr($value)
+    {
+        return $value === '' ? null : ($value && !is_numeric($value) ? strtotime($value) : $value);
+    }
+
+    protected function setExpiretimeAttr($value)
+    {
+        return $value === '' ? null : ($value && !is_numeric($value) ? strtotime($value) : $value);
+    }
+
+    public function user()
+    {
+        return $this->belongsTo("User", 'user_id', 'id')->setEagerlyType(0);
+    }
+}

+ 26 - 0
application/admin/validate/Third.php

@@ -0,0 +1,26 @@
+<?php
+
+namespace app\admin\validate;
+
+use think\Validate;
+
+class Third extends Validate
+{
+    /**
+     * 验证规则
+     */
+    protected $rule = [
+    ];
+    /**
+     * 提示消息
+     */
+    protected $message = [
+    ];
+    /**
+     * 验证场景
+     */
+    protected $scene = [
+        'add'  => [],
+        'edit' => [],
+    ];
+}

+ 23 - 0
application/admin/view/third/index.html

@@ -0,0 +1,23 @@
+<div class="panel panel-default panel-intro">
+    {:build_heading()}
+
+    <div class="panel-body">
+        <div id="myTabContent" class="tab-content">
+            <div class="tab-pane fade active in" id="one">
+                <div class="widget-body no-padding">
+                    <div id="toolbar" class="toolbar">
+                        <a href="javascript:;" class="btn btn-primary btn-refresh" title="{:__('Refresh')}"><i class="fa fa-refresh"></i> </a>
+                        <a href="javascript:;" class="btn btn-danger btn-del btn-disabled disabled {:$auth->check('third/del')?'':'hide'}" title="{:__('Delete')}"><i class="fa fa-trash"></i> {:__('Delete')}</a>
+
+                    </div>
+                    <table id="table" class="table table-striped table-bordered table-hover table-nowrap"
+                           data-operate-edit=""
+                           data-operate-del="{:$auth->check('third/del')}"
+                           width="100%">
+                    </table>
+                </div>
+            </div>
+
+        </div>
+    </div>
+</div>

+ 6 - 0
application/extra/addons.php

@@ -19,6 +19,7 @@ return [
         ],
         'config_init' => [
             'qcloudsms',
+            'third',
         ],
         'sms_send' => [
             'qcloudsms',
@@ -37,6 +38,11 @@ return [
         '/example/d2/[:name]' => 'example/demo/demo2',
         '/qrcode$' => 'qrcode/index/index',
         '/qrcode/build$' => 'qrcode/index/build',
+        '/third$' => 'third/index/index',
+        '/third/connect/[:platform]' => 'third/index/connect',
+        '/third/callback/[:platform]' => 'third/index/callback',
+        '/third/bind/[:platform]' => 'third/index/bind',
+        '/third/unbind/[:platform]' => 'third/index/unbind',
     ],
     'priority' => [],
     'domain' => '',

+ 112 - 0
application/index/controller/Third.php

@@ -0,0 +1,112 @@
+<?php
+
+namespace app\index\controller;
+
+use addons\third\library\Application;
+use app\common\controller\Frontend;
+use think\Lang;
+use think\Session;
+
+/**
+ * 第三方登录控制器
+ */
+class Third extends Frontend
+{
+    protected $noNeedLogin = ['prepare'];
+    protected $noNeedRight = ['*'];
+    protected $app = null;
+    protected $options = [];
+    protected $layout = 'default';
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $config = get_addon_config('third');
+        $this->app = new Application($config);
+    }
+
+    /**
+     * 准备绑定
+     */
+    public function prepare()
+    {
+        $platform = $this->request->request('platform');
+        $url = $this->request->get('url', '/');
+        if ($this->auth->id) {
+            $this->redirect(url("index/third/bind") . "?" . http_build_query(['platform' => $platform, 'url' => $url]));
+        }
+
+        // 授权成功后的回调
+        $userinfo = Session::get("{$platform}-userinfo");
+        if (!$userinfo) {
+            $this->error("操作失败,请返回重度");
+        }
+
+        Lang::load([
+            APP_PATH . 'index' . DS . 'lang' . DS . $this->request->langset() . DS . 'user' . EXT,
+        ]);
+
+        $this->view->assign('userinfo', $userinfo['userinfo']);
+        $this->view->assign('platform', $platform);
+        $this->view->assign('url', $url);
+        $this->view->assign('bindurl', url("index/third/bind") . '?' . http_build_query(['platform' => $platform, 'url' => $url]));
+        $this->view->assign('captchaType', config('fastadmin.user_register_captcha'));
+        $this->view->assign('title', "账号绑定");
+
+        return $this->view->fetch();
+    }
+
+    /**
+     * 绑定账号
+     */
+    public function bind()
+    {
+        $platform = $this->request->request('platform');
+        $url = $this->request->get('url', $this->request->server('HTTP_REFERER'));
+        if (!$platform) {
+            $this->error("参数不正确");
+        }
+
+        // 授权成功后的回调
+        $userinfo = Session::get("{$platform}-userinfo");
+        if (!$userinfo) {
+            $this->redirect(addon_url('third/index/connect', [':platform' => $platform]) . '?url=' . urlencode($url));
+        }
+        $third = \addons\third\model\Third::where('user_id', $this->auth->id)->where('platform', $platform)->find();
+        if ($third) {
+            $this->error("已绑定账号,请勿重复绑定");
+        }
+        $time = time();
+        $values = [
+            'platform'      => $platform,
+            'user_id'       => $this->auth->id,
+            'openid'        => $userinfo['openid'],
+            'openname'      => isset($userinfo['userinfo']['nickname']) ? $userinfo['userinfo']['nickname'] : '',
+            'access_token'  => $userinfo['access_token'],
+            'refresh_token' => $userinfo['refresh_token'],
+            'expires_in'    => $userinfo['expires_in'],
+            'logintime'     => $time,
+            'expiretime'    => $time + $userinfo['expires_in'],
+        ];
+        $third = \addons\third\model\Third::create($values);
+        if ($third) {
+            $this->success("账号绑定成功", $url);
+        } else {
+            $this->error("账号绑定失败,请重试", $url);
+        }
+    }
+
+    /**
+     * 解绑账号
+     */
+    public function unbind()
+    {
+        $platform = $this->request->request('platform');
+        $third = \addons\third\model\Third::where('user_id', $this->auth->id)->where('platform', $platform)->find();
+        if (!$third) {
+            $this->error("未找到指定的账号绑定信息");
+        }
+        $third->delete();
+        $this->success("账号解绑成功");
+    }
+}

+ 128 - 0
application/index/view/third/prepare.html

@@ -0,0 +1,128 @@
+<div id="content-container" class="container">
+    <div class="text-center">
+        <img src="{$userinfo.avatar}" class="img-circle" width="80" height="80" alt=""/>
+        <div style="margin-top:15px;">{$userinfo.nickname|htmlentities}</div>
+    </div>
+    <div class="user-section login-section" style="margin-top:20px;">
+        <div class="logon-tab clearfix">
+            <a href="javascript:" data-type="bind" class="active">绑定已有账号</a>
+            <a href="javascript:" data-type="register">创建新账号</a>
+        </div>
+        <div class="bind-main login-main">
+            <form name="form" id="bind-form" class="form-vertical" method="POST" action="{:url('user/login')}">
+                {:token()}
+                <input type="hidden" name="platform" value="{$platform|htmlentities}"/>
+                <input type="hidden" name="url" value="{$url|htmlentities}"/>
+                <div class="form-group">
+                    <label class="control-label">{:__('Account')}</label>
+                    <div class="controls">
+                        <input type="text" id="account" name="account" data-rule="required" class="form-control input-lg" placeholder="{:__('Email/Mobile/Username')}">
+                        <p class="help-block"></p>
+                    </div>
+                </div>
+                <div class="form-group">
+                    <label class="control-label">{:__('Password')}</label>
+                    <div class="controls">
+                        <input type="password" id="password" name="password" data-rule="required;password" class="form-control input-lg" placeholder="{:__('Password must be 6 to 30 characters')}">
+                        <p class="help-block"></p>
+                    </div>
+                </div>
+                <div class="form-group">
+                    <button type="submit" class="btn btn-primary btn-block btn-lg">{:__('确认绑定')}</button>
+                </div>
+            </form>
+        </div>
+        <div class="register-main login-main hidden">
+            <form name="form" id="register-form" class="form-vertical" method="POST" action="{:url('user/register')}">
+                {:token()}
+                <input type="hidden" name="platform" value="{$platform|htmlentities}"/>
+                <input type="hidden" name="url" value="{$url|htmlentities}"/>
+                <div class="form-group">
+                    <label class="control-label">{:__('Email')}</label>
+                    <div class="controls">
+                        <input type="text" id="email" name="email" data-rule="required;email" class="form-control input-lg" placeholder="{:__('Email')}">
+                        <p class="help-block"></p>
+                    </div>
+                </div>
+                <div class="form-group">
+                    <label class="control-label">{:__('Username')}</label>
+                    <div class="controls">
+                        <input type="text" id="username" name="username" data-rule="required;length(3~30, true)" class="form-control input-lg" placeholder="{:__('Username must be 3 to 30 characters')}">
+                        <p class="help-block"></p>
+                    </div>
+                </div>
+                <div class="form-group">
+                    <label class="control-label">{:__('Password')}</label>
+                    <div class="controls">
+                        <input type="password" id="password" name="password" data-rule="required;password" class="form-control input-lg" placeholder="{:__('Password must be 6 to 30 characters')}">
+                        <p class="help-block"></p>
+                    </div>
+                </div>
+                <div class="form-group">
+                    <label class="control-label">{:__('Mobile')}</label>
+                    <div class="controls">
+                        <input type="text" id="mobile" name="mobile" data-rule="required;mobile" class="form-control input-lg" placeholder="{:__('Mobile')}">
+                        <p class="help-block"></p>
+                    </div>
+                </div>
+                {if $captchaType}
+                <div class="form-group">
+                    <label class="control-label">{:__('Captcha')}</label>
+                    <div class="controls">
+                        <div class="input-group">
+                            {include file="common/captcha" event="register" type="$captchaType" /}
+                        </div>
+                        <p class="help-block"></p>
+                    </div>
+                </div>
+                {/if}
+                <div class="form-group">
+                    <button type="submit" class="btn btn-primary btn-block btn-lg">{:__('创建账号并绑定')}</button>
+                </div>
+            </form>
+        </div>
+    </div>
+</div>
+
+<script>
+    require.callback = function () {
+        define('frontend/third', ['jquery', 'bootstrap', 'frontend', 'template', 'form'], function ($, undefined, Frontend, Template, Form) {
+            var Controller = {
+                prepare: function () {
+                    var validatoroptions = {
+                        invalid: function (form, errors) {
+                            $.each(errors, function (i, j) {
+                                Layer.msg(j);
+                            });
+                        }
+                    };
+
+                    $(".user-section .logon-tab > a").on("click", function () {
+                        $(".bind-main,.register-main").addClass("hidden");
+                        $("." + $(this).data("type") + "-main").removeClass("hidden");
+                        $(".user-section .logon-tab > a").removeClass("active");
+                        $(this).addClass("active");
+                    });
+
+                    //本地验证未通过时提示
+                    $("#register-form").data("validator-options", validatoroptions);
+
+                    //为表单绑定事件
+                    Form.api.bindevent($("#bind-form"), function (data, ret) {
+                        location.href = "{$bindurl}";
+                        return false;
+                    });
+
+                    //为表单绑定事件
+                    Form.api.bindevent($("#register-form"), function (data, ret) {
+                        location.href = "{$bindurl}";
+                        return false;
+                    }, function (data) {
+                        $("input[name=captcha]").next(".input-group-addon").find("img").trigger("click");
+                    });
+                }
+            };
+            return Controller;
+        });
+    }
+</script>

+ 14 - 0
public/assets/js/addons.js

@@ -386,4 +386,18 @@ require(['form', 'upload'], function (Form, Upload) {
     };
 });
 
+if (Config.modulename === 'index' && Config.controllername === 'user' && ['login', 'register'].indexOf(Config.actionname) > -1 && $("#register-form,#login-form").size() > 0) {
+    $('<style>.social-login{display:flex}.social-login a{flex:1;margin:0 2px;}.social-login a:first-child{margin-left:0;}.social-login a:last-child{margin-right:0;}</style>').appendTo("head");
+    $("#register-form,#login-form").append('<div class="form-group social-login"></div>');
+    if (Config.third.status.indexOf("wechat") > -1) {
+        $('<a class="btn btn-success" href="' + Fast.api.fixurl('/third/connect/wechat') + '"><i class="fa fa-wechat"></i> 微信登录</a>').appendTo(".social-login");
+    }
+    if (Config.third.status.indexOf("qq") > -1) {
+        $('<a class="btn btn-info" href="' + Fast.api.fixurl('/third/connect/qq') + '"><i class="fa fa-qq"></i> QQ登录</a>').appendTo(".social-login");
+    }
+    if (Config.third.status.indexOf("weibo") > -1) {
+        $('<a class="btn btn-danger" href="' + Fast.api.fixurl('/third/connect/weibo') + '"><i class="fa fa-weibo"></i> 微博登录</a>').appendTo(".social-login");
+    }
+}
+
 });

+ 58 - 0
public/assets/js/backend/third.js

@@ -0,0 +1,58 @@
+define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
+
+    var Controller = {
+        index: function () {
+            // 初始化表格参数配置
+            Table.api.init({
+                extend: {
+                    index_url: 'third/index' + location.search,
+                    add_url: 'third/add',
+                    edit_url: 'third/edit',
+                    del_url: 'third/del',
+                    multi_url: 'third/multi',
+                    table: 'third',
+                }
+            });
+
+            var table = $("#table");
+
+            // 初始化表格
+            table.bootstrapTable({
+                url: $.fn.bootstrapTable.defaults.extend.index_url,
+                pk: 'id',
+                sortName: 'id',
+                columns: [
+                    [
+                        {checkbox: true},
+                        {field: 'id', title: __('Id')},
+                        {field: 'user_id', title: __('User_id'), formatter: Table.api.formatter.search},
+                        {field: 'user.nickname', title: __('Nickname')},
+                        {field: 'platform', title: __('Platform'), formatter: Table.api.formatter.search},
+                        {field: 'unionid', title: __('Unionid')},
+                        {field: 'openid', title: __('Openid')},
+                        {field: 'openname', title: __('Openname')},
+                        {field: 'createtime', title: __('Createtime'), operate: 'RANGE', addclass: 'datetimerange', formatter: Table.api.formatter.datetime},
+                        {field: 'updatetime', title: __('Updatetime'), operate: 'RANGE', addclass: 'datetimerange', formatter: Table.api.formatter.datetime},
+                        {field: 'logintime', title: __('Logintime'), operate: 'RANGE', addclass: 'datetimerange', formatter: Table.api.formatter.datetime},
+                        {field: 'operate', title: __('Operate'), table: table, events: Table.api.events.operate, formatter: Table.api.formatter.operate}
+                    ]
+                ]
+            });
+
+            // 为表格绑定事件
+            Table.api.bindevent(table);
+        },
+        add: function () {
+            Controller.api.bindevent();
+        },
+        edit: function () {
+            Controller.api.bindevent();
+        },
+        api: {
+            bindevent: function () {
+                Form.api.bindevent($("form[role=form]"));
+            }
+        }
+    };
+    return Controller;
+});