Skip to content

微信授权开发指南

概述

DSPlatform 实现了完整的微信授权体系,支持微信小程序、微信公众号等多种场景的授权登录、绑定和解绑功能。系统通过统一的授权接口,实现了多端用户身份的统一管理。

技术架构

核心组件

  • WechatAuthService: 微信授权服务类,处理所有授权相关业务逻辑
  • DeshangOfficialEasyService: 微信公众号服务封装
  • DeshangMiniEasyService: 微信小程序服务封装
  • UserIdentityDao: 用户身份数据访问层,管理微信绑定关系
  • LoginService: 登录服务,处理登录后的业务逻辑

目录结构

app/api/
├── controller/
│   └── wechat/
│       ├── WechatAuth.php                    # 微信授权控制器
│       └── validate/
│           └── WechatAuthValidate.php        # 参数验证器
├── service/
│   └── wechat/
│       └── WechatAuthService.php             # 微信授权服务
└── route/
    └── wechat.php                            # 微信路由配置

授权场景

系统支持以下微信授权场景:

1. 微信小程序 (wechat_mini)

  • 使用场景: 小程序内登录、绑定、支付
  • 获取方式: 通过 uni.login() 获取 code
  • 特点: 无需用户授权,直接获取 openid

2. 微信公众号 (wechat_official)

  • 使用场景: 公众号内网页登录、绑定
  • 获取方式: 通过 OAuth2.0 授权页面获取 code
  • 授权范围:
    • snsapi_base: 静默授权,仅获取 openid
    • snsapi_userinfo: 需要用户确认,获取用户信息

授权模式

1. 登录模式 (login)

用途: 通过微信授权实现用户登录

流程:

  1. 前端获取微信 code
  2. 调用授权接口,传入 mode: 'login'
  3. 后端通过 unionid 查找用户
  4. 如果用户存在,自动登录
  5. 如果用户不存在,自动注册并登录

特点:

  • 必须通过 unionid 关联用户
  • 支持自动注册
  • 登录模式下 merchant_id 必须为 0(平台授权)

2. 绑定模式 (bind)

用途: 将微信账号绑定到已登录的用户

流程:

  1. 用户必须先登录
  2. 前端获取微信 code
  3. 调用授权接口,传入 mode: 'bind'
  4. 后端检查该微信是否已被其他用户绑定
  5. 如果未绑定,创建绑定关系

特点:

  • 需要用户已登录
  • 通过 unionid 防止重复绑定
  • 绑定前会删除用户原有的绑定关系

3. 获取 OpenID 模式 (getOpenid)

用途: 仅获取 openid,用于支付等场景

流程:

  1. 用户必须先登录
  2. 前端获取微信 code
  3. 调用授权接口,传入 mode: 'getOpenid'
  4. 后端仅更新 openid,不更新 unionid

特点:

  • 需要用户已登录
  • 不更新 unionid,避免影响登录关联
  • 主要用于支付场景

数据库设计

user_identity 表

用户身份关联表,存储用户与微信的绑定关系:

sql
CREATE TABLE `user_identity` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) NOT NULL COMMENT '用户ID',
  `merchant_id` int(11) NOT NULL DEFAULT '0' COMMENT '商户ID',
  `wx_oauth_openid` varchar(32) DEFAULT NULL COMMENT '微信网页授权openid',
  `wx_mini_openid` varchar(32) DEFAULT NULL COMMENT '小程序openid',
  `wx_app_openid` varchar(32) DEFAULT NULL COMMENT 'APP openid',
  `wx_unionid` varchar(32) DEFAULT NULL COMMENT '微信unionid,用于账户绑定唯一标识',
  `create_at` int(11) DEFAULT NULL,
  `update_at` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `udx_wx_unionid` (`wx_unionid`),
  KEY `idx_user_id` (`user_id`),
  KEY `idx_merchant_id` (`merchant_id`),
  KEY `idx_wx_oauth_openid` (`wx_oauth_openid`),
  KEY `idx_wx_mini_openid` (`wx_mini_openid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户身份关联表';

字段说明

  • wx_unionid: 微信 unionid,同一微信开放平台账号下唯一,用于关联同一用户的不同端
  • wx_oauth_openid: 公众号 openid,用于公众号相关功能
  • wx_mini_openid: 小程序 openid,用于小程序相关功能
  • wx_app_openid: APP openid,用于 APP 相关功能
  • merchant_id: 商户ID,0 表示平台授权

API 接口

1. 微信授权接口

接口地址: POST /api/wechat/auth

请求参数:

json
{
    "code": "微信授权码",
    "scene": "wechat_mini|wechat_official|app",
    "mode": "login|bind|getOpenid",
    "merchant_id": 0
}

参数说明:

参数类型必填说明
codestring微信授权码
scenestring授权场景:wechat_mini/wechat_official/app
modestring授权模式:login/bind/getOpenid
merchant_idinteger商户ID,登录和绑定时必须为0

响应示例:

json
{
    "code": 10000,
    "msg": "授权成功",
    "data": {
        "access_token": "xxx",
        "access_expires_time": 1234567890,
        "refresh_token": "xxx",
        "refresh_expires_time": 1234567890,
        "userinfo": {
            "id": 1,
            "username": "user123",
            "nickname": "用户昵称"
        }
    }
}

2. 检测微信绑定状态

接口地址: GET /api/wechat/check-auth

请求参数:

merchant_id: 商户ID
scene: 授权场景

响应示例:

json
{
    "code": 10000,
    "msg": "操作成功",
    "data": true  // true表示已绑定,false表示未绑定
}

3. 获取公众号授权URL

接口地址: POST /api/wechat/official-auth-url

请求参数:

json
{
    "merchant_id": 0,
    "scene": "wechat_official",
    "mode": "login|bind|getOpenid",
    "redirect_url": "授权成功后的回调地址"
}

响应示例:

json
{
    "code": 10000,
    "msg": "操作成功",
    "data": "https://open.weixin.qq.com/connect/oauth2/authorize?..."
}

4. 获取 JS SDK 配置

接口地址: POST /api/wechat/js-sdk-config

请求参数:

json
{
    "url": "当前页面URL",
    "merchant_id": 0
}

响应示例:

json
{
    "code": 10000,
    "msg": "操作成功",
    "data": {
        "appId": "wx1234567890",
        "timestamp": 1234567890,
        "nonceStr": "random_string",
        "signature": "signature_string"
    }
}

服务层实现

WechatAuthService 核心方法

1. 获取微信授权信息

php
/**
 * 获取微信授权信息
 * @param array $data 授权参数
 * @return array
 */
public function getWechatAuthInfo($data)
{
    // 根据场景获取微信用户信息
    if ($data['scene'] == 'wechat_official') {
        $scope = $data['mode'] == 'getOpenid' ? 'snsapi_base' : 'snsapi_userinfo';
        $wechatInfo = (new DeshangOfficialEasyService())
            ->init($data['merchant_id'])
            ->getWechatInfoByCode($data['code'], $scope);
    } else if ($data['scene'] == 'wechat_mini') {
        $wechatInfo = (new DeshangMiniEasyService())
            ->init($data['merchant_id'])
            ->getWechatInfoByCode($data['code']);
    }
    
    // 根据模式处理授权信息
    if ($data['mode'] == 'login') {
        return $this->handleLogin($wechatInfo, $data);
    } else if ($data['mode'] == 'bind') {
        return $this->handleBind($wechatInfo, $data);
    } else if ($data['mode'] == 'getOpenid') {
        return $this->handleGetOpenid($wechatInfo, $data);
    }
}

2. 处理登录

php
/**
 * 处理登录微信授权信息
 * @param array $wechatInfo 微信用户信息
 * @param array $data 授权参数
 * @return array
 */
private function handleLogin(array $wechatInfo, array $data)
{
    // 验证 merchant_id
    if ($data['merchant_id'] != 0) {
        throw new CommonException('merchant_id 错误');
    }
    
    // 验证 unionid
    if (empty($wechatInfo['unionid'])) {
        throw new CommonException('用户信息unionid不存在');
    }
    
    // 通过 unionid 查找用户
    $user_identity = (new UserIdentityDao())->getUserIdentityInfo([
        ['wx_unionid', '=', $wechatInfo['unionid']],
        ['merchant_id', '=', $data['merchant_id']]
    ]);
    
    if (!empty($user_identity)) {
        // 用户已存在,自动登录
        $user_id = $user_identity['user_id'];
    } else {
        // 用户不存在,自动注册
        $register_data = [
            'username' => $this->generateRandomUsername(),
            'password' => generateRandomString(6),
        ];
        $user_id = (new DeshangUserService())->createUser($register_data);
    }
    
    // 更新用户绑定信息
    $user_identity_data = [
        'user_id' => $user_id,
        'merchant_id' => $data['merchant_id'],
        'wx_unionid' => $wechatInfo['unionid'],
    ];
    
    // 根据场景设置对应的 openid
    switch ($data['scene']) {
        case 'wechat_official':
            $user_identity_data['wx_oauth_openid'] = $wechatInfo['openid'];
            break;
        case 'wechat_mini':
            $user_identity_data['wx_mini_openid'] = $wechatInfo['openid'];
            break;
        case 'wechat_app':
            $user_identity_data['wx_app_openid'] = $wechatInfo['openid'];
            break;
    }
    
    // 保存或更新绑定信息
    if (empty($user_identity)) {
        (new UserIdentityDao())->createUserIdentity($user_identity_data);
    } else {
        (new UserIdentityDao())->updateUserIdentity([
            ['user_id', '=', $user_id],
            ['merchant_id', '=', $data['merchant_id']],
            ['wx_unionid', '=', $wechatInfo['unionid']]
        ], $user_identity_data);
    }
    
    // 执行登录
    $user_info = (new UserDao())->getUserInfo(['id' => $user_id]);
    return (new LoginService())->loginAfter($user_info);
}

3. 处理绑定

php
/**
 * 处理绑定微信授权信息
 * @param array $wechatInfo 微信用户信息
 * @param array $data 授权参数
 * @return array
 */
private function handleBind(array $wechatInfo, array $data)
{
    // 验证用户已登录
    if (!$this->user_id) {
        throw new AuthException('请先登录');
    }
    
    // 验证 merchant_id
    if ($data['merchant_id'] != 0) {
        throw new CommonException('merchant_id 错误');
    }
    
    // 验证 unionid
    if (empty($wechatInfo['unionid'])) {
        throw new CommonException('用户信息unionid不存在');
    }
    
    // 检查该微信是否已被其他用户绑定
    $user_identity = (new UserIdentityDao())->getUserIdentityInfo([
        ['wx_unionid', '=', $wechatInfo['unionid']],
        ['merchant_id', '=', $data['merchant_id']]
    ]);
    
    if (!empty($user_identity)) {
        throw new CommonException('此微信已经绑定了用户');
    }
    
    // 删除当前用户的所有绑定(避免冲突)
    (new UserIdentityDao())->deleteUserIdentity([
        ['user_id', '=', $this->user_id],
        ['merchant_id', '=', $data['merchant_id']]
    ]);
    
    // 创建新的绑定关系
    $user_identity_data = [
        'user_id' => $this->user_id,
        'merchant_id' => $data['merchant_id'],
        'wx_unionid' => $wechatInfo['unionid'],
    ];
    
    // 根据场景设置对应的 openid
    switch ($data['scene']) {
        case 'wechat_official':
            $user_identity_data['wx_oauth_openid'] = $wechatInfo['openid'];
            break;
        case 'wechat_mini':
            $user_identity_data['wx_mini_openid'] = $wechatInfo['openid'];
            break;
        case 'wechat_app':
            $user_identity_data['wx_app_openid'] = $wechatInfo['openid'];
            break;
    }
    
    (new UserIdentityDao())->createUserIdentity($user_identity_data);
    
    return $wechatInfo;
}

4. 处理获取 OpenID

php
/**
 * 处理获取 OpenID(用于支付等场景)
 * @param array $wechatInfo 微信用户信息
 * @param array $data 授权参数
 * @return array
 */
private function handleGetOpenid(array $wechatInfo, array $data)
{
    // 验证用户已登录
    if (!$this->user_id) {
        throw new AuthException('请先登录');
    }
    
    // 获取用户身份信息
    $user_identity = (new UserIdentityDao())->getUserIdentityInfo([
        ['user_id', '=', $this->user_id],
        ['merchant_id', '=', $data['merchant_id']]
    ]);
    
    // 仅更新 openid,不更新 unionid
    $user_identity_data = [
        'user_id' => $this->user_id,
        'merchant_id' => $data['merchant_id'],
    ];
    
    // 根据场景设置对应的 openid
    switch ($data['scene']) {
        case 'wechat_official':
            $user_identity_data['wx_oauth_openid'] = $wechatInfo['openid'];
            break;
        case 'wechat_mini':
            $user_identity_data['wx_mini_openid'] = $wechatInfo['openid'];
            break;
        case 'wechat_app':
            $user_identity_data['wx_app_openid'] = $wechatInfo['openid'];
            break;
    }
    
    // 保存或更新
    if (empty($user_identity)) {
        (new UserIdentityDao())->createUserIdentity($user_identity_data);
    } else {
        (new UserIdentityDao())->updateUserIdentity([
            ['user_id', '=', $this->user_id],
            ['merchant_id', '=', $data['merchant_id']]
        ], $user_identity_data);
    }
    
    return $wechatInfo;
}

使用示例

小程序登录

前端代码 (uniapp):

typescript
// 小程序登录
uni.login({
    provider: 'weixin',
    success: async (loginRes) => {
        const res = await wechatAuth({
            code: loginRes.code,
            scene: 'wechat_mini',
            mode: 'login',
            merchant_id: 0
        });
        
        if (res.code === 10000) {
            // 登录成功,保存用户信息
            const userStore = useUserStore();
            userStore.loginAfter(res.data);
        }
    }
});

后端调用:

php
// 控制器
public function getWechatAuthInfo()
{
    $data = [
        'merchant_id' => input('param.merchant_id', 0),
        'code' => input('param.code', ''),
        'scene' => input('param.scene', ''),
        'mode' => input('param.mode', ''),
    ];
    
    $this->validate($data, 'app\api\controller\wechat\validate\WechatAuthValidate.getWechatAuthInfo');
    
    $result = (new WechatAuthService())->getWechatAuthInfo($data);
    return ds_json_success('授权成功', $result);
}

公众号登录

前端代码:

typescript
// 1. 获取授权URL
const res = await getOfficialAuthUrl({
    merchant_id: 0,
    scene: 'wechat_official',
    mode: 'login',
    redirect_url: window.location.href
});

// 2. 跳转到授权页面
if (res.code === 10000) {
    window.location.href = res.data;
}

// 3. 授权回调后处理
onLoad((options) => {
    if (options.code) {
        wechatAuthUtil.handleOfficialCallback({
            code: options.code,
            mode: 'login',
            merchant_id: 0
        });
    }
});

绑定微信

前端代码:

typescript
// 用户已登录,绑定微信
uni.login({
    provider: 'weixin',
    success: async (loginRes) => {
        const res = await wechatAuth({
            code: loginRes.code,
            scene: 'wechat_mini',
            mode: 'bind',
            merchant_id: 0
        });
        
        if (res.code === 10000) {
            uni.showToast({
                title: '绑定成功',
                icon: 'success'
            });
        }
    }
});

获取 OpenID(用于支付)

前端代码:

typescript
// 用户已登录,获取 openid 用于支付
uni.login({
    provider: 'weixin',
    success: async (loginRes) => {
        const res = await wechatAuth({
            code: loginRes.code,
            scene: 'wechat_mini',
            mode: 'getOpenid',
            merchant_id: 0
        });
        
        if (res.code === 10000) {
            // openid 已保存,可以用于支付
            proceedToPay();
        }
    }
});

参数验证

WechatAuthValidate 验证器

php
class WechatAuthValidate extends BaseValidate
{
    protected $rule = [
        'merchant_id' => 'require|integer',
        'scene' => 'require|in:wechat_official,wechat_mini,app',
        'mode' => 'require|in:getOpenid,login,bind',
        'redirect_url' => 'require|url',
        'code' => 'string'
    ];
    
    protected $message = [
        'merchant_id.require' => '商户ID不能为空',
        'merchant_id.integer' => '商户ID必须是整数',
        'scene.require' => '场景不能为空',
        'scene.in' => '场景必须是wechat_official,wechat_mini或app',
        'mode.require' => '授权模式不能为空',
        'mode.in' => '授权模式必须是getOpenid,login或bind',
        'redirect_url.require' => '重定向URL不能为空',
        'redirect_url.url' => '重定向URL格式不正确',
        'code.string' => 'code必须是字符串'
    ];
    
    protected $scene = [
        'getWechatAuthInfo' => ['merchant_id', 'code', 'scene', 'mode'],
        'checkWechatAuth' => ['merchant_id', 'scene'],
        'getOfficialAuthUrl' => ['merchant_id', 'scene', 'mode', 'redirect_url'],
        'getJsSdkConfig' => ['merchant_id', 'url']
    ];
}

最佳实践

1. UnionID 关联策略

  • 登录模式: 必须通过 unionid 关联用户,确保同一微信账号在不同端登录的是同一用户
  • 绑定模式: 通过 unionid 防止重复绑定
  • 获取 OpenID 模式: 不更新 unionid,避免影响登录关联

2. 自动注册策略

  • 登录模式下,如果用户不存在,自动创建账号
  • 用户名自动生成:日期时间 + 随机字符串
  • 密码自动生成:6位随机字符串

3. 绑定关系管理

  • 绑定前先删除用户原有的绑定关系,避免冲突
  • 通过 unionid 唯一索引防止重复绑定
  • 支持解绑后重新绑定

4. 多端统一

  • 同一微信账号在不同端(小程序、公众号)通过 unionid 关联
  • 不同端的 openid 分别存储,互不影响
  • 登录时通过 unionid 查找用户,实现多端统一

5. 安全性考虑

  • 验证 code 的有效性
  • 验证 unionid 的存在性
  • 防止重复绑定
  • 验证 merchant_id 的正确性

路由配置

php
// app/api/route/wechat.php
Route::group('wechat', function () {
    // 微信授权
    Route::post('auth', 'wechat.WechatAuth/getWechatAuthInfo');
    
    // 检测微信授权
    Route::get('check-auth', 'wechat.WechatAuth/checkWechatAuth');
    
    // 获取微信授权URL
    Route::post('official-auth-url', 'wechat.WechatAuth/getOfficialAuthUrl');
    
    // 获取JS SDK配置
    Route::post('js-sdk-config', 'wechat.WechatAuth/getJsSdkConfig');
})->middleware([
    UserAuthorizeToken::class,
    UserAuthorizeLog::class,
]);

注意: 授权接口需要认证中间件,但登录模式时用户可能未登录,需要在中间件中特殊处理。

配置说明

微信配置

需要在管理后台配置微信相关参数:

  • 小程序配置: AppID、AppSecret
  • 公众号配置: AppID、AppSecret
  • 开放平台配置: 需要绑定小程序和公众号,获取 unionid

环境要求

  • 微信开放平台账号
  • 已认证的小程序/公众号
  • 小程序和公众号绑定到同一开放平台账号

常见问题

Q1: 为什么登录模式必须通过 unionid?

A: 因为同一微信账号在不同端(小程序、公众号)的 openid 是不同的,只有 unionid 是唯一的,可以确保多端登录的是同一用户。

Q2: 为什么获取 OpenID 模式不更新 unionid?

A: 因为可能存在用户通过普通方式登录,但支付时需要获取 openid 的情况。如果更新了 unionid,可能会影响原有的登录关联关系。

Q3: 如何解绑微信?

A: 删除 user_identity 表中对应的记录即可。注意需要同时删除 unionid 和对应的 openid。


最后更新:2024-01-20
维护者:DSPlatform技术团队(德尚网络)

获取帮助

如果您在使用过程中遇到问题,可以通过以下方式获取帮助:

  • 官方网站https://www.csdeshang.com
  • 电话咨询:15364080101(微信同号)
  • QQ咨询:858761000
  • 邮箱咨询:858761000@qq.com
  • 工作时间:工作日 9:00-18:00
  • 微信咨询:扫码添加微信
微信二维码

版权所有 © 2014-至今 德尚网络