首先注册时可以看到公众号有三种类型,个人用户大多数选择订阅号,而企业用户一般选择服务号和企业号。
我们平常大多数关注的都是订阅号,他们统一都放置在微信应用的订阅号消息列表中,没有微信支付等高级功能,只是用于发布文章等基础功能。
而服务号和企业号都在会话列表,和我们的微信好友是同级别的位置,具备微信支付等高级功能,一般是某个企业品牌的对外操作窗口,如海底捞火锅、顺丰速运等。
我们前期开发测试只需要注册个人订阅号即可,真正开发使用的是开发者工具里的测试号,具体下面会说。
真正生产的话,使用的都是经过微信认证的订阅号、服务号、企业号。
我们在微信公众平台扫码登录后可以发现管理页面左侧菜单栏有丰富的功能:
大概可以分为这几大模块:
首页、功能、小程序、管理、推广、统计、设置、开发
作为开发人员,首先应该关注的是设置、开发模块;而作为产品运营人员,关注的是功能、管理、推广模块;作为数据分析人员,关注的是统计模块。
首先我们不妨各个功能模块都点击看一看,大概了解下我们能做些什么。可以确认的是,这个微信公众平台当然不只是给开发人员使用的,它提供了很多非技术人员可在UI界面上交互操作的功能模块。
如配置消息回复、自定义菜单、发布文章等:
这个时候我们可能会想:这些功能好像非技术人员都能随意操作,那么还需要我们技术人员去开发吗?
答案是: 如果只是日常简单的推送文章,就像我们关注的大多数公众号一样,那确实不需要技术人员去开发;但是,如果你想将你们的网站嵌入进去公众号菜单里(这里指的是把前端项目的首页链接配置在自定义菜单),并且实现微信端的独立登录认证、获取微信用户信息、微信支付等高级功能,或者觉得UI交互的配置方式无法满足你的需求,你需要更加自由、随心所欲的操作,那么我们就必须启用开发者模式了,通过技术人员的手段去灵活控制公众号。
这里有一点需要注意,如果我们决定技术人员开发公众号,必须启用服务器配置,而这将导致UI界面设置的自动回复和自定义菜单失效!
我们在 开发 - 基本配置 - 服务器配置 中点击启用:
我们团队就遇到过这种情况:两个项目组共用一个公众号,结果一个启用了服务器配置,使另一个项目组手动配置的菜单失效了。所以要注意这点!
至于服务器配置中的选项代表什么意思、如何填写,我们下面再讲。
我们进入 开发 - 开发者工具, 可以发现微信提供了六种开发者工具,其中前四种属于开发必备:开发者文档、在线接口调试工具、web开发者工具、公众平台测试账号。
1、开发者文档
这个不用说!在我们开发中属于最最最基础和重要的东西了,我们要想熟练开发公众号,首先必须熟读开发者文档!有些功能的开发甚至非要反复研读、咬文嚼字一番不可。PS:该文档吐槽的地方也不少,有些地方的确讲的不够明确!
2、在线接口调试工具
这个工具也算比较实用,包含大多数接口的在线调试,我们可以直接在上面输入参数,获取微信服务端的返回结果。
3、web开发者工具
这个工具是一款桌面应用,需要下载,它通过模拟微信客户端的UI使得开发者可以使用这个工具方便地在PC或者Mac上进行开发和调试工作,一般是前端使用该工具进行页面、接口调试。
4、公众平台测试账号
这个测试号工具对我们的重要性可以说是仅次于开发者文档。我们可以创建测试号,无需申请、认证真实的公众帐号、可在测试帐号中体验并测试微信公众平台所有高级接口。并且所有的配置都可在一个页面上编辑,使开发测试变得极其便利。
需要注意的是,细读开发者文档不是让你所有模块都去阅读,而是重点的重复细读,非重点的选择性阅读。
其中前两个模块:开始前必读、开始开发,属于重点关注对象,也是整个微信开发的基石所在,需要多读几遍。其次是微信网页开发模块的微信网页授权,比较难理解,需要特别注意。其他的模块则根据你们的项目功能需求,有选择性的阅读即可。
这里我就不多罗嗦了,大家看文档去吧!下面我会描述一些重点内容的实际操作情况以及代码,请确保你已经浏览过文档
1.开发环境准备
这里所谓的开发环境准备主要指的是我们项目服务端和微信服务端的网络通讯环境准备。
我们平常开发可能只需要IP端口就能通讯,顶多配置下白名单放行,但微信公众号开发我们需要通过域名通讯(微信会访问我们配置的域名地址:服务器基本配置中的URL,下面会介绍),也就是我们各自开发环境需要拥有独立的域名,微信就能通过这个域名请求到我们的本地开发服务,各自进行开发测试。
而我们一般都是内网开发,整个内网只有一个对外域名,所以这时就需要 内网穿透 ,为我们每个开发人员配置各自开发机器的域名。
那如何进行内网穿透呢?你首先可以找下你们的网管,看他能不能帮你解决,如果不能,那就安装内网穿透工具,我们自己动手!
我选择的内网穿透工具是natapp,这个有免费版、收费版,免费版的域名会随机变化,而收费版可以拥有固定域名,建议选择收费版,9元每月并不贵;大家可以对照natapp的文档安装使用,并不难。
这样我们本地开发环境就拥有自己的域名啦!然后就可以在测试号管理页面配置本地访问地址URL了。
2.服务器基本配置
无论是在真实公众号的 开发 - 基本配置 - 服务器配置,还是在 测试号管理 中,我们都可以看到这几个基本参数:
开发者ID(AppID)、开发者密码(AppSecret)、服务器地址(URL)、令牌(Token)
AppID 是公众号唯一开发识别码,配合开发者密码可调用公众号的接口能力,大多数微信接口都需要附带该参数。
AppSecret 是校验公众号开发者身份的密码,具有极高的安全性。切记勿把密码直接交给第三方开发者或直接存储在代码中。如需第三方代开发公众号,请使用授权方式接入。其中获取accessToken就需要同时传入AppID和AppSecret获取。
URL 是开发者用来接收微信消息和事件的接口URL,也就是我们服务后端的入口地址,需要注意的是该地址必须以域名形式填写,且必须以http 或 https 开头,分别支持80端口和443端口。如:http://yuanj.natapp1.cc/wechat。
Token 可由开发者可以任意填写,用作生成签名(该Token会和接口URL中包含的Token进行比对,从而验证安全性),也就是我们项目和微信服务端进行通信时,必须保证公众平台配置的Token和我们后台代码配置的Token保持一致,这样微信就能验证我们身份。
注:EncodingAESKey 参数由开发者手动填写或随机生成,将用作消息体加解密密钥,我们前期可以采用明文模式进行开发测试,暂时先不用关注。
我们点击提交时,微信会以GET请求的方式访问我们配置的URL地址,并附加几个参数进行验证,所以你需要在该地址对应的项目后端接口里对这几个参数进行加工处理,返回微信需要的结果,这样就可以验证成功,使微信服务端认可你配置的URL和Token参数,后续就能互相通信了!
具体情况可以阅读微信文档 - 开始前必读 - 接入指南。
微信服务器将发送GET请求到填写的服务器地址URL上,GET请求参数如下:
验证签名主要流程:
1)将token、timestamp、nonce三个参数进行字典序排序
2)将三个参数字符串拼接成一个字符串进行sha1加密
3)开发者获得加密后的字符串可与signature对比,标识该请求来源于微信
这里附上我的签名校验的代码。
public String wxOfficialTokenCheck(String signature, String timestamp, String nonce, String echostr) {
log.info("开始校验此次消息是否来自微信服务器,param->signature:{},\ntimestamp:{},\nnonce:{},\nechostr:{}",
signature, timestamp, nonce, echostr);
if (CheckUtils.checkSignature(signature, timestamp, nonce)) {
return echostr;
}
return "";
}
import lombok.extern.log4j.Log4j2;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
/**
* @author wangshanshan
* @date 2021/3/25
*/
@Log4j2
public class CheckUtils {
private static final String TOKEN = "xiaobot";
/**
* 校验微信服务器Token签名
*
* @param signature 微信加密签名
* @param timestamp 时间戳
* @param nonce 随机数
* @return boolean
*/
public static boolean checkSignature(String signature, String timestamp, String nonce) {
String[] arr = {TOKEN, timestamp, nonce};
Arrays.sort(arr);
StringBuilder stringBuilder = new StringBuilder();
for (String param : arr) {
stringBuilder.append(param);
}
String hexString = SHA1(stringBuilder.toString());
return signature.equals(hexString);
}
private static String SHA1(String str) {
MessageDigest md;
try {
md = MessageDigest.getInstance("SHA-1");
byte[] digest = md.digest(str.getBytes());
return toHexString(digest);
} catch (NoSuchAlgorithmException e) {
log.info("校验令牌Token出现错误:{}", e.getMessage());
}
return "";
}
/**
* 字节数组转化为十六进制
*
* @param digest 字节数组
* @return String
*/
private static String toHexString(byte[] digest) {
StringBuilder hexString = new StringBuilder();
for (byte b : digest) {
String shaHex = Integer.toHexString(b & 0xff);
if (shaHex.length() < 2) {
hexString.append(0);
}
hexString.append(shaHex);
}
return hexString.toString();
}
}
3.access_token参数
access_token是公众号的全局唯一接口调用凭据,公众号调用各接口时都需使用access_token。开发者需要进行妥善保存。access_token的存储至少要保留512个字符空间。access_token的有效期目前为2个小时(7200秒),需定时刷新,重复获取将导致上次获取的access_token失效。
access_token这个参数非常重要,几乎贯穿整个微信公关号项目开发,我们如何在有效期内定时刷新获取呢?
如果我们的微信公众号项目是单服务架构,可以直接作为静态变量存储在内存里;如果是多服务,可以用中间件存储,Redis、数据库都可以。SpringBoot项目内部可以通过@Scheduled注解,执行定时任务,既然access_token有效期是2小时,那我们可以一小时刷新获取一次,将其存入Redis,覆盖之前的access_token。
大体思路:
1)生成一个带参数的二维码,用户扫码后会向配置的URL发送事件消息
2)在配置的URL中可以获取到用户的openId以及二维码的参数等信息,若事件为关注和扫描,则将二维码参数保存到redis中,并规划好过期时间
3)前端拿着二维码参数轮询redis,如果redis中有此key,则证明用户已关注
1.生成带参二维码
当前面的签名验证完成后,此时公众号接收到的消息就会放松到我们配置的URL上,对于扫码关注公众号实现登录的逻辑来讲,需要阅读以下模块文档
首先由于需要用户扫码跳转到公众号关注页面,所以先要产生一个带参数的二维码,用户扫描后,公众号可以接收到事件推送。
目前有2种类型的二维码:
临时二维码,是有过期时间的,最长可以设置为在二维码生成后的30天(即2592000秒)后过期,但能够生成较多数量。临时二维码主要用于帐号绑定等不要求二维码永久保存的业务场景
永久二维码,是无过期时间的,但数量较少(目前为最多10万个)。永久二维码主要用于适用于帐号绑定、用户来源统计等场景。
获取带参数的二维码的过程包括两步,首先创建二维码ticket,然后凭借ticket到指定URL换取二维码。
创建二维码ticket
每次创建二维码ticket需要提供一个开发者自行设定的参数(scene_id),分别介绍临时二维码和永久二维码的创建二维码ticket过程。
临时二维码请求说明
http请求方式: POST URL: https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=TOKEN
POST数据格式:
json POST数据例子:{“expire_seconds”: 604800, “action_name”: “QR_SCENE”, “action_info”: {“scene”: {“scene_id”: 123}}}
或者也可以使用以下POST数据创建字符串形式的二维码参数:{“expire_seconds”: 604800, “action_name”: “QR_STR_SCENE”, “action_info”: {“scene”: {“scene_str”: “test”}}}
永久二维码请求说明
http请求方式: POST URL: https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=TOKEN
POST数据格式:
json POST数据例子:{“action_name”: “QR_LIMIT_SCENE”, “action_info”: {“scene”: {“scene_id”: 123}}}
或者也可以使用以下POST数据创建字符串形式的二维码参数: {“action_name”: “QR_LIMIT_STR_SCENE”, “action_info”: {“scene”: {“scene_str”: “test”}}}
在我们的业务里由于不需要保存二维码,所以选择了使用临时二维码,代码如下(这里的代码有不同的Class,我放在了一起,复制时可根据跟人需要灵活摘取和删减):
public ResponseBean<GetQRCodeRes> getQRCode() {
GetQRCodeRes getQRCodeRes = new GetQRCodeRes();
Snowflake snowflake = IdUtil.createSnowflake(SnowFlakeUtil.getWorkId(), SnowFlakeUtil.getDataCenterId());
String scene = String.valueOf(snowflake.nextId());
String qrUrl = WxOfficalApiClient.getQrCode(APPID, SECRET, scene);
getQRCodeRes.setQrCodeUrl(qrUrl);
getQRCodeRes.setScene(scene);
return ResponseBean.create(getQRCodeRes);
}
@Data
@ApiModel("获取带参数的二维码返回值")
public class GetQRCodeRes {
/**
* 二维码url
*/
private String qrCodeUrl;
/**
* redis的key值,可通过此key查询用户是否关注
*/
private String scene;
}
@Log4j2
public class WxOfficalApiClient {
public static String getQrCode(String appId, String appSecret, String scene) {
WxOfficalAccessToken accessToken = WxOfficalApi.getAccessToken(appId, appSecret);
if (accessToken == null) {
return null;
}
if (accessToken.getErrCode() != null) {
log.error("获取access_token错误 {}", accessToken.getErrMsg());
return null;
}
WxOfficalTicket ticket = WxOfficalApi.getTicket(accessToken.getAccessToken(), scene);
if (ticket == null) {
return null;
}
if (ticket.getErrCode() != null) {
log.error("获取ticket错误 {}", ticket.getErrMsg());
return null;
}
return ticket.getUrl();
}
public static WxOfficalUserInfo getUserInfo(String appId, String appSecret, String openId) {
WxOfficalAccessToken accessToken = WxOfficalApi.getAccessToken(appId, appSecret);
WxOfficalUserInfo userInfo = WxOfficalApi.getUserInfo(accessToken.getAccessToken(), openId);
if (userInfo != null) {
if (userInfo.getErrcode() != null) {
log.error("获取微信info错误 {}", userInfo.getErrmsg());
} else {
return userInfo;
}
}
return null;
}
}
@Slf4j
public class WxOfficalApi {
/**
* 获取access_token
*/
private static final String GET_ACCESS_TOKEN = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s";
/**
* 获取创建二维码ticket
*/
private static final String GET_TICKET = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=%s";
/**
* 创建二维码
*/
private static final String GET_QR_CODE = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=%s";
/**
* 获取用户信息
*/
private static final String GET_USER_INFO = "https://api.weixin.qq.com/cgi-bin/user/info?access_token=%s&openid=%s";
public static String getAccessTokenUrl(String appId, String appSecret) {
return String.format(GET_ACCESS_TOKEN, appId, appSecret);
}
public static String getTicketUrl(String accessToken) {
return String.format(GET_TICKET, accessToken);
}
public static String getQRCodeUrl(String ticket) {
return String.format(GET_QR_CODE, ticket);
}
public static String getUserInfoUrl(String accessToken, String openId) {
return String.format(GET_USER_INFO, accessToken, openId);
}
@SuppressWarnings("DuplicatedCode")
public static WxOfficalUserInfo getUserInfo(String accessToken, String openId) {
WxOfficalUserInfo info = null;
JSONObject jsonObject = WxApi.httpsRequest(getUserInfoUrl(accessToken, openId), "GET", null);
if (null != jsonObject && !jsonObject.containsKey("errcode")) {
try {
info = new WxOfficalUserInfo();
info.setSubscribe(jsonObject.getInteger("subscribe"));
info.setOpenid(jsonObject.getString("openid"));
info.setNickname(jsonObject.getString("nickname"));
info.setCity(jsonObject.getString("city"));
info.setCountry(jsonObject.getString("country"));
info.setHeadimgurl(jsonObject.getString("headimgurl"));
info.setUnionid(jsonObject.getString("unionid"));
info.setProvince(jsonObject.getString("province"));
info.setSex(jsonObject.getInteger("sex"));
} catch (JSONException e) {
log.error("用户信息解析异常", e);
}
} else if (null != jsonObject) {
info = new WxOfficalUserInfo();
info.setErrcode(jsonObject.getInteger("errcode"));
info.setErrmsg(jsonObject.getString("errmsg"));
}
return info;
}
/**
* 获取access_token
*/
public static WxOfficalAccessToken getAccessToken(String appId, String appSecret) {
WxOfficalAccessToken info = null;
JSONObject jsonObject = WxApi.httpsRequest(getAccessTokenUrl(appId, appSecret), "GET", null);
if (null != jsonObject && !jsonObject.containsKey("errcode")) {
try {
info = new WxOfficalAccessToken();
info.setAccessToken(jsonObject.getString("access_token"));
} catch (JSONException e) {
log.error("accessToken解析异常", e);
}
} else if (null != jsonObject) {
info = new WxOfficalAccessToken();
info.setErrCode(jsonObject.getInteger("errcode"));
info.setErrMsg(jsonObject.getString("errmsg"));
}
return info;
}
/**
* 根据access_token获取Ticket
*/
public static WxOfficalTicket getTicket(String accessToken, String scene) {
WxOfficalTicket info = null;
WxOfficialTicketRequestParam requestParam = new WxOfficialTicketRequestParam(scene);
JSONObject jsonObject = WxApi.httpsRequest(getTicketUrl(accessToken), "POST",
JSONObject.toJSONString(requestParam));
if (null != jsonObject && !jsonObject.containsKey("errcode")) {
try {
info = new WxOfficalTicket();
info.setTicket(jsonObject.getString("ticket"));
info.setUrl(jsonObject.getString("url"));
info.setExpireSeconds(jsonObject.getLong("expire_seconds"));
} catch (JSONException e) {
log.error("Ticket解析异常", e);
}
} else if (null != jsonObject) {
info = new WxOfficalTicket();
info.setErrCode(jsonObject.getInteger("errcode"));
info.setErrMsg(jsonObject.getString("errmsg"));
}
return info;
}
}
/**
* 雪花算法获取workId和dataCenterId
*
* @author wangshanshan
* @date 2021/3/29
*/
public class SnowFlakeUtil {
public static Long getWorkId() {
try {
String hostAddress = Inet4Address.getLocalHost().getHostAddress();
int[] ints = StringUtils.toCodePoints(hostAddress);
int sums = 0;
for (int b : ints) {
sums += b;
}
return (long) (sums % 32);
} catch (UnknownHostException e) {
// 如果获取失败,则使用随机数备用
return RandomUtils.nextLong(0, 31);
}
}
public static Long getDataCenterId() {
try {
int[] ints = StringUtils.toCodePoints(SystemUtils.getHostName());
int sums = 0;
for (int i : ints) {
sums += i;
}
return (long) (sums % 32);
} catch (NullPointerException e) {
// 如果获取失败,则使用随机数备用
return RandomUtils.nextLong(0, 31);
}
}
}
需要说明的是,项目开始阶段我使用的是scene_id作为二维码携带的参数,文档中的scene_id需要的是32位非0整型,并非是十进制的32位,而是二进制的32位,也就是不超过2^32-1,由于在业务中我选择了使用场景值ID绑定用户,通知前端扫描此二维码的用户已经关注,所以此处的scene_id必须要保证不重复,但是对于32位来讲我并没有想到一个很好的办法保证不重复,因此我选择了使用scene_str,文档介绍是一个字符串型的场景值ID,长度限制为1到64,此处生成的不重复的scene_str我选择了雪花算法(需要导入hutool依赖),用起来很简单,可以适当去了解一下。
2.事件消息回调
用户扫描了带参数的二维码,可能推送以下两种事件:
参数会以xml的数据包格式发到配置的URL,xml格式如下
123456789
MsgType为event,证明是事件消息
Event为subscribe,证明是关注事件
Event为SCAN证明是已关注后的扫描事件
EventKey是场景值ID,即二维码携带的参数
public String wxOfficialCallback(Map<String, String> map) {
try {
Map<String, String> map = XmlUtil.xmlToMap(request.getInputStream());
Assert.notEmpty(map, "微信公众号事件回调xml数据包参数为空");
if ("event".equals(map.get("MsgType"))) {
String jsonString = JSONObject.toJSONString(map);
MessageEventInfo messageEventInfo = JSONObject.parseObject(jsonString, MessageEventInfo.class);
log.info("接收到来自微信服务器的时间消息:{}", jsonString);
String openId = messageEventInfo.getFromUserName();
String event = messageEventInfo.getEvent();
WxOfficalUserInfo wxOfficalUserInfo = userDomainService.getWxOfficalUserInfo(EnumUtil.fromString(AppEnum.class, AppEnum.XIAOBOT.name()), openId);
String sceneId;
if (MessageEventEnum.SUBSCRIBE.getName().equals(event)) {
//关注
log.info("用户{}关注公众号", openId);
sceneId = messageEventInfo.getEventKey().replace("qrscene_", "");
messageEventInfo.setEventKey(sceneId);
//将场景值或者ticket保存到redis
RBucket<Object> bucket = redissonClient.getBucket(qrSceneRedisKeyPrefix + ":" + sceneId);
bucket.set(wxOfficalUserInfo, 2, TimeUnit.MINUTES);
} else if (MessageEventEnum.SCAN.getName().equals(event)) {
//扫码
log.info("用户{}扫描公众号二维码", openId);
sceneId = messageEventInfo.getEventKey();
//将场景值或者Ticket保存到redis
RBucket<Object> bucket = redissonClient.getBucket(qrSceneRedisKeyPrefix + ":" + sceneId);
bucket.set(wxOfficalUserInfo, 2, TimeUnit.MINUTES);
} else if (MessageEventEnum.UNSUBSCRIBE.getName().equals(event)) {
//取消关注
log.info("用户{}取消关注公众号", openId);
}
}
} catch (Exception e) {
log.info("微信公众号事件回调接口异常:{}", e.getMessage());
return "ERROR";
}
return "SUCCESS";
}
@Log4j2
public class XmlUtil {
public static Map<String, String> xmlToMap(InputStream inputStream) {
Map<String, String> map = new HashMap<>();
try {
SAXReader reader = new SAXReader();
org.dom4j.Document document = reader.read(inputStream);
Element root = document.getRootElement();
List<Element> elementList = root.elements();
// 遍历所有子节点
for (Element e : elementList)
map.put(e.getName(), e.getText());
// 释放资源
inputStream.close();
} catch (IOException | DocumentException e) {
log.info("xml转化为map出现异常:{}", e.getMessage());
}
return map;
}
}
@Data
public class MessageEventInfo {
/**
* 用户openid
*/
private String FromUserName;
/**
* 消息类型
*/
private String MsgType;
/**
* 事件类型
*/
private String Event;
/**
* 事件KEY值,获取二维码时的scene_id
*/
private String EventKey;
/**
* 二维码的Ticket
*/
private String Ticket;
}
这里的返回值必须要返回一个字符串,否则会导致扫描后公众号提示“该公众号提供的服务出现故障,请稍后再试”。
3.轮询
轮询接口很简单,就是去查redis中是否有这个场景值ID。
这里我返回的是openId和unionId,是因为在登录的逻辑上需要使用
public ResponseBean<Map<String, String>> pollingLoginStatus(String sceneId) {
RBucket<Object> bucket = redissonClient.getBucket(qrSceneRedisKeyPrefix + ":" + sceneId);
if (bucket.isExists()) {
WxOfficalUserInfo wxOfficalUserInfo = (WxOfficalUserInfo) bucket.get();
HashMap<String, String> map = new HashMap<>();
map.put("openId", wxOfficalUserInfo.getOpenid());
map.put("unionId", wxOfficalUserInfo.getUnionid());
return ResponseBean.create(map);
}
return new ResponseBean<>(-1, "用户未关注");
}
4.登录注册
前端轮询得知用户已关注时就可以进行登录了,登录注册的逻辑需要根据业务需求去做,简单的流程就是获取到微信用户的信息然后在库里新增一个网站用户,绑定上他的openId、unionId等信息,也可以绑定上手机号等信息,这里我就不贴我的代码了,我们的代码业务很繁琐。
这是我在看到这位博主的博客受到的启发,讲的非常清楚,通俗易懂,并且跟着这位博主的引导,我也测试成功了网页授权的逻辑。
接下来我就粘贴一下吧。
注意,这是公众号开发的重难点之一,请把技术文档中的微信网页授权模块多读两遍,然后带着疑问来看我的解析。
(1)先明确为什么需要网页授权?我们的目的是什么?
答:用户在微信客户端中访问第三方网页,公众号可以通过微信网页授权机制,来获取用户基本信息,进而实现业务逻辑。也就是通过这种授权机制,我们能获取微信用户信息,比如:头像、昵称、地区、个性签名等。
(2)既然目的是获取用户基本信息,微信不是提供了专门的接口吗?非要网页授权?
答:在文档的 用户管理 - 获取用户基本信息(UnionID机制) 模块可以看到的确有获取用户基本信息接口:
可以看到,这个接口只需要提供openid或者unionid,即可直接获取用户基本信息。那么问题来了,openid(unionid)又是如何获取呢?
微信平台提供了两种方式获取用户的openid
第一种方式:
用户与公众号产生消息交互时,会以POST请求的方式向我们配置的服务器URL地址发送XML格式的消息,并附带该用户对应公众号的openid!关于什么是消息交互我们可以查看文档中的消息管理模块,比如我们在公众号输入栏中发送文字图片语音等属于普通消息交互,我们关注、取关、点击自定义菜单等属于事件消息交互,每当前端用户进行这个操作时,微信服务端都会向我们项目后台发送POST请求给我们传达信息:
可以看到,这个推送数据包中就包含了用户的消息交互类型、时间以及我们需要的openid!也就是说,无论用户在公众号里干了啥操作,我们都能知道他这个操作干了啥,以及他是谁(openid),这时就能调用 用户管理 - 获取用户基本信息(UnionID机制) 接口获取用户基本信息了。
别高兴太早,这种通过消息交互获取用户信息的方式,用户占主动地位,我们项目后端服务被动接受,那么如果我有个基本需求:我想在自定义菜单 - 对应我们网站的前端页面上展示微信用户基本信息,能做到吗?你如何把后台接收到的消息和前端用户关联绑定?
可见,这种被动的方式并不能实现该功能,我们需要主动出击,在前端就能获取到当前操作用户的openid!
第二种方式:
这种方式就是通过网页授权机制主动出击!详情见下文。
(3)网页授权有哪几种机制?分别是怎样实现?应用于什么场景?
答:主要有两种机制,对应两种scope:
以snsapi_base为scope发起的网页授权,是用来获取进入页面的用户的openid的,并且是静默授权并自动跳转到回调页的。用户感知的就是直接进入了回调页(往往是业务页面)。
以snsapi_userinfo为scope发起的网页授权,是用来获取用户基本信息的。但这种授权需要用户手动同意,并且由于用户同意过,所以无须关注,就可在授权后获取该用户的基本信息。
光看这两句解释你可能有一堆疑问,我们逐一分析:
两种机制的前面授权步骤相同,大概如下:
我们先要按照文档要求构造一个链接:https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
其中重点参数是redirect_uri,这个参数填的既可以是前端项目url,也可以是后端接口url,然后点击这个链接后,微信服务端经过重定向到我们填写的redirect_uri,会在此redirect_uri后拼接上一个code参数!然后前端或者后端通过code参数就可以调微信接口https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code获取openid等信息了:
这里讲下 snsapi_base 和 snsapi_userinfo 的不同点:
首先snsapi_base是静默授权,什么意思呢?就是用户没有感知;与之对应的就是非静默授权的snsapi_userinfo了,这个scope公众号会弹出一个小窗口,需要用户手动点击授权,类似这种:
snsapi_base 的优势在于用户无感知,体验好,方便快捷;劣势在于获取openid后只能通过用户管理 - 获取用户基本信息(UnionID机制) 接口获取用户基本信息,而这种方式需要确保用户已经关注,不然是没有相关信息的!
snsapi_userinfo 的优势在于无需用户关注公众号,只要用户点击了授权确认,即可通过access_token和openid调用专门的拉去用户信息接口获取信息,比较暴力。。;劣势在于需要用户手动授权,可能影响用户体验。
在此说下,我们项目是通过snsapi_base静默授权的,其中redirect_uri配置的是前端项目首页地址(前后端分离),并将构造的这个链接封装起来,直接配置在自定义菜单里,那么用户点击菜单,就直接重定向到前端项目,然后前端获取code参数调用后端获取openid接口,将获取的openid缓存到客户端,以便后面使用。
(4)想要进行网页授权,我们需要在公众平台配置什么吗?
答:需要!
如果是测试号,需要在 测试号管理 - 体验接口权限表 - 网页服务 - 网页帐号 点击 修改。
在这里配置的是回调页面即redirect_uri的域名!
如果是正式号(需要微信认证),需要在 开发 - 接口权限 - 网页服务 - 网页帐号 - 网页授权获取用户基本信息 的配置选项中,修改授权回调域名。请注意,这里填写的是域名(是一个字符串),而不是URL,因此请勿加 http:// 等协议头;
而且正式号其他配置的地方也和测试号不一样,比如多了IP白名单、域名根路径下的txt验证文件,这个稍微摸索下应该没啥问题的。
借鉴于:微信公众号开发基本流程