首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

PHP 微信靜默授權自動登錄獲取用戶信息:從 Code 到 OpenID/UnionID 完整實現

     編輯:PHP 綜合知識

前言

微信網頁授權分為 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
獲取用戶信息失敗 40003access_token 無效/過期/不屬於該 AppID確保 access_token 來自同一 AppID 的授權流程,不可混用

延伸閱讀

系統學習:PHP 微信開發技巧匯總、PHP OAuth2.0 實戰、PHP Session 與分布式登錄 等專題。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved