跳转至

微信与企业微信获取手机号实现指南

核对日期:2026-07-29
示例技术栈:微信原生小程序、H5、Java 8、Spring Boot 2.2.x


1. 方案选择

微信登录、公众号授权和企业微信客户授权并不等于手机号授权。应先根据用户类型选择方案。

用户类型 推荐方案 能否直接获取手机号
普通微信用户、消费者 微信小程序 getPhoneNumber 可以,需要用户点击授权
企业内部员工 企业微信 snsapi_privateinfo 可以,需要管理员和员工授权
企业微信外部联系人、微信客户 小程序授权或短信验证 企业微信接口不能直接获取
公众号网页、普通微信 H5 小程序授权或短信验证 网页 OAuth 不返回手机号

推荐结论:

普通消费者 -> 微信小程序 getPhoneNumber
企业内部员工 -> 企业微信 snsapi_privateinfo
企业微信外部客户 -> 跳转小程序,或者填写手机号并进行短信验证

企业微信外部联系人的限制

企业微信客户详情中的 remark_mobiles 是企业成员手工备注的号码,不是微信验证后授权返回的手机号,不保证存在或准确,也不能作为正式的手机号认证结果。


2. 微信小程序获取手机号

2.1 实现流程

用户点击手机号授权按钮
    -> 小程序获得一次性 code
    -> 前端把 code 发送给业务后端
    -> 后端获取并缓存小程序 access_token
    -> 后端调用 getuserphonenumber
    -> 微信返回手机号
    -> 后端绑定当前登录用户并加密保存

关键限制:

  • 小程序需要是非个人主体,并完成微信认证。
  • 用户必须主动点击授权按钮,不能静默获取。
  • code 有效期为 5 分钟,只能使用一次。
  • codewx.login() 返回的登录 code 不同,不能混用。
  • 新流程不再需要前端传递 encryptedDataiv
  • 当前官方标准价格为成功调用一次 0.03 元,实际以公众平台为准。

2.2 小程序前端关键代码

页面按钮:

<button
  open-type="getPhoneNumber"
  bindgetphonenumber="onGetPhoneNumber"
  loading="{{submitting}}"
  disabled="{{submitting}}"
>
  使用微信手机号
</button>

页面逻辑:

Page({
  data: {
    submitting: false
  },

  /**
   * 接收微信返回的一次性手机号动态令牌。
   */
  onGetPhoneNumber: function (event) {
    const detail = event.detail || {};
    if (detail.errMsg !== 'getPhoneNumber:ok' || !detail.code) {
      wx.showToast({
        title: detail.errno === 1400001
          ? '手机号授权额度不足'
          : '你已取消手机号授权',
        icon: 'none'
      });
      return;
    }

    this.exchangePhoneCode(detail.code);
  },

  /**
   * 将 code 交给业务后端消费,前端不直接调用微信服务端接口。
   */
  exchangePhoneCode: function (code) {
    this.setData({ submitting: true });

    wx.request({
      url: 'https://api.example.com/api/wechat/phone',
      method: 'POST',
      header: {
        'Content-Type': 'application/json',
        // 应先通过 wx.login 建立业务登录态,再携带业务 Token 绑定手机号。
        'Authorization': 'Bearer ' + wx.getStorageSync('userToken')
      },
      data: { code: code },
      success: function (response) {
        if (response.statusCode === 200) {
          wx.showToast({ title: '绑定成功', icon: 'success' });
          return;
        }
        wx.showToast({ title: '绑定失败,请重新授权', icon: 'none' });
      },
      fail: function () {
        wx.showToast({ title: '网络异常,请重新授权', icon: 'none' });
      },
      complete: () => {
        this.setData({ submitting: false });
      }
    });
  }
});

手机号授权和登录是两个流程

wx.login() 用于识别小程序用户并建立业务登录态,getPhoneNumber 用于获取手机号。后端应把手机号绑定到已经登录的业务用户,不能只凭手机号 code 创建不受控的账号绑定关系。

2.3 后端配置

application.yml

wechat:
  mini-program:
    app-id: ${WECHAT_MINI_APP_ID}
    app-secret: ${WECHAT_MINI_APP_SECRET}

密钥只能通过环境变量或密钥管理服务注入,禁止写入小程序代码或提交到 Git。

2.4 Spring Boot 后端关键代码

下面只展示关键调用链。accessTokenService 负责获取并缓存小程序 access_token,生产环境应在 Token 到期前约 5 分钟刷新。

package com.example.wechat;

import java.util.Collections;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;

import com.fasterxml.jackson.databind.JsonNode;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

/**
 * 微信小程序手机号绑定接口。
 */
@RestController
@RequestMapping("/api/wechat")
public class WechatPhoneController {

    private static final String PHONE_API =
            "https://api.weixin.qq.com/wxa/business/getuserphonenumber"
                    + "?access_token={accessToken}";

    private final RestTemplate restTemplate;
    private final WechatAccessTokenService accessTokenService;
    private final MobileBindingService mobileBindingService;

    public WechatPhoneController(
            RestTemplate restTemplate,
            WechatAccessTokenService accessTokenService,
            MobileBindingService mobileBindingService
    ) {
        this.restTemplate = restTemplate;
        this.accessTokenService = accessTokenService;
        this.mobileBindingService = mobileBindingService;
    }

    /**
     * 消费微信一次性 code,获取手机号并绑定当前登录用户。
     *
     * @param request 小程序手机号授权请求
     * @return 脱敏后的手机号
     */
    @PostMapping("/phone")
    public ResponseEntity<PhoneResult> bindPhone(
            @Valid @RequestBody PhoneCodeRequest request
    ) {
        String accessToken = accessTokenService.getMiniProgramAccessToken();
        JsonNode response = restTemplate.postForObject(
                PHONE_API,
                Collections.singletonMap("code", request.getCode()),
                JsonNode.class,
                accessToken
        );

        validateWechatResponse(response);
        String phone = response.path("phone_info").path("phoneNumber").asText();
        String countryCode = response.path("phone_info").path("countryCode").asText();

        // currentUserId 应从已校验的登录态中取得,不能由前端直接提交。
        String currentUserId = CurrentUserContext.requireUserId();
        mobileBindingService.bindEncrypted(currentUserId, phone, countryCode);

        return ResponseEntity.ok(new PhoneResult(maskPhone(phone)));
    }

    private void validateWechatResponse(JsonNode response) {
        if (response == null || response.path("errcode").asInt(-1) != 0) {
            throw new IllegalStateException("微信手机号授权失败");
        }
        if (response.path("phone_info").path("phoneNumber").asText().isEmpty()) {
            throw new IllegalStateException("微信未返回手机号");
        }
    }

    private String maskPhone(String phone) {
        if (phone == null || phone.length() < 7) {
            return "****";
        }
        return phone.substring(0, 3)
                + "****"
                + phone.substring(phone.length() - 4);
    }

    /**
     * 小程序提交的手机号授权参数。
     */
    public static class PhoneCodeRequest {

        @NotBlank
        private String code;

        public String getCode() {
            return code;
        }

        public void setCode(String code) {
            this.code = code;
        }
    }

    /**
     * 返回给小程序的脱敏手机号。
     */
    public static class PhoneResult {

        private final String maskedPhone;

        public PhoneResult(String maskedPhone) {
            this.maskedPhone = maskedPhone;
        }

        public String getMaskedPhone() {
            return maskedPhone;
        }
    }
}

小程序 access_token 获取地址:

GET https://api.weixin.qq.com/cgi-bin/token
    ?grant_type=client_credential
    &appid=APPID
    &secret=APPSECRET

返回的 access_token 通常包含 expires_in,后端必须缓存,不要在每次手机号请求时重新获取。多实例部署时应使用 Redis 和分布式锁统一刷新。


3. 企业微信获取内部员工手机号

3.1 开通条件

  1. 在企业微信管理后台创建自建应用。
  2. 设置应用可见范围,目标员工必须位于可见范围内。
  3. 配置网页授权可信域名和 OAuth 回调地址。
  4. 在应用详情中允许获取手机号敏感字段。
  5. OAuth 使用 snsapi_privateinfo,并且必须传 agentid
  6. 员工在授权页面确认提供敏感信息。

3.2 实现流程

H5 请求后端生成企业微信授权地址
    -> 员工确认 snsapi_privateinfo 授权
    -> 企业微信回调 code + state
    -> 后端通过 auth/getuserinfo 获取 userid + user_ticket
    -> 后端通过 auth/getuserdetail 获取 mobile
    -> 根据 userid 建立登录态或绑定员工账号

3.3 H5 前端关键代码

发起企业微信授权:

<button id="wecomAuthorize" type="button">使用企业微信手机号</button>

<script>
  document.getElementById('wecomAuthorize').addEventListener('click', async function () {
    const response = await fetch(
      '/api/wecom/authorization-url?returnUrl=' + encodeURIComponent('/profile'),
      { credentials: 'include' }
    );
    const result = await response.json();
    window.location.assign(result.authorizationUrl);
  });
</script>

OAuth 回调页读取 codestate,再交给后端:

<script>
  async function completeWeComAuthorization() {
    const params = new URLSearchParams(window.location.search);
    const code = params.get('code');
    const state = params.get('state');

    if (!code || !state) {
      throw new Error('企业微信授权参数不完整');
    }

    const response = await fetch('/api/wecom/phone', {
      method: 'POST',
      credentials: 'include',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ code: code, state: state })
    });

    const result = await response.json();
    if (!response.ok) {
      throw new Error(result.message || '企业微信手机号授权失败');
    }

    // 后端应在换取手机号后建立业务会话,前端只负责跳回业务页面。
    window.location.replace(result.returnUrl || '/');
  }

  completeWeComAuthorization();
</script>

3.4 后端生成授权地址

企业微信授权地址格式:

https://open.weixin.qq.com/connect/oauth2/authorize
?appid=CORPID
&redirect_uri=URL_ENCODED_CALLBACK
&response_type=code
&scope=snsapi_privateinfo
&state=STATE
&agentid=AGENTID
#wechat_redirect

Spring Boot 关键代码:

/**
 * 生成企业微信敏感信息授权地址。
 */
public String buildAuthorizationUrl(String state) {
    return UriComponentsBuilder
            .fromHttpUrl("https://open.weixin.qq.com/connect/oauth2/authorize")
            .queryParam("appid", corpId)
            .queryParam("redirect_uri", oauthCallbackUrl)
            .queryParam("response_type", "code")
            .queryParam("scope", "snsapi_privateinfo")
            .queryParam("state", state)
            .queryParam("agentid", agentId)
            .fragment("wechat_redirect")
            .build()
            .encode()
            .toUriString();
}

state 必须满足:

  • 使用安全随机数生成。
  • 与当前浏览器会话及授权后的站内地址关联。
  • 建议 5 至 10 分钟过期。
  • 回调成功后立即删除,只允许使用一次。
  • 多实例部署时保存在 Redis 中。
  • returnUrl 只允许站内相对路径,防止开放重定向。

3.5 后端换取企业员工手机号

下面展示三次关键调用:获取企业微信 access_token、获取 user_ticket、获取手机号。

package com.example.wecom;

import java.util.Collections;

import com.fasterxml.jackson.databind.JsonNode;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

/**
 * 企业微信内部成员敏感信息获取服务。
 */
@Service
public class WeComPhoneService {

    private final RestTemplate restTemplate;
    private final WeComAccessTokenService accessTokenService;

    public WeComPhoneService(
            RestTemplate restTemplate,
            WeComAccessTokenService accessTokenService
    ) {
        this.restTemplate = restTemplate;
        this.accessTokenService = accessTokenService;
    }

    /**
     * 使用企业微信 OAuth code 获取内部员工手机号。
     *
     * @param code 企业微信 OAuth 回调 code
     * @return 企业微信员工身份与手机号
     */
    public EmployeePhone getEmployeePhone(String code) {
        String accessToken = accessTokenService.getAccessToken();

        JsonNode userInfo = restTemplate.getForObject(
                "https://qyapi.weixin.qq.com/cgi-bin/auth/getuserinfo"
                        + "?access_token={accessToken}&code={code}",
                JsonNode.class,
                accessToken,
                code
        );
        validateResponse(userInfo, "获取企业微信用户身份失败");

        String userId = userInfo.path("userid").asText();
        if (userId.isEmpty()) {
            // 非企业成员通常只返回 openid 或 external_userid。
            throw new IllegalStateException("当前用户不是企业内部成员");
        }

        String userTicket = userInfo.path("user_ticket").asText();
        if (userTicket.isEmpty()) {
            throw new IllegalStateException(
                    "未获得user_ticket,请检查snsapi_privateinfo、agentid和应用可见范围"
            );
        }

        JsonNode detail = restTemplate.postForObject(
                "https://qyapi.weixin.qq.com/cgi-bin/auth/getuserdetail"
                        + "?access_token={accessToken}",
                Collections.singletonMap("user_ticket", userTicket),
                JsonNode.class,
                accessToken
        );
        validateResponse(detail, "获取企业微信敏感信息失败");

        String mobile = detail.path("mobile").asText();
        if (mobile.isEmpty()) {
            throw new IllegalStateException("企业微信未返回员工手机号");
        }

        return new EmployeePhone(userId, mobile);
    }

    private void validateResponse(JsonNode response, String message) {
        if (response == null || response.path("errcode").asInt(-1) != 0) {
            throw new IllegalStateException(message);
        }
    }

    /**
     * 企业微信内部员工手机号结果。
     */
    public static class EmployeePhone {

        private final String userId;
        private final String mobile;

        public EmployeePhone(String userId, String mobile) {
            this.userId = userId;
            this.mobile = mobile;
        }

        public String getUserId() {
            return userId;
        }

        public String getMobile() {
            return mobile;
        }
    }
}

企业微信 access_token 获取接口:

GET https://qyapi.weixin.qq.com/cgi-bin/gettoken
    ?corpid=CORP_ID
    &corpsecret=APP_SECRET

使用的是自建应用自身的 Secret。返回的 Token 同样需要集中缓存并提前刷新。


4. 企业微信外部联系人怎么办

企业微信 OAuth 对普通微信客户通常只返回:

{
  "errcode": 0,
  "errmsg": "ok",
  "openid": "OPENID",
  "external_userid": "EXTERNAL_USERID"
}

它不会返回真实手机号,应选择以下方式之一。

方式一:跳转小程序

企业微信客户进入 H5 或客户群入口
    -> 打开已关联的小程序
    -> 点击 getPhoneNumber
    -> 后端换取手机号
    -> 通过业务登录态或 unionid 关联客户

方式二:短信验证

客户填写手机号
    -> 后端发送一次性验证码
    -> 客户提交验证码
    -> 后端验证有效期、次数和风控规则
    -> 完成手机号绑定

短信接口应限制同一手机号、IP 和设备的发送频率,并设置验证码有效期、最大验证次数和成功后立即失效规则。


5. 上线检查清单

微信小程序

  • 小程序为非个人主体并完成认证。
  • 已声明手机号用途和隐私政策。
  • 后端域名使用 HTTPS,并加入小程序 request 合法域名。
  • 手机号组件额度充足。
  • wx.login code 与手机号 code 分开处理。
  • code 消费失败后让用户重新授权,不重复使用旧 code。

企业微信

  • 已创建自建应用并设置员工可见范围。
  • 已配置网页授权可信域名和回调地址。
  • OAuth 使用 snsapi_privateinfo 并传入 agentid
  • 管理员已允许应用获取手机号敏感字段。
  • 员工授权后能够返回 user_ticket

服务端安全

  • Secret 只存放在服务端环境变量或密钥管理服务。
  • access_token 使用 Redis 等集中式缓存。
  • OAuth state 随机、短时有效并一次性消费。
  • 手机号加密存储,接口和页面只返回脱敏值。
  • 日志不记录手机号、code、user_ticket、Secret 和 access_token。
  • 手机号绑定接口必须校验当前业务登录态并进行限流。
  • 不使用手机号作为永久身份主键。

6. 常见问题

现象 常见原因 处理方式
小程序没有返回 code 用户拒绝、额度不足、账号不符合条件 检查 errMsgerrno、认证主体和额度
微信返回 40029 code 无效、过期或已使用 让用户重新点击授权,不要重试旧 code
微信返回 40013 code 与 access_token 所属 AppID 不一致 检查环境配置和多小程序 Token 隔离
企业微信没有 user_ticket scope 错误、缺少 agentid、员工不在可见范围 使用 snsapi_privateinfo 并检查后台配置
企业微信没有 mobile 管理员未开放手机号或员工未同意 检查敏感字段配置和授权页面
企业微信只返回 openid 当前访问者不是企业内部成员 按外部联系人方案处理

7. 官方文档

上线前再次核对

微信与企业微信会持续调整账号资质、敏感字段权限、收费规则和接口安全要求。上线前应再次检查官方文档和对应管理后台的实际提示。