本文目录导读:

- 数据库设计
- 用户类(User.php)
- OAuth用户类(OAuthUser.php)
- 微信登录类(WechatAuth.php)
- 短信验证码类(SmsService.php)
- 绑定手机号控制器(BindPhoneController.php)
- 处理微信登录回调(CallbackController.php)
- 前端绑定页面(bind_phone.php)
- 路由配置
- 安全建议
我来帮你实现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';
}
安全建议
-
验证码安全:
- 限制发送频率
- 验证码有效期设置
- 防止暴力破解
-
用户隐私:
- 手机号加密存储
- 敏感信息脱敏显示
-
防刷机制:
- IP限制
- 设备指纹
- 图形验证码
这个完整的实现包括了:
- 第三方登录获取用户信息
- 手机号验证码验证
- 账号绑定和合并逻辑
- 完整的前后端交互
- 安全措施
使用时需要替换为你的实际配置(数据库连接、微信应用配置等),对于生产环境,建议加入更多的安全机制和异常处理。