PHP DEMO

PHP 扫码登录 Demo

一套开箱即用的 PHP 接入代码:服务端申请二维码参数,页面内嵌微信官方二维码, 扫码确认后在顶层完成 state 校验、ticket 换票与 Session 写入。无 Composer 依赖,上传即跑。

PHP 7.4+ 无第三方依赖 完整可运行源码 PHP 服务端示例 Markdown 版(供 AI 读取)

1. 下载与运行

Demo 包含 7 个 PHP 文件与一份样式表,覆盖「申请二维码 → 扫码回调 → 换 ticket → 写 Session」 的完整链路,可直接作为业务站的登录模块基础。

下载 wx-login-php-demo.zip

运行环境:PHP 7.4+(推荐 8.0+),需要 curl、json、 session、mbstring 扩展;Nginx / Apache / 宝塔面板均可,无需 Composer。
部署步骤
# 1. 解压到站点目录(或子目录)
unzip wx-login-php-demo.zip -d /www/wwwroot/你的站点/

# 2. 编辑 config.php,填入「我的项目」中的凭证
#    gateway / client_id / client_secret / redirect_url

# 3. 确认 redirect_url 的域名已在项目域名白名单内
# 4. 浏览器访问
https://你的站点/index.php
目录说明: index.php 首页与主流程、qr.php 申请 / 刷新二维码、 callback.php 回调桥接、login.php 整页跳转、 logout.php 退出、lib.php 公共函数、config.php 配置。

2. 配置项

全部配置集中在 config.php,返回一个数组:

config.php
<?php
return [
    'gateway'       => 'https://login.fps5.cn',
    'client_id'     => 'ak_你的client_id',
    'client_secret' => 'sk_你的client_secret',
    'redirect_url'  => 'https://你的站点/callback.php',
];
配置项必填说明
gateway 是 登录网关地址,默认 https://login.fps5.cn。
client_id 是 项目 client_id,在「我的项目」中查看。
client_secret 是 项目 client_secret,只用于服务端换取用户资料,不会出现在前端。
redirect_url 是 微信授权后回跳的地址,域名必须在项目白名单内,通常填 https://你的站点/callback.php。
关于 redirect_url: 网关只校验域名,不要求路径完全一致。但本 Demo 依赖 callback.php 完成桥接, 因此路径请指向它,不要直接指到 index.php。

3. 登录链路

整个流程有两类 state,不要混用:业务 state 由本站生成并存入 Session;网关 state 由 /oauth/qr 返回,只用于网关与微信之间的校验。

01
申请二维码参数

GET https://login.fps5.cn/oauth/qr,服务端传 client_id、redirect_url 和本地生成的业务 state,拿到 appid / scope / redirect_uri / 网关 state。

02
渲染二维码

页面加载微信官方 wxLogin.js,用 self_redirect: true 在登录卡片内渲染二维码。

03
扫码确认

用户在微信中确认授权后,二维码 iframe 自身跳转到 redirect_url,并带上 code 与网关 state。

04
回调桥接

callback.php 运行在第三方 iframe 上下文,不读写 Session,只把 code / state 交给顶层 index.php。

05
校验业务 state

顶层页面与本站同源,Session 可用,取出并消费业务 state,一次性有效。

06
换取用户资料

POST https://login.fps5.cn/api/user/info,用 client_id + client_secret + ticket 拿到 openid、unionid、昵称、头像,写入 Session。

为什么必须经过 callback.php: 微信回调发生在 iframe 内,属于第三方上下文,浏览器不会带上本站的 Session Cookie。 如果在回调页直接校验 state,会读到空的 Session,从而误报「state 校验失败」。

4. 关键代码

完整实现见压缩包,核心只有三段。

4.1 服务端申请二维码参数

lib.php
// 业务 state:回调时原样带回,用于校验来源
$bizState = bin2hex(random_bytes(16));

$query = http_build_query([
    'client_id'    => $config['client_id'],
    'redirect_url' => $config['redirect_url'],
    'state'        => $bizState,
]);

[$http, $body] = demo_http_get(
    rtrim($config['gateway'], '/') . '/oauth/qr?' . $query
);

$json = json_decode($body, true);
if ((int) ($json['code'] ?? 0) !== 200 || empty($json['data']['state'])) {
    return ['ok' => false, 'msg' => $json['msg'] ?? '网关返回异常'];
}

// 只记录业务 state,不要记录网关内部 state
demo_remember_state($bizState);

return ['ok' => true, 'data' => $json['data']];

4.2 回调桥接页

callback.php
$code  = trim((string) ($_GET['code'] ?? ''));
$state = (string) ($_GET['state'] ?? '');

$query = [];
if ($code !== '') {
    $query['wx_code']  = $code;
    $query['wx_state'] = $state;
}

$target = 'index.php' . ($query ? '?' . http_build_query($query) : '');
?>

<script>
(function () {
    var target = <?= json_encode($target) ?>;
    try {
        if (window.top && window.top !== window) {
            // 顶层与本站同源,可安全导航
            window.top.location.replace(target);
            return;
        }
    } catch (e) {}
    window.location.replace(target);
})();
</script>

4.3 顶层校验 state 并换取用户资料

index.php
$wxCode  = trim((string) ($_GET['wx_code'] ?? ''));
$wxState = (string) ($_GET['wx_state'] ?? '');

if ($wxCode !== '') {
    if (!demo_consume_state($wxState)) {
        $loginError = '登录会话已过期或来源不可信,请重新扫码登录。';
    } else {
        [$http, $body] = demo_http_post(
            rtrim($config['gateway'], '/') . '/api/user/info',
            [
                'client_id'     => $config['client_id'],
                'client_secret' => $config['client_secret'],
                'code'          => $wxCode,
            ]
        );

        $json = json_decode($body, true);
        if ($http !== 200 || (int) ($json['code'] ?? 0) !== 200) {
            $loginError = '换取用户信息失败:' . ($json['msg'] ?? '接口异常');
        } else {
            session_regenerate_id(true);           // 防会话固定
            $_SESSION['wx_user'] = $json['data'];
            header('Location: index.php');         // 去掉地址栏里的 code
            exit;
        }
    }
}

5. 常见问题

页面提示「请先完成配置」

config.php 里仍然是 ak_你的client_id 这类占位符。填入真实凭证并刷新页面。

提示「登录会话已过期或来源不可信」

通常是二维码放置超过 30 分钟,或刷新页面后又扫了旧二维码。点击「刷新二维码」重新发起即可。若频繁出现,检查服务器 session.save_path 是否可写。

提示「域名不在白名单」

redirect_url 的域名必须与项目里配置的域名一致,修改白名单后需要重新审核。

二维码区域空白或一直加载中

res.wx.qq.com 无法访问时会出现。可先用页面底部的「整页跳转登录」验证链路是否正常。

扫码后停在「正在返回…」

顶层跳转被浏览器安全策略拦截。页面已提供「点此继续」链接,点击即可;也可改用 login.php 的整页跳转方式。

服务器无法访问网关

确认服务器可出网访问 https://login.fps5.cn,并已开启 PHP 的 curl 或 allow_url_fopen。

相关文档: 接口参数与错误码见 API 文档; 项目创建、域名白名单与审核流程见 使用说明; 桌面端接入见 Python Demo。