PHP 第三方登录绑定手机

wen PHP项目 5

本文目录导读:

PHP 第三方登录绑定手机

  1. 数据库设计
  2. 用户类(User.php)
  3. OAuth用户类(OAuthUser.php)
  4. 微信登录类(WechatAuth.php)
  5. 短信验证码类(SmsService.php)
  6. 绑定手机号控制器(BindPhoneController.php)
  7. 处理微信登录回调(CallbackController.php)
  8. 前端绑定页面(bind_phone.php)
  9. 路由配置
  10. 安全建议

我来帮你实现PHP第三方登录绑定手机号的完整方案,这需要集成第三方登录(如微信、QQ、GitHub等)和手机号绑定功能。

数据库设计

-- 用户表
CREATE TABLE `users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `phone` varchar(20) DEFAULT NULL COMMENT '手机号',
  `password` varchar(255) DEFAULT NULL COMMENT '密码',
  `nickname` varchar(50) DEFAULT NULL,
  `avatar` varchar(255) DEFAULT NULL,
  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_phone` (`phone`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 第三方登录用户表
CREATE TABLE `oauth_users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) NOT NULL COMMENT '关联用户ID',
  `provider` varchar(20) NOT NULL COMMENT '第三方平台: wechat, qq, github等',
  `openid` varchar(100) NOT NULL COMMENT '第三方平台唯一标识',
  `unionid` varchar(100) DEFAULT NULL COMMENT '微信unionid',
  `nickname` varchar(50) DEFAULT NULL,
  `avatar` varchar(255) DEFAULT NULL,
  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_provider_openid` (`provider`, `openid`),
  KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

用户类(User.php)

<?php
class User {
    private $db;
    public function __construct($db) {
        $this->db = $db;
    }
    /**
     * 通过手机号查找用户
     */
    public function findByPhone($phone) {
        $stmt = $this->db->prepare("SELECT * FROM users WHERE phone = ?");
        $stmt->execute([$phone]);
        return $stmt->fetch(PDO::FETCH_ASSOC);
    }
    /**
     * 创建用户
     */
    public function create($data) {
        $sql = "INSERT INTO users (phone, password, nickname, avatar) 
                VALUES (?, ?, ?, ?)";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([
            $data['phone'] ?? null,
            $data['password'] ?? null,
            $data['nickname'] ?? null,
            $data['avatar'] ?? null
        ]);
        return $this->db->lastInsertId();
    }
    /**
     * 更新用户手机号
     */
    public function updatePhone($userId, $phone) {
        $stmt = $this->db->prepare("UPDATE users SET phone = ? WHERE id = ?");
        return $stmt->execute([$phone, $userId]);
    }
    /**
     * 检查手机号是否已绑定其他账号
     */
    public function phoneExists($phone, $excludeUserId = null) {
        $sql = "SELECT id FROM users WHERE phone = ?";
        $params = [$phone];
        if ($excludeUserId) {
            $sql .= " AND id != ?";
            $params[] = $excludeUserId;
        }
        $stmt = $this->db->prepare($sql);
        $stmt->execute($params);
        return $stmt->fetch() !== false;
    }
}

OAuth用户类(OAuthUser.php)

<?php
class OAuthUser {
    private $db;
    private $userModel;
    public function __construct($db) {
        $this->db = $db;
        $this->userModel = new User($db);
    }
    /**
     * 通过OAuth信息查找用户
     */
    public function findByProvider($provider, $openid) {
        $stmt = $this->db->prepare(
            "SELECT ou.*, u.phone, u.nickname as user_nickname 
             FROM oauth_users ou 
             LEFT JOIN users u ON ou.user_id = u.id 
             WHERE ou.provider = ? AND ou.openid = ?"
        );
        $stmt->execute([$provider, $openid]);
        return $stmt->fetch(PDO::FETCH_ASSOC);
    }
    /**
     * 创建OAuth关联
     */
    public function create($userId, $provider, $openid, $unionid = null, $nickname = null, $avatar = null) {
        $stmt = $this->db->prepare(
            "INSERT INTO oauth_users (user_id, provider, openid, unionid, nickname, avatar) 
             VALUES (?, ?, ?, ?, ?, ?)"
        );
        return $stmt->execute([
            $userId, $provider, $openid, $unionid, $nickname, $avatar
        ]);
    }
    /**
     * 通过手机号查找OAuth用户
     */
    public function findByPhone($phone) {
        $stmt = $this->db->prepare(
            "SELECT ou.* FROM oauth_users ou 
             LEFT JOIN users u ON ou.user_id = u.id 
             WHERE u.phone = ?"
        );
        $stmt->execute([$phone]);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
    /**
     * 更新OAuth用户信息
     */
    public function updateOAuthInfo($id, $nickname, $avatar) {
        $stmt = $this->db->prepare(
            "UPDATE oauth_users SET nickname = ?, avatar = ? WHERE id = ?"
        );
        return $stmt->execute([$nickname, $avatar, $id]);
    }
}

微信登录类(WechatAuth.php)

<?php
class WechatAuth {
    private $appId;
    private $appSecret;
    private $redirectUri;
    public function __construct($appId, $appSecret, $redirectUri) {
        $this->appId = $appId;
        $this->appSecret = $appSecret;
        $this->redirectUri = $redirectUri;
    }
    /**
     * 获取授权URL
     */
    public function getAuthUrl($state = '') {
        $params = [
            'appid' => $this->appId,
            'redirect_uri' => $this->redirectUri,
            'response_type' => 'code',
            'scope' => 'snsapi_login',
            'state' => $state ?: uniqid()
        ];
        return 'https://open.weixin.qq.com/connect/qrconnect?' . http_build_query($params);
    }
    /**
     * 通过code获取access_token
     */
    public function getAccessToken($code) {
        $params = [
            'appid' => $this->appId,
            'secret' => $this->appSecret,
            'code' => $code,
            'grant_type' => 'authorization_code'
        ];
        $url = 'https://api.weixin.qq.com/sns/oauth2/access_token?' . http_build_query($params);
        $response = $this->httpGet($url);
        return json_decode($response, true);
    }
    /**
     * 获取用户信息
     */
    public function getUserInfo($accessToken, $openid) {
        $params = [
            'access_token' => $accessToken,
            'openid' => $openid
        ];
        $url = 'https://api.weixin.qq.com/sns/userinfo?' . http_build_query($params);
        $response = $this->httpGet($url);
        return json_decode($response, true);
    }
    private function httpGet($url) {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
        $response = curl_exec($ch);
        curl_close($ch);
        return $response;
    }
}

短信验证码类(SmsService.php)

<?php
class SmsService {
    private $db;
    public function __construct($db) {
        $this->db = $db;
    }
    /**
     * 生成并保存验证码
     */
    public function sendVerifyCode($phone) {
        // 生成6位随机验证码
        $code = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT);
        // 验证码10分钟内有效
        $expiresAt = date('Y-m-d H:i:s', time() + 600);
        // 保存到数据库
        $stmt = $this->db->prepare(
            "INSERT INTO sms_codes (phone, code, expires_at) VALUES (?, ?, ?)"
        );
        $stmt->execute([$phone, $code, $expiresAt]);
        // TODO: 调用短信API发送验证码
        // $this->sendSms($phone, $code);
        // 开发环境直接返回验证码
        return $code;
    }
    /**
     * 验证验证码
     */
    public function verifyPhoneCode($phone, $code) {
        $stmt = $this->db->prepare(
            "SELECT * FROM sms_codes 
             WHERE phone = ? AND code = ? AND expires_at > NOW() 
             ORDER BY id DESC LIMIT 1"
        );
        $stmt->execute([$phone, $code]);
        $result = $stmt->fetch(PDO::FETCH_ASSOC);
        if ($result) {
            // 验证成功后删除验证码
            $deleteStmt = $this->db->prepare("DELETE FROM sms_codes WHERE id = ?");
            $deleteStmt->execute([$result['id']]);
            return true;
        }
        return false;
    }
}

绑定手机号控制器(BindPhoneController.php)

<?php
class BindPhoneController {
    private $db;
    private $userModel;
    private $oauthUserModel;
    private $smsService;
    public function __construct($db) {
        $this->db = $db;
        $this->userModel = new User($db);
        $this->oauthUserModel = new OAuthUser($db);
        $this->smsService = new SmsService($db);
        session_start();
    }
    /**
     * 发送验证码
     */
    public function sendVerifyCode() {
        header('Content-Type: application/json');
        $phone = $_POST['phone'] ?? '';
        // 验证手机号格式
        if (!preg_match('/^1[3-9]\d{9}$/', $phone)) {
            echo json_encode(['code' => 400, 'msg' => '手机号格式不正确']);
            return;
        }
        // 生成并发送验证码
        $code = $this->smsService->sendVerifyCode($phone);
        echo json_encode([
            'code' => 200, 
            'msg' => '验证码已发送',
            'debug_code' => $code // 开发环境返回,生产环境删除
        ]);
    }
    /**
     * 绑定手机号
     */
    public function bindPhone() {
        header('Content-Type: application/json');
        // 检查用户登录状态
        if (!isset($_SESSION['oauth_user'])) {
            echo json_encode(['code' => 401, 'msg' => '未登录']);
            return;
        }
        $phone = $_POST['phone'] ?? '';
        $code = $_POST['code'] ?? '';
        // 验证手机号格式
        if (!preg_match('/^1[3-9]\d{9}$/', $phone)) {
            echo json_encode(['code' => 400, 'msg' => '手机号格式不正确']);
            return;
        }
        // 验证验证码
        if (!$this->smsService->verifyPhoneCode($phone, $code)) {
            echo json_encode(['code' => 400, 'msg' => '验证码错误或已过期']);
            return;
        }
        $oauthUser = $_SESSION['oauth_user'];
        // 检查手机号是否已被绑定
        if ($this->userModel->phoneExists($phone)) {
            // 手机号已存在,询问是否绑定到已有账号
            echo json_encode([
                'code' => 409,
                'msg' => '该手机号已绑定其他账号,是否需要合并?'
            ]);
            return;
        }
        // 开始事务
        $this->db->beginTransaction();
        try {
            // 创建新用户
            $userId = $this->userModel->create([
                'phone' => $phone,
                'nickname' => $oauthUser['nickname'],
                'avatar' => $oauthUser['avatar']
            ]);
            // 创建OAuth关联
            $this->oauthUserModel->create(
                $userId,
                $oauthUser['provider'],
                $oauthUser['openid'],
                $oauthUser['unionid'] ?? null,
                $oauthUser['nickname'],
                $oauthUser['avatar']
            );
            $this->db->commit();
            // 更新会话
            $_SESSION['user_id'] = $userId;
            $_SESSION['phone'] = $phone;
            unset($_SESSION['oauth_user']);
            echo json_encode([
                'code' => 200,
                'msg' => '绑定成功',
                'data' => ['user_id' => $userId]
            ]);
        } catch (Exception $e) {
            $this->db->rollBack();
            echo json_encode(['code' => 500, 'msg' => '绑定失败,请重试']);
        }
    }
    /**
     * 处理账号合并
     */
    public function mergeAccount() {
        header('Content-Type: application/json');
        $phone = $_POST['phone'] ?? '';
        $code = $_POST['code'] ?? '';
        $action = $_POST['action'] ?? ''; // bind: 绑定到已有账号, new: 创建新账号
        // 验证验证码
        if (!$this->smsService->verifyPhoneCode($phone, $code)) {
            echo json_encode(['code' => 400, 'msg' => '验证码错误或已过期']);
            return;
        }
        $oauthUser = $_SESSION['oauth_user'];
        $targetUser = $this->userModel->findByPhone($phone);
        if (!$targetUser && $action !== 'new') {
            echo json_encode(['code' => 400, 'msg' => '用户不存在']);
            return;
        }
        if ($action === 'new') {
            // 创建新账号
            $userId = $this->userModel->create([
                'phone' => $phone,
                'nickname' => $oauthUser['nickname']
            ]);
        } else {
            // 绑定到已有账号
            $userId = $targetUser['id'];
        }
        // 检查是否已存在关联
        $existing = $this->oauthUserModel->findByProvider(
            $oauthUser['provider'], 
            $oauthUser['openid']
        );
        if ($existing) {
            // 更新关联的用户ID
            $stmt = $this->db->prepare(
                "UPDATE oauth_users SET user_id = ? WHERE id = ?"
            );
            $stmt->execute([$userId, $existing['id']]);
        } else {
            // 创建新的关联
            $this->oauthUserModel->create(
                $userId,
                $oauthUser['provider'],
                $oauthUser['openid'],
                $oauthUser['unionid'] ?? null
            );
        }
        // 更新会话
        $_SESSION['user_id'] = $userId;
        $_SESSION['phone'] = $phone;
        unset($_SESSION['oauth_user']);
        echo json_encode([
            'code' => 200,
            'msg' => '操作成功',
            'data' => ['user_id' => $userId]
        ]);
    }
}

处理微信登录回调(CallbackController.php)

<?php
class CallbackController {
    private $db;
    private $wechatAuth;
    private $oauthUserModel;
    private $userModel;
    public function __construct($db, $config) {
        $this->db = $db;
        $this->wechatAuth = new WechatAuth(
            $config['app_id'],
            $config['app_secret'],
            $config['redirect_uri']
        );
        $this->oauthUserModel = new OAuthUser($db);
        $this->userModel = new User($db);
        session_start();
    }
    /**
     * 处理微信回调
     */
    public function wechatCallback() {
        $code = $_GET['code'] ?? '';
        if (empty($code)) {
            // 用户取消了授权
            header('Location: /login.php?error=access_denied');
            exit;
        }
        // 获取access_token
        $tokenData = $this->wechatAuth->getAccessToken($code);
        if (isset($tokenData['errcode'])) {
            header('Location: /login.php?error=' . $tokenData['errcode']);
            exit;
        }
        // 获取用户信息
        $userInfo = $this->wechatAuth->getUserInfo(
            $tokenData['access_token'],
            $tokenData['openid']
        );
        // 查找是否已有关联
        $oauthUser = $this->oauthUserModel->findByProvider('wechat', $userInfo['openid']);
        if ($oauthUser) {
            // 已有绑定,直接登录
            $_SESSION['user_id'] = $oauthUser['user_id'];
            $_SESSION['phone'] = $oauthUser['phone'];
            header('Location: /index.php');
            exit;
        } else {
            // 未绑定,保存OAuth信息到会话,跳转到绑定页面
            $_SESSION['oauth_user'] = [
                'provider' => 'wechat',
                'openid' => $userInfo['openid'],
                'unionid' => $tokenData['unionid'] ?? null,
                'nickname' => $userInfo['nickname'] ?? '',
                'avatar' => $userInfo['headimgurl'] ?? ''
            ];
            // 跳转到绑定手机号页面
            if ($this->isNewUser($userInfo['openid'])) {
                header('Location: /bind_phone.php');
            } else {
                header('Location: /bind_phone.php?action=merge');
            }
            exit;
        }
    }
    private function isNewUser($openid) {
        // 处理新用户逻辑
        return true;
    }
}

前端绑定页面(bind_phone.php)

<!DOCTYPE html>
<html>
<head>绑定手机号</title>
    <style>
        .container { max-width: 400px; margin: 50px auto; padding: 20px; }
        .form-group { margin-bottom: 15px; }
        label { display: block; margin-bottom: 5px; }
        input { width: 100%; padding: 8px; }
        .btn { padding: 8px 15px; background: #4CAF50; color: white; border: none; }
        .error { color: red; }
        .success { color: green; }
    </style>
</head>
<body>
    <div class="container">
        <h2>绑定手机号</h2>
        <div id="message"></div>
        <form id="bindForm">
            <div class="form-group">
                <label>手机号</label>
                <input type="text" id="phone" placeholder="请输入手机号" maxlength="11">
            </div>
            <div class="form-group">
                <label>验证码</label>
                <input type="text" id="code" placeholder="请输入验证码" maxlength="6">
            </div>
            <div class="form-group">
                <button type="button" id="sendCode">发送验证码</button>
                <button type="submit" class="btn">绑定手机</button>
            </div>
        </form>
    </div>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <script>
        $(document).ready(function() {
            // 发送验证码
            $('#sendCode').click(function() {
                var phone = $('#phone').val();
                // 手机号验证
                if (!/^1[3-9]\d{9}$/.test(phone)) {
                    showMessage('请输入正确的手机号', 'error');
                    return;
                }
                $.post('/api/send_verify_code.php', {
                    phone: phone
                }, function(response) {
                    response = JSON.parse(response);
                    showMessage(response.msg, response.code === 200 ? 'success' : 'error');
                    if (response.code === 200) {
                        // 开始倒计时
                        var countdown = 60;
                        var $btn = $('#sendCode');
                        $btn.prop('disabled', true);
                        var timer = setInterval(function() {
                            countdown--;
                            $btn.text(countdown + '秒后重发');
                            if (countdown === 0) {
                                clearInterval(timer);
                                $btn.text('发送验证码');
                                $btn.prop('disabled', false);
                            }
                        }, 1000);
                    }
                });
            });
            // 提交绑定
            $('#bindForm').submit(function(e) {
                e.preventDefault();
                var phone = $('#phone').val();
                var code = $('#code').val();
                if (!phone || !code) {
                    showMessage('请填写完整信息', 'error');
                    return;
                }
                $.post('/api/bind_phone.php', {
                    phone: phone,
                    code: code
                }, function(response) {
                    response = JSON.parse(response);
                    if (response.code === 200) {
                        showMessage('绑定成功,正在跳转...', 'success');
                        setTimeout(function() {
                            window.location.href = '/index.php';
                        }, 2000);
                    } else if (response.code === 409) {
                        // 手机号已被绑定
                        if (confirm('该手机号已绑定其他账号,是否合并?')) {
                            $.post('/api/merge_account.php', {
                                phone: phone,
                                code: code,
                                action: 'bind'
                            }, function(mergeResponse) {
                                mergeResponse = JSON.parse(mergeResponse);
                                if (mergeResponse.code === 200) {
                                    showMessage('合并成功,正在跳转...', 'success');
                                    setTimeout(function() {
                                        window.location.href = '/index.php';
                                    }, 2000);
                                }
                            });
                        }
                    } else {
                        showMessage(response.msg, 'error');
                    }
                });
            });
            // 显示消息
            function showMessage(msg, type) {
                $('#message').attr('class', type).text(msg);
            }
        });
    </script>
</body>
</html>

路由配置

// router.php
<?php
$routes = [
    '/api/send_verify_code.php' => 'sendVerifyCode',
    '/api/bind_phone.php' => 'bindPhone',
    '/api/merge_account.php' => 'mergeAccount',
    '/callback/wechat.php' => 'wechatCallback'
];
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
if (isset($routes[$path])) {
    // 初始化数据库连接
    $db = new PDO('mysql:host=localhost;dbname=test', 'root', '');
    $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    // 配置信息
    $config = [
        'app_id' => '你的AppID',
        'app_secret' => '你的AppSecret',
        'redirect_uri' => 'http://yourdomain.com/callback/wechat.php'
    ];
    if ($path === '/callback/wechat.php') {
        $controller = new CallbackController($db, $config);
    } else {
        $controller = new BindPhoneController($db);
    }
    $action = $routes[$path];
    $controller->$action();
} else {
    http_response_code(404);
    echo 'Not Found';
}

安全建议

  1. 验证码安全

    • 限制发送频率
    • 验证码有效期设置
    • 防止暴力破解
  2. 用户隐私

    • 手机号加密存储
    • 敏感信息脱敏显示
  3. 防刷机制

    • IP限制
    • 设备指纹
    • 图形验证码

这个完整的实现包括了:

  • 第三方登录获取用户信息
  • 手机号验证码验证
  • 账号绑定和合并逻辑
  • 完整的前后端交互
  • 安全措施

使用时需要替换为你的实际配置(数据库连接、微信应用配置等),对于生产环境,建议加入更多的安全机制和异常处理。

抱歉,评论功能暂时关闭!