- title: 微信公众号开发:获取openId和用户信息
- tags: 微信公众号
- categories: 笔记
- date: 2017-05-01 14:35:34
最近一直在做微信公众号项目的开发,会有一种感觉,就是微信相关的开发真TM的啰嗦麻烦啊。当首次接触这个微信相关内容的开发时候,需要按照其官方文档一步一步的按部就班,按照他的要求去操作。若是中途需要什么其他的开发资质或者权限,还要进行申请,不仅要花钱,重点还特么的很花时间,直接来几个工作日,我晕菜了。。不过多吐槽一句,微信这边的跟阿里云平台那边处理速度,真的差太远了~~
不过,若是有一次的微信开发经验,我相信日后再次进行微信相关内容的开发就不会有什么难度了,只要不在申请微信七七八八的东西上面花费太多时间就好了。(方式多种,符合微信官方规范即可,此处仅作参考)
要进行微信公众号开发,那么要申请一个微信公众号就必不可少了。现在微信公众号类型有这几种:订阅号,企业号,服务号。关于三者的差别和介绍可以参考微信公众号官网微信公众平台。申请通过之后,就能登陆微信公众平台管理平台进行开发设置了。
每个公众号申请成功后,微信服务那边就会给每个公众号分配一个与公众号唯一性绑定的APPID。结合我们公众平台自己生成的密钥,两者就可以唯一确定某个公众号以及是否合法了。接下来就应该参照微信公众平台开发文档来进行开发前的配置设置。
开发前服务器配置
按照文档所说,接入微信公众平台开发,开发者需要按照如下步骤完成:
1. 填写服务器配置
2. 验证服务器地址的有效性
3. 依据接口文档实现业务逻辑
接下来就上述每个步骤进行说明好了。
(1) 填写服务器配置:
在申请成功公众号后,登入web微信公众号管理平台,在管理平台首页左边的导航栏选择: 开发 --> 基本配置。就会进入改公众号的开发基本配置管理。就可以看到有个区域是服务器配置,这个服务器配置,就是填写我们将开发测试完成后的项目部署所在的服务器,且要填写已经备案后域名地址(若是要进行微信支付开发)。
在填写完所有服务器配置信息后,点击提交,那么微信服务器会发送一个GET请求到我们上述URL填写的地址上,我们服务器上通过接收微信服务器发送请求进行按照某种规则进行处理,将得到的结果返回微信服务器,进行判断我们的服务器是否通过校验,那么我们服务器上的代码要如何处理这个校验请求呢,往下看。
(2) 验证服务器地址的有效性:
开发者提交信息后,微信服务器将发送GET请求到填写的服务器地址URL上,GET请求携带四个参数:
参数 描述
signature 微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数。
timestamp 时间戳
nonce 随机数
echostr 随机字符串
开发者通过检验signature对请求进行校验,若确定此次GET请求来着微信服务器,请原样返回echostr参数内容,则接入生效,成为开发者成功,否则接入失败。(这一步也就是向微信服务器说明,上述我们配置的服务器确实是我们项目所在服务器,在接下来的开发,测试过程中,都会在外网的这个服务器上进行,微信那边会认为是合法的)
加密/校验流程如下:
1. 将token、timestamp、nonce三个参数进行字典序排序
2. 将三个参数字符串拼接成一个字符串进行sha1加密
3. 开发者获得加密后的字符串可与signature对比,标识该请求来源于微信
好了,来看看我们在java后台中,如何对微信这个请求进行处理的:结合上图的URL地址,会通过springmvc被路由派发到指定类的指定方法中进行处理。
@Controller(value="WXConfigUtil")
@Qualifier(value="WXConfigUtil")
@RequestMapping(value="/wXConfigUtil")
public class WXConfigUtil{
//这个token要与公众平台服务器配置填写的token一致
private final static String token = "wechat";
private Log log = LogFactory.getLog(WXConfigUtil.class);
@RequestMapping(value="/WXConfig",method=RequestMethod.GET)
@ResponseBody
public String verifyWXConfig(@RequestParam(value="signature",required=false) String signature,
@RequestParam(value="timestamp",required=false) String timestamp,
@RequestParam(value="nonce",required=false) String nonce,
@RequestParam(value="echostr",required=false) String echostr) {
System.out.println(" PARAM VAL: >>>" + signature + "\t" + timestamp + "\t" + nonce + "\t" + echostr);
log.info("开始签名验证:"+" PARAM VAL: >>>" + signature + "\t" + timestamp + "\t" + nonce + "\t" + echostr);
if (StringUtils.isNotEmpty(signature) && StringUtils.isNotEmpty(timestamp)
&&StringUtils.isNotEmpty(nonce) && StringUtils.isNotEmpty(echostr)) {
String sTempStr = "";
try {
sTempStr = SHA1.getSHA1(timestamp, nonce, token, "");
} catch (Exception e) {
e.printStackTrace();
}
if (StringUtils.isNotEmpty(sTempStr) && StringUtils.equals(signature, sTempStr)) {
log.info("验证成功:-----------:"+sTempStr);
return echostr;
} else {
log.info("验证失败:-----------:00000");
return "-1";
}
} else {
log.info("验证失败:-----------:11111");
return "-1";
}
}
通过校验后,会在公众号的服务器配置页面有提示的,通过校验后,后面就可以在此服务器上进行开发与测试了。
(3)依据接口文档实现业务逻辑
用户向公众号发送消息时,公众号方收到的消息发送者是一个OpenID,是使用用户微信号加密后的结果,每个用户对每个公众号有一个唯一的OpenID。
此外,由于开发者经常有需在多个平台(移动应用、网站、公众帐号)之间共通用户帐号,统一帐号体系的需求,微信开放平台(open.weixin.qq.com)提供了UnionID机制。开发者可通过OpenID来获取用户基本信息,而如果开发者拥有多个应用(移动应用、网站应用和公众帐号,公众帐号只有在被绑定到微信开放平台帐号下后,才会获取UnionID),可通过获取用户基本信息中的UnionID来区分用户的唯一性,因为只要是同一个微信开放平台帐号下的移动应用、网站应用和公众帐号,用户的UnionID是唯一的。换句话说,同一用户,对同一个微信开放平台帐号下的不同应用,UnionID是相同的。详情请在微信开放平台的资源中心-移动应用开发-微信登录-授权关系接口调用指引-获取用户个人信息(UnionID机制)中查看。
网页授权获取用户基本信息
如果用户在微信客户端中访问第三方网页,这个场景就是在我们公众号中通过菜单或者其他连接地址访问我们自己开发的三方网页,公众号可以通过微信网页授权机制来获取用户基本信息,从而实现业务逻辑。详情参考:网页授权获取用户基本信息
微信对于这个微信用户访问的三方页面的授权是通过OAuth2.0鉴权的,现在很普遍的一个用户授权机制。在官方文档中有说明,若是需要在网页中授权操作,那么需要填写配置授权回调域名。仅仅是填写一个不带http或者https的域名字符串。该授权回调域名会在下面的网页授权过程中,重定向的时候进行域名校验。若是不填写,或者填写有误,则网页授权接口调用会失败。
在微信公众号管理平台首页中,点击左边导航栏:"设置" ---> "公众号设置" --->"功能设置"。就会看到包括图片水印,业务域名,JS接口安全域名,网页授权域名。我们要设置的就是网页授权回调域名,点击网页授权域名栏的设置按钮,进行域名设置:
(例如直接写 wx.example.com)
只是需要下载图片上的一个text文件,上传到服务器指定位置,能通过url直接访问,让微信服务器可以访问进行字符串对比校验即可,配置成功。
在微信客户端的网页开发的授权中,有两种授权范围scope:snsapi_base和snsapi_userinfo。
1、以snsapi_base为scope发起的网页授权,是用来获取进入页面的用户的openid的,并且是静默授权并自动跳转到回调页的。用户感知的就是直接进入了回调页(往往是业务页面)
2、以snsapi_userinfo为scope发起的网页授权,是用来获取用户的基本信息的。但这种授权需要用户手动同意,并且由于用户同意过,所以无须关注,就可在授权后获取该用户的基本信息。
3、用户管理类接口中的“获取用户基本信息接口”,是在用户和公众号产生消息交互或关注后事件推送后,
才能根据用户OpenID来获取用户基本信息。这个接口,包括其他微信接口,都是需要该用户(即openid)关注了公众号后,才能调用成功的。
可以看到,在微信公众号开发中,对与获取微信用户信息其实是有几种不同的方式的。分别针对不同的实际场景下,若是要获取的微信用户并没有关注我们的公众号,我们只能通过网页授权auth2.0,来让页面显示提示获取用户信息,让用户来决定是否同意让我们公众号来获取他信息;另一方面,若是微信用户已经关注我们的公众号话,我们就有权限直接通过指定接口获取用户信息,而无需让用户授权。
下面来说说通过网页授权方式,获取未关注公众号的微信用户信息。
snsapi_base
这个授权叫"静默授权",意思就是在用户打开我们开发的三方网页页面的时候,并不会显示的弹出一个授权页面,让用户知道要授权。而是进入页面就默认可以获取用户的openId。这个静默授权仅仅只能拿到微信用户的openId就结束了。
(1) 获取code
必须是在微信客户端,引导微信用户访问下面文档指定的url。注意,我们可以修改的仅仅是接口中重定向redirect_uri部分,可以重定向到我们自己开发的页面中。这里要注意的是:<font color="red">这个重定向的url,跳转回调redirect_uri必须要经过URLEncoder编码</font>。
参考链接(请在微信客户端中打开此链接体验)
Scope为snsapi_base
https://open.weixin.qq.com/connect/oauth2/authorize?appid=wx520c15f417810387
&redirect_uri=https%3A%2F%2Fchong.qq.com%2Fphp%2Findex.php%3Fd%3D%26c%3DwxAdapter%26m%3DmobileDeal%26showwxpaytitle%3D1%26vb2ctag%3D4_2030_5_1194_60
&response_type=code&scope=snsapi_base&state=123#wechat_redirect
看看在java项目中,如何进行这个操作:
@RequestMapping(value="/routerToMyPage.html",method=RequestMethod.GET)
public void redirectToMyPage(HttpServletRequest request,HttpServletResponse response){
StringBuffer sb = new StringBuffer();
StringBuffer encodeUrl = new StringBuffer(300);
//公众号中配置的回调域名(网页授权回调域名)
String doname = ConfigService.getValue("DONAME", "doname");
String root = request.getContextPath();
String appId = Constants.APPID;
sb.append("https://open.weixin.qq.com/connect/oauth2/authorize?appid=");
sb.append(appId);
String url = "";
try {
//对重定向url进行编码,官方文档要求
encodeUrl.append(doname).append(root).append("/my/myPage.html");
// url = URLEncoder.encode(encodeUrl.toString(), "utf-8");
url = URLEncoder.encode("https://domain/xbdWXBG(项目名称)/my/myPage.html", "utf-8");
sb.append("&redirect_uri=").append(url);
//网页授权的静默授权snsapi_base
sb.append("&response_type=code&scope=snsapi_base&state=123#wechat_redirect");
response.sendRedirect(sb.toString());
} catch (UnsupportedEncodingException e) {
log.error("重定向url编码失败:>>" + e.getMessage());
e.printStackTrace();
} catch (Exception e) {
log.error("response重定向失败:>>" + e.getMessage());
e.printStackTrace();
}
}
在上述代码中,当用户进入到routerToMyPage.html页面进行请求,那么就能通过回调uri进行我们其他的页面中,就能拿到code。因为在跳转到重定向页面过程中,微信服务器会将一个请求参数code值携带在请求url中。
(2) 得到openId
在静默授权的第(1)步中,页面会携带code参数,重定向跳转到myPage.html页面中,那么我们就可以在这个页面中获取code的值,并根据该值调用指定接口获得当前微信用户的openId。
@RequestMapping(value="/myPage.html",method=RequestMethod.GET)
public ModelAndView toMyPage(HttpServletRequest request,HttpServletResponse response){
....
ModelAndView mv = new ModelAndView("/mypage/mypage");
//获取重定向携带的code参数值
String code = request.getParameter("code");
Object openId = "";
if (null == openId) {
/*
* 根据得到的code参数,内部请求获取openId的方法。
*/
openId = getOpenId(request,response,code);
}
log.info("session中得到的openId值为:>>" + String.valueOf(openId));
//根据openId查询用户信息
Users user = myPageService.getUserByOpenId(String.valueOf(openId));
...
return mv;
}
//发送请求,根据code获取openId
public String getOpenId(HttpServletRequest request, HttpServletResponse response,String code) {
String content = "";
String openId = "";
String unionId = "";
//封装获取openId的微信API
StringBuffer url = new StringBuffer();
url.append("https://api.weixin.qq.com/sns/oauth2/access_token?appid=")
.append(Constants.APPID)
.append("&secret=")
.append(Constants.APPSECRET)
.append("&code=")
.append(code)
.append("&grant_type=authorization_code");
ObjectMapper objectMapper = new ObjectMapper();
try {
content = HttpClient.requestGet(url.toString());
Map map = objectMapper.readValue(content, Map.class);
openId = String.valueOf(map.get("openid"));
unionId = String.valueOf(map.get("unionid"));
log.info("获取的openID:" + openId);
/*
* 将openId保存到session中,当其他业务获取openId时,
* 可先从session中获取openId.
*/
request.getSession().setAttribute("openId", openId);
} catch (JsonParseException e) {
log.error("json解析失败:", e);
} catch (JsonMappingException e) {
log.error("map转换成json失败:", e);
} catch (Exception e) {
log.error("http获取openId请求失败:", e);
}
return openId;
}
可以看到getOpenId方法中,通过code值和appid,secret发送了一个http请求,用于获取用户的openId,请求成功返回如下格式内容:
{
"access_token":"ACCESS_TOKEN",
"expires_in":7200,
"refresh_token":"REFRESH_TOKEN",
"openid":"OPENID",
"scope":"SCOPE"
}
##参数说明
access_token 网页授权接口调用凭证,注意:此access_token与基础支持的access_token不同
expires_in access_token接口调用凭证超时时间,单位(秒)
refresh_token 用户刷新access_token
openid 用户唯一标识,请注意,在未关注公众号时,用户访问公众号的网页,也会产生一个用户和公众号唯一的OpenID
scope 用户授权的作用域,使用逗号(,)分隔
因为同一个微信用户对每一个微信公众号来说,都有唯一的标识就是这个openId。所以,我们拿到openId就能针对这个指定用户来做一些其他的业务操作,静默授权snsapi_base就到此结束了,并不会获取得到微信的用户信息。
snsapi_userinfo
若是不仅仅想要获取微信用户的openId,还想获取未关注公众号的微信用户信息,那么可以通过snsapi_userinfo授权来实现。
(1) 重定向获取code
Scope为snsapi_userinfo
https://open.weixin.qq.com/connect/oauth2/authorize?appid=wxf0e81c3bee622d60
&redirect_uri=http%3A%2F%2Fnba.bluewebgame.com%2Foauth_response.php&response_type=code
&scope=snsapi_userinfo&state=STATE#wechat_redirect
当微信用户点击这个页面,会跳出授权页面,如下图:
(2) 得到openId
与静默授权一样,重定向到我们开发的三方页面,则可以先获取code值,在根据code值发送下面的连接请求,得到openId:
//通过request.getParameter("code")获取code
获取code后,请求以下链接获取access_token:
https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code
可以参考静默授权java代码实现详情,此处省略。
(3) 获取个人信息
在拿到微信用户对应的openId之后,因为之前用户已经同意授权后,就能通过以下请求连接获取未关注的用户个人信息:
http:GET(请使用https协议)
https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN
此处要注意的是access_token值,这个access_token的值是上面(2)方法调用返回的token,并不是JSSDK网页调用基础支持全局的token。响应返回json格式的用户个人信息。
用户管理-获取用户基本信息
微信公众号开发文档中,有指定的api 接口可以让我们调用,获取微信用户的基本信息。这个接口调用的成功的前提条件是:所要获取的微信用户信息是已经关注了我们的微信公众号的。若是该微信用户没有关注,则不能通过此接口调用,只能通过上述的网页授权方式获取用户信息。详情参考:用户管理-获取用户基本信息
(1) 获取用户基本信息api
文档中声明的调用接口如下:
http请求方式: GET
https://api.weixin.qq.com/cgi-bin/user/info?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN
## 参数说明
access_token 是 调用接口凭证
openid 是 普通用户的标识,对当前公众号唯一
lang 否 返回国家地区语言版本,zh_CN 简体,zh_TW 繁体,en 英语
这里要注意的是这个access_token是微信接口调用的凭证。与网页授权通过code拿到的access_token是不同的。这个接口凭证,是全局token,也就是说,若是项目中要调用所有微信其他jssdk等接口,请求url参数中多数时候都是需要这个参数token值的。具体的可以查看下面的第(2)步获取全局token。再次,就设定已经拿到了全局接口调用的access_token,并将该token保存到内存中。看看java中如何调用接口获取微信用户信息。
public Users getWechatUserInfo(String openId) {
//获取保存在内存中的全局接口调用access_token
String accessToken = Constants.ACCESS_TOKEN;
log.info("全局token>>" + accessToken);
//构造获取用户基本信息api
StringBuffer url = new StringBuffer();
url.append("https://api.weixin.qq.com/cgi-bin/user/info?")
.append("access_token=").append(accessToken)
.append("&openid=").append(openId).append("&lang=zh_CN");
String content = "";
ObjectMapper objectMapper = new ObjectMapper();
Users user = null;
try {
for (int i = 1; i <= 3; i++) {
//content就是json格式的用户信息
content = httpUtil.executeGet(url.toString());
log.info("获取微信用户请求响应信息:>>" + content);
Map map = objectMapper.readValue(content, Map.class);
Object mopenId = map.get("openid");
Object nickName = map.get("nickname");
log.info("第" + i + "次获取openId=" + openId + "的微信用户昵称:>>"+ nickName);
if (openId.equals(mopenId) && nickName != null) {
/*
* 获取微信用户基本信息成功,并将信息封装到平台用户对象中。
*/
// user = myPageDao.getUserByOpenId(openId);
user = new Users();
if(user != null) {
user.setNickname(String.valueOf(nickName));
// user.setName(String.valueOf(nickName));
user.setSex((Integer) map.get("sex"));
user.setPictureURL(String.valueOf(map.get("headimgurl")));
user.setOpenid(String.valueOf(mopenId));
user.setUnionID(String.valueOf(map.get("unionid")));
}
log.info("调用微信得到的用户信息:>>" + user.getNickname() + ",photo>>"+ user.getPictureURL());
return user;
}
log.info("第" + i + "次获取openId=" + openId + "的微信用户信息失败!!");
}
} catch (JsonParseException e) {
log.error("获取微信基本用户信息时,json转换失败:>>", e);
e.printStackTrace();
} catch (Exception e) {
log.error("http请求执行错误:>>", e);
e.printStackTrace();
}
return user == null ? new Users() : user;
}
(2) 获取全局接口调用token
access_token是公众号的全局唯一票据,公众号调用各接口时都需使用access_token。开发者需要进行妥善保存。access_token的存储至少要保留512个字符空间。access_token的有效期目前为2个小时,需定时刷新,重复获取将导致上次获取的access_token失效。详情:获取access token
我们可以通过定时器定时调用获取token的api请求,得到这个access_token就保存在内存中,若是其他接口需要使用,直接调用即可。这里官方也有说明:如果第三方不使用中控服务器,而是选择各个业务逻辑点各自去刷新access_token,那么就可能会产生冲突,导致服务不稳定。,也就是不建议我们频繁的手动调用这个api来更新access_token。
http请求方式: GET
https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
## 参数说明
grant_type 是 获取access_token填写client_credential
appid 是 第三方用户唯一凭证
secret 是 第三方用户唯一凭证密钥,即appsecret
## 返回
{"access_token":"ACCESS_TOKEN","expires_in":7200}
来看看java中如何获取这个全局接口调用token:
private void getAccessToken() {
StringBuffer url = new StringBuffer();
url.append("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential")
.append("&appid=").append(Constants.APPID)
.append("&secret=").append(Constants.APPSECRET);
log.info("获取全局accesss_token的请求:>>" + url.toString());
try {
String content;
ObjectMapper objectMapper = new ObjectMapper();
/*
* 发送请求获取access_token,最多发送3次请求进行获取。
*/
for(int i = 1; i <= 3; i++) {
if(httpUtil == null) {
httpUtil = new HttpUtil();
}
content = httpUtil.executeGet(url.toString());
try {
Map map = objectMapper.readValue(content, Map.class);
Object at = map.get("access_token");
log.info("第" + i + "次定时器获取全局access_token:>>" + at);
if(null != at) {
//刷新内存中的全局ACCESS_TOKEN值。
Constants.ACCESS_TOKEN = String.valueOf(at);
log.info("全局access_token刷新成功!!");
break;
}
log.info("全局access_token刷新失败!!");
} catch (Exception e) {
log.error("获取全局access_token时,json转换失败:" + e.getMessage());
break;
}
}
} catch (Exception e) {
log.error("获取全局access_token失败:" + e.getMessage());
}
}
在来多说几句,这个如何用定时器来统一刷新内存中全局token,主要结合quartz定时器来实现。
第一步,要先引入定时器quartz依赖库:
<!-- 定时器 -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>1.8.6</version>
</dependency>
第二步,实现定时器调用的job具体实现类:
@Service("accessTokenService")
public class AccessTokenService {
//定时器调用方法
public void execute() {
getAccessToken();//上述代码已经列出
}
/**
* refresh: 提供一个入口,进行强制手动刷新token。
*/
public static void refresh() {
new AccessTokenService().getAccessToken();
}
}
第三步,在spring配置文件中配置定时器:
## applicationContext.xml
<!-- 应用程序定时器配置 -->
<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers">
<list>
<ref bean="accessTokenTrigger" />
</list>
</property>
<property name="autoStartup" value="true" />
</bean>
<!-- 配置定时器:每2小时刷新一次微信接口调用全局token -->
<bean id="accessTokenTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean">
<property name="jobDetail" ref="tokenJobDetail" />
<property name="cronExpression" value="0 0 */2 * * ?" /><!-- 每隔2个小时触发一次 -->
</bean>
<bean id="tokenJobDetail"
class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<property name="targetObject" ref="accessTokenService" />
<property name="targetMethod" value="execute" />
<property name="concurrent" value="false" />
<!-- 是否允许任务并发执行。当值为false时,表示必须等到前一个线程处理完毕后才再启一个新的线程 -->
</bean>
第四步,要使这个定时器在服务器启动时候,必须调用一次。基于服务器启动时间,每间隔2个小时就进行token刷新:
需要我们在web.xml配置文件中配置监听器listener,定义一个在服务器启动时候,就进行调用的类方法。
## web.xml
<listener>
<listener-class>com.cybbj.utils.AccessTokenInit</listener-class>
</listener>
## AccessTokenInit.java 集成ServletContextListener实现服务器启动监听
public class AccessTokenInit implements ServletContextListener{
public void contextInitialized(ServletContextEvent sce) {
AccessTokenService.refresh();
}
public void contextDestroyed(ServletContextEvent sce) {
}
}
总结
至此,这个微信公众号的基本基础开发配置完成了,后面还有JSSDK页面开发要进行说明。在微信开发过程中,只要严格按照微信官方的开发文档进行操作就应该没有什么大的问题,感觉都是调用接口api,没啥难度....
不过,还是有很长的路要走啊....