微信網頁授權分為 snsapi_base(靜默獲取 OpenID)和 snsapi_userinfo(彈窗授權獲取詳細信息)。本文實現一個靜默授權自動登錄流程:用戶訪問頁面 → 判斷是否微信瀏覽器 → 未登錄則跳轉微信授權 → 獲取 Code 換取 OpenID → 查詢本地用戶表 → 不存在則創建用戶(可選獲取 UnionID/詳細信息) → 設置 Session/Cookie 完成登錄。
用戶訪問頁面
│
▼
是否微信瀏覽器? ──否──► 正常頁面邏輯
│是
▼
是否已登錄(Session/Cookie)? ──是──► 正常頁面邏輯
│否
▼
URL 中有 code 參數? ──否──► 重定向到微信授權頁(snsapi_base)
│是
▼
用 code 換取 access_token + openid
│
▼
查詢本地用戶表 (openid/unionid)
│
├─ 存在 ──► 設置登錄態,跳轉原頁
│
└─ 不存在
│
▼
是否需要詳細信息? ──否──► 創建用戶(隨機昵稱),設置登錄態
│是
▼
用 access_token 獲取用戶詳細信息(需 snsapi_userinfo)
│
▼
創建用戶(微信昵稱/頭像/性別/地區),設置登錄態
│
▼
跳轉原頁
<?php
/**
* 微信靜默授權自動登錄服務
* 依賴:PDO 數據庫連接、Session、配置文件
*/
class WechatAutoLogin {
private $pdo;
private $config;
public function __construct(PDO $pdo, array $config) {
$this->pdo = $pdo;
$this->config = $config;
}
/**
* 入口方法:在控制器基類或中間件中調用
* @return array|null 當前登錄用戶信息,未登錄且非微信環境返回 null
*/
public function handle(): ?array {
// 1. 非微信環境直接返回
if (!$this->isWechat()) {
return $this->getCurrentUser();
}
// 2. 已登錄直接返回
$user = $this->getCurrentUser();
if ($user) {
return $user;
}
// 3. 處理授權回調
if (isset($_GET['code'])) {
return $this->handleCallback();
}
// 4. 發起靜默授權跳轉
$this->redirectToAuth();
exit; // 重定向後必須退出
}
/**
* 判斷是否微信瀏覽器
*/
private function isWechat(): bool {
return strpos($_SERVER['HTTP_USER_AGENT'] ?? '', 'MicroMessenger') !== false;
}
/**
* 獲取當前登錄用戶(Session)
*/
private function getCurrentUser(): ?array {
if (session_status() === PHP_SESSION_NONE) session_start();
return $_SESSION['wechat_user'] ?? null;
}
/**
* 設置登錄態
*/
private function setLoginUser(array $user): void {
if (session_status() === PHP_SESSION_NONE) session_start();
$_SESSION['wechat_user'] = $user;
// 可選:設置長期 Cookie
setcookie('wechat_uid', $user['id'], time() + 86400 * 30, '/', '', true, true);
}
/**
* 重定向到微信授權頁
*/
private function redirectToAuth(): void {
$redirectUri = urlencode($this->getCurrentUrl());
$scope = 'snsapi_base'; // 靜默授權,不彈窗
$state = 'AUTH_' . time() . '_' . random_int(1000, 9999);
// 可選:將 state 存入 Session 防 CSRF
if (session_status() === PHP_SESSION_NONE) session_start();
$_SESSION['wechat_oauth_state'] = $state;
$url = "https://open.weixin.qq.com/connect/oauth2/authorize"
. "?appid={$this->config['appid']}"
. "&redirect_uri={$redirectUri}"
. "&response_type=code"
. "&scope={$scope}"
. "&state={$state}"
. "#wechat_redirect";
header("Location: {$url}");
}
/**
* 獲取當前完整 URL
*/
private function getCurrentUrl(): string {
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
return $protocol . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
}
/**
* 處理授權回調
*/
private function handleCallback(): ?array {
$code = $_GET['code'] ?? '';
$state = $_GET['state'] ?? '';
// 可選:驗證 state 防 CSRF
if (session_status() === PHP_SESSION_NONE) session_start();
if (($state !== $_SESSION['wechat_oauth_state'] ?? '')) {
error_log('WeChat OAuth state mismatch');
// 可選:重新授權
}
unset($_SESSION['wechat_oauth_state']);
if (empty($code)) {
error_log('WeChat OAuth missing code');
return null;
}
// 1. 用 code 換取 access_token + openid
$tokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token"
. "?appid={$this->config['appid']}"
. "&secret={$this->config['appsecret']}"
. "&code={$code}"
. "&grant_type=authorization_code";
$tokenRes = $this->httpGet($tokenUrl);
$tokenData = json_decode($tokenRes, true);
if (isset($tokenData['errcode'])) {
error_log("WeChat OAuth token error: {$tokenData['errcode']} - {$tokenData['errmsg']}");
return null;
}
$accessToken = $tokenData['access_token'];
$openid = $tokenData['openid'];
$unionid = $tokenData['unionid'] ?? null; // 僅綁定開放平台時返回
// 2. 查詢本地用戶
$user = $this->findUserByOpenid($openid);
if ($user) {
$this->setLoginUser($user);
return $user;
}
// 3. 用戶不存在,嘗試獲取詳細信息(需 snsapi_userinfo)
// 靜默授權無法獲取詳細信息,這裡有兩種策略:
// 策略 A:直接創建用戶(隨機昵稱),後續引導用戶完善信息
// 策略 B:重定向到 snsapi_userinfo 授權頁獲取詳細信息
// 這裡演示策略 A:創建用戶
$user = $this->createUser($openid, $unionid, $accessToken);
$this->setLoginUser($user);
return $user;
}
/**
* 查詢用戶(支持 openid 或 unionid)
*/
private function findUserByOpenid(string $openid): ?array {
$sql = "SELECT * FROM `users` WHERE `wechat_openid` = ? OR `wechat_unionid` = ? LIMIT 1";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([$openid, $openid]); // unionid 暫用 openid 佔位
return $stmt->fetch() ?: null;
}
/**
* 創建新用戶
*/
private function createUser(string $openid, ?string $unionid, string $accessToken): array {
// 嘗試獲取詳細信息(如果有 unionid 或配置了 snsapi_userinfo)
$nickname = '微信用戶_' . substr($openid, -6);
$headimg = '';
$sex = 0;
$province = '';
$city = '';
// 如果有 unionid,說明綁定了開放平台,可嘗試獲取信息
// 或者如果業務強制需要詳細信息,這裡應重定向到 snsapi_userinfo 授權
if ($unionid) {
$info = $this->getUserInfo($accessToken, $openid);
if ($info) {
$nickname = $info['nickname'] ?? $nickname;
$headimg = $info['headimgurl'] ?? '';
$sex = $info['sex'] ?? 0;
$province = $info['province'] ?? '';
$city = $info['city'] ?? '';
}
}
$password = password_hash(random_bytes(16), PASSWORD_DEFAULT);
$sql = "INSERT INTO `users`
(`wechat_openid`, `wechat_unionid`, `nickname`, `avatar`, `sex`, `province`, `city`, `password`, `created_at`)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW())";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([$openid, $unionid, $nickname, $headimg, $sex, $province, $city, $password]);
$userId = $this->pdo->lastInsertId();
return [
'id' => $userId,
'wechat_openid'=> $openid,
'wechat_unionid'=> $unionid,
'nickname' => $nickname,
'avatar' => $headimg,
'sex' => $sex,
'province' => $province,
'city' => $city,
];
}
/**
* 獲取用戶詳細信息(需 snsapi_userinfo 授權)
*/
private function getUserInfo(string $accessToken, string $openid): ?array {
$url = "https://api.weixin.qq.com/sns/userinfo"
. "?access_token={$accessToken}"
. "&openid={$openid}"
. "&lang=zh_CN";
$res = $this->httpGet($url);
$data = json_decode($res, true);
return isset($data['errcode']) ? null : $data;
}
/**
* 簡易 HTTP GET
*/
private function httpGet(string $url): string {
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
]);
$response = curl_exec($ch);
curl_close($ch);
return $response;
}
}
<?php
// BaseController.php
class BaseController extends Controller {
protected $wechatUser = null;
protected function initialize() {
$pdo = new PDO("mysql:host=localhost;dbname=app;charset=utf8mb4", 'user', 'pass');
$config = [
'appid' => 'YOUR_APPID',
'appsecret' => 'YOUR_APPSECRET',
];
$login = new WechatAutoLogin($pdo, $config);
$this->wechatUser = $login->handle();
// 模板全局變量
$this->assign('wechatUser', $this->wechatUser);
}
}
// IndexController.php
class IndexController extends BaseController {
public function index() {
if ($this->wechatUser) {
return "歡迎,{$this->wechatUser['nickname']} (ID: {$this->wechatUser['id']})";
}
return '非微信環境或未登錄';
}
}
| 問題 | 原因 | 解決 |
|---|---|---|
| 未關注公眾號無法獲取昵稱 | 靜默授權(snsapi_base)僅返回 openid,需關注後才能通過用戶信息接口獲取詳細信息 | 策略 A:創建用戶時用隨機昵稱,引導用戶關注後更新;策略 B:強制使用 snsapi_userinfo 授權(會彈窗) |
| 授權循環跳轉 | code 使用後失效,刷新頁面再次跳轉授權 | 登錄成功後重定向到不帶 code 的乾淨 URL(header('Location: ' . $cleanUrl)) |
| 多域名/多入口共享登錄態 | Session 默認同域名,跨域名需共享 Session 或用 JWT | 同主域名設置 session.cookie_domain = .example.com;異域名用 Redis 共享 Session 或 JWT Token |
| UnionID 為空 | 公眾號未綁定微信開放平台賬號 | 在開放平台綁定公眾號,綁定後新關注用戶會返回 unionid |
| 獲取用戶信息失敗 40003 | access_token 無效/過期/不屬於該 AppID | 確保 access_token 來自同一 AppID 的授權流程,不可混用 |
系統學習:PHP 微信開發技巧匯總、PHP OAuth2.0 實戰、PHP Session 與分布式登錄 等專題。