微信公众号踩坑之旅
2019-02-20 15:26:27 By Magina
开发过程中使用的是微信测试公众号
在线申请:
https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login
第二步准备工作.获取开发环境的影射外网地址:ngrok百度自行下载,
打开ngrok.exe -> ngrok http 8080 // 此处同样可以指定其他端口.例如8087
此时:外网穿透已经成功.http和https理论上都可以使用.我使用的是http.
下面去测试号 页面查找网页授权域名: 注意.不带http://
再设置js接口域名
此时页面的配置基本完成.我们需要接入到 微信接口配置信息:
微信提供一系列的规则.测试号相对简单,只需提供接口url和token,token自定义.需要和我们后台项目设定的一致.url指向后台项目的接口校验.
项目搭建不多做介绍.
官方提供信息:
开发者提交信息后,微信服务器将发送GET请求到填写的服务器地址URL上,GET请求携带参数如下表所示:
signature
微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数。
timestamp
时间戳
nonce
随机数
echostr
随机字符串
开发者通过检验signature对请求进行校验(下面有校验方式)。若确认此次GET请求来自微信服务器,请原样返回echostr参数内容,则接入生效,成为开发者成功,否则接入失败。加密/校验流程如下:
1)将token、timestamp、nonce三个参数进行字典序排序
2)将三个参数字符串拼接成一个字符串进行sha1加密
3)开发者获得加密后的字符串可与signature对比,标识该请求来源于微信
项目代码controller:
// token
private final String token = "cass";
@GetMapping(value = "/checkSignature")
public void testInfo(HttpServletRequest request, HttpServletResponse response) throws Exception {
log.info("=============================进入Wechat==================================");
log.info(request.toString());
System.out.println("开始签名校验");
String signature = request.getParameter("signature");
String timestamp = request.getParameter("timestamp");
String nonce = request.getParameter("nonce");
String echostr = request.getParameter("echostr");
//排序
String sortString = sort(token, timestamp, nonce);//按照微信官方文档提供的规则 组合String
//加密
String mytoken = Decript.SHA1(sortString);
//校验签名
if (mytoken != null && mytoken != "" && mytoken.equals(signature)) {
System.out.println("签名校验通过。");
response.getWriter().print(echostr); //如果检验成功输出echostr,微信服务器接收到此输出,才会确认检验完成。
}
}
public static String sort(String token, String timestamp, String nonce) {
String[] strArray = {token, timestamp, nonce};
Arrays.sort(strArray);
StringBuilder sbuilder = new StringBuilder();
for (String str : strArray) {
sbuilder.append(str);
}
return sbuilder.toString();
}
Decript(sha1加密类):
public class Decript {
public static String SHA1(String decript) {
try {
MessageDigest digest = MessageDigest
.getInstance("SHA-1");
digest.update(decript.getBytes());
byte messageDigest[] = digest.digest();
// Create Hex String
StringBuffer hexString = new StringBuffer();
// 字节数组转换为 十六进制 数
for (int i = 0; i < messageDigest.length; i++) {
String shaHex = Integer.toHexString(messageDigest[i] & 0xFF);
if (shaHex.length() < 2) {
hexString.append(0);
}
hexString.append(shaHex);
}
return hexString.toString();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return "";
}
}
很简单理解.微信官方根据你给的token会生成一个签名.然后参数给开发者.开发者也必须生存一个签名.签名equals相等.ok.你即可以成为开发者
项目url:http://1527ade6.ngrok.io/Wechat/checkSignature
坑: 这个url里面一定不能带端口号.就算用nginx配置过也需要改掉.项目里被坑过
token:cass
此时.开发者账号已经接入成功了.
我们可以先通过微信官方的api接口 测试一些功能.比如自定义菜单跳转后台
接口api:https://mp.weixin.qq.com/debug/cgi-bin/apiinfo?
https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=wx00ab3357d8686b43&secret=39d83b4cad24fdcf519bb998c9b9e30f
返回结果:
* 200 OK
* Connection: close
* Date: Wed, 20 Feb 2019 07:36:19 GMT
* Content-Type: application/json; encoding=utf-8
* Content-Length: 173
*
{
"access_token": "18_NWcKxgaJP6EnY7QO-QuJSZlxIxpAt6ekwIHm-B7xRkHuKJwOwYra6YW2qRQc_9rCrm2d1o7yURT3Yw5H8ClA6gIfg2wSa9pZAcWsx-NEi5am2T6WU_kDKMk9TYoQZEiAGAHQV",
"expires_in": 7200
}
如果返回非200就是有问题.这步很简单.如有问题百度
https://api.weixin.qq.com/cgi-bin/menu/get?access_token=18_NWcKxgaJP6EnY7QO-QuJSZlxIxpAt6ekwIHm-B7xRkHuKJwOwYra6YW2qRQc_9rCrm2d1o7yURT3Yw5H8ClA6gIfg2wSa9pZAcWsx-NEi5am2T6WU_kDKMk9TYoQZEiAGAHQV
返回结果:
* 200 OK
* Connection: keep-alive
* Date: Wed, 20 Feb 2019 08:10:35 GMT
* Content-Type: application/json; encoding=utf-8
* Content-Length: 408
*
{
"menu": {
"button": [
{
"type": "view",
"name": "测试案例",
"url": "http://87af154f.ngrok.io/Wechat/testData",
"sub_button": [ ]
},
{
"type": "view",
"name": "摇珠计划",
"url": "https://open.weixin.qq.com/connect/oauth2/authorize?appid=WWWWWredirect_uri=http://87af154f.ngrok.io/Wechat/tologin/userinforesponse_type=codescope=snsapi_userinfostate=STATE#wechat_redirect",
"sub_button": [ ]
}
]
}}
提示:
Request successful
这是我的正常返回值.查看菜单接口
下面来自定义菜单.拼接menu的json:
{
"button": [
{
"type": "view",
"name": "测试十三",
"url": "http://1527ade6.ngrok.io/Wechat/testData",
"sub_button": [ ]
},
{
"type": "view",
"name": "计划十三",
"url": "https://open.weixin.qq.com/connect/oauth2/authorize?appid=wx00ab3357d8686b43&redirect_uri=http://1527ade6.ngrok.io/Wechat/tologin/userinfo&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect",
"sub_button": [ ]
}
]
}
来说下这边的坑:
1.json不需要带menu层.只需要button这层
2:url最好带上http.这里的http只得是本身跳转的url和rediect_uri的url.微信的是https不需要管
3.下面的url等下讲解
http://1527ade6.ngrok.io/Wechat/testData 对应的是项目接口的localhost:8080/Wechat/testData
代码:
@GetMapping(value = "/testData")
public String testInfo() throws Exception {
log.info("测试通过了啊");
return "测试通过了啊";
}
自定义菜单创建成功.ok这时候我们拿自己的微信去扫二维码关注看下结果
这是我手机关注后的菜单显示.
简单的自定义菜单已经完成.
下面说下"计划十三"这个button的url:https://open.weixin.qq.com/connect/oauth2/authorize?appid=wx00ab3357d8686b43&redirect_uri=http://1527ade6.ngrok.io/Wechat/tologin/userinfo&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect
这个是微信网页授权的url.访问微信的接口,会回调到http://1527ade6.ngrok.io/Wechat/tologin/userinfo我们这个接口.在这个接口里面.我们回得到当前用户的微信基本信息.从授权说起.
官方文档:https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140842
想从微信按钮跳转到我们自定义网页(第三方).需要用户对此网页授权.才能继续.微信提供静默授权和手动授权.很简单.静默就是你点击了按钮.微信后台自动帮你授权了.再跳转第三方网页.手动就会弹出一个授权的页面点击授权才能跳转第三方网页.官方给出2中的url:
scope为snsapi_base静默授权
https://open.weixin.qq.com/connect/oauth2/authorize?appid=wx3431726cd6f932f4&redirect_uri=http://pds.movitech.cn:8686/Wechat/tologin/userinfo&response_type=code&scope=snsapi_base&state=123#wechat_redirect
scope为snsapi_userinfo手动授权.
https://open.weixin.qq.com/connect/oauth2/authorize?appid=wx3431726cd6f932f4&redirect_uri=http://pds.movitech.cn:8686/Wechat/tologin/userinfo&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect
只需要把appid和rediect_uri替换调就可以
接口实例,我的项目例子是这样:前后端分离.此接口既可以给前端调用,也可以给微信调用.调用授权跳转接口.如果此用户不再项目系统里面用手机号码注册过.就返回前端一个false.否则就查找出planList业务数据给前端.
@GetMapping("/tologin/userinfo")
@ApiOperation(value = "传入code登录微信接口", notes = "前端传入code,调微信接口,获取微信用户信息,返回值:status为true则是此微信号绑定过,返回planList计划列表.status为flase" +
"微信号尚未绑定手机号码,需要跳转手机验证码页面")
public Response check(HttpServletRequest request, HttpSession session, Map map) throws BusinessException {
log.info("=======================进入tologin/userinfo");
//首先判断一下session中,是否有保存着的当前用户的信息,有的话,就不需要进行重复请求信息
WeiXinUser weiXinUser = null;
if (session.getAttribute("currentUser") != null) {
weiXinUser = (WeiXinUser) session.getAttribute("currentUser");
} else {
/**
* 进行获取openId,必须的一个参数,这个是当进行了授权页面的时候,再重定向了我们自己的一个页面的时候,
* 会在request页面中,新增这个字段信息,要结合这个ProjectConst.Get_WEIXINPAGE_Code这个常量思考
*/
String code = request.getParameter("code");
// code = "021Ct0F42we4lP0ohqI42664F42Ct0Fg";
try {
//得到当前用户的信息(具体信息就看weixinUser这个javabean)
weiXinUser = getTheCode(session, code);
//将获取到的用户信息,放入到session中
session.setAttribute("currentUser", weiXinUser);
} catch (Exception e) {
e.printStackTrace();
}
}
if(ObjectUtils.isEmpty(weiXinUser)){
return Response.fail("3999","查找微信用户失败!");
}
List planList = wechatService.getPlanListByOpenId(weiXinUser.getOpenId());
if(CollectionUtils.isEmpty(planList) && null != planList){
map.put("status",true);
map.put("planList",planList);
}else{
map.put("status",false);
}
map.put("weiXinUser", weiXinUser);
return Response.succeed(map);
}
看到这里很疑惑.为什么直接从String code = request.getParameter("code");
这个是当进行了授权页面的时候,再重定向了我们自己的一个页面的时候,
微信会在request页面中,新增这个字段信息.不信的话我们可以测试:
打开微信开发者工具.1获取token.2.获取button.3复制button的url:https://open.weixin.qq.com/connect/oauth2/authorize?appid=wx00ab3357d8686b43&redirect_uri=http://1527ade6.ngrok.io/Wechat/tologin/userinfo&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect
粘贴到地址栏enter.演示利用微信工具获取code.最后一个接口的请求参数里面可以看到
看到上图console.回调地址里面其实就是?code=XXXXXXXXXXXXXXXXXXX去请求我们的接口
其他相关代码:
WeiXinUser
/**
* @desc 对于微信用户本身存在的信息的一个javabean,不需要在数据库中进行处理
**/
@Setter
@Getter
public class WeiXinUser {
// 用户的标识
private String openId;
// 关注状态(1是关注,0是未关注),未关注时获取不到其余信息
private int subscribe;
// 用户关注时间,为时间戳。如果用户曾多次关注,则取最后关注时间
private String subscribeTime;
// 昵称
private String nickname;
// 用户的性别(1是男性,2是女性,0是未知)
private int sex;
// 用户所在国家
private String country;
// 用户所在省份
private String province;
// 用户所在城市
private String city;
// 用户的语言,简体中文为zh_CN
private String language;
// 用户头像
private String headImgUrl;
@Override
public String toString() {
return "WeiXinUser{" +
"openId='" + openId + '\'' +
", 昵称='" + nickname + '\'' +
", 性别=" + (sex == 1? "男" : "女") +
", 国籍='" + country + '\'' +
", 省份='" + province + '\'' +
", 城市='" + city + '\'' +
'}';
}
}
AccessToken
@Getter
@Setter
public class AccessToken {
// 错误code
private Integer errcode;
// 错误msg
private String errmsg;
// 获取到的凭证
private String access_token;
// 凭证有效时间,单位:秒
private Long expires_in;
@Override
public String toString() {
return "AccessToken{" +
"errcode='" + errcode + '\'' +
", errmsg='" + errmsg + '\'' +
", access_token='" + access_token + '\'' +
", expires_in=" + expires_in +
'}';
}
}
ProjectConst:
/**
* @author Magina
* @create 2019-02-20 17:36:12 By Magina
* @desc 项目相关的静态量
**/
public class ProjectConst {
/**
* 用于获取当前与微信公众号交互的用户信息的接口(一般是用第一个接口地址)
*/
public static final String GET_WEIXIN_USER_URL = "https://api.weixin.qq.com/cgi-bin/user/info?access_token=ACCESS_TOKEN&openid=OPENID";
public final static String GetPageUsersUrl = "https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN";
/**
* 用于进行网页授权验证的接口URL,通过这个才可以得到opendID等字段信息
*/
public final static String GET_WEBAUTH_URL = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code";
/**
* 用于进行当点击按钮的时候,能够在网页授权之后获取到code,再跳转到自己设定的一个URL路径上的接口,这个主要是为了获取之后于
* 获取openId的接口相结合
* 注意:参数:toselfURL 表示的是当授权成功后,跳转到的自己设定的页面,所以这个要根据自己的需要进行修改
*/
public final static String Get_WEIXINPAGE_Code = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=toselfURL&response_type=code&scope=snsapi_userinfo&state=1#wechat_redirect";
/**
* 获取access_token的URL
*/
public static final String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET";
/**
* SMS服务的参数
*/
// public static final String SMS_ACCOUNT_SID = "AC83609469e581a0349a3aa499a55938b9";
// public static final String SMS_AUTH_TOKEN = "f7020503662698bce7e3e5465278c31e";
}
oauth2GetOpenid:
/**
* 进行用户授权,获取到需要的授权字段,比如openId
* @param code 识别得到用户id必须的一个值
* 得到网页授权凭证和用户id
* @return
*/
@Override
public Map oauth2GetOpenid(String code) {
//自己的配置appid(公众号进行查阅)
String appid = projectAppid;
//自己的配置APPSECRET;(公众号进行查阅)
String appsecret = projectAppsecret;
//拼接用户授权接口信息
String requestUrl = ProjectConst.GET_WEBAUTH_URL.replace("APPID", appid).replace("SECRET", appsecret).replace("CODE", code);
//存储获取到的授权字段信息
Map result = new HashMap();
try {
JSONObject OpenidJSONO = WeiXinUtils.doGetStr(requestUrl);
//OpenidJSONO可以得到的内容:access_token expires_in refresh_token openid scope
String Openid = String.valueOf(OpenidJSONO.get("openid"));
System.out.println("openId:"+Openid);
String AccessToken = String.valueOf(OpenidJSONO.get("access_token"));
//用户保存的作用域
String Scope = String.valueOf(OpenidJSONO.get("scope"));
String refresh_token = String.valueOf(OpenidJSONO.get("refresh_token"));
result.put("Openid", Openid);
result.put("AccessToken", AccessToken);
result.put("scope", Scope);
result.put("refresh_token", refresh_token);
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
wechatService.getAuthInfo(code);
/**
* 获取到微信用户的唯一的OpendID
* @param code 这是要获取OpendId的必须的一个参数
* @return
*/
@Override
public Map getAuthInfo(String code) {
//进行授权验证,获取到OpenID字段等信息
Map result = oauth2GetOpenid(code);
// 从这里可以得到用户openid
String openId = result.get("Openid");
return result;
}
wechatService.getUserInfo(accessToken, openId):
@Override
public WeiXinUser getUserInfo(String accessToken, String openId) {
WeiXinUser weixinUserInfo = null;
// 拼接获取用户信息接口的请求地址
String requestUrl = ProjectConst.GET_WEIXIN_USER_URL.replace("ACCESS_TOKEN", accessToken).replace(
"OPENID", openId);
// 获取用户信息(返回的是Json格式内容)
JSONObject jsonObject = WeiXinUtils.doGetStr(requestUrl);
if (null != jsonObject) {
try {
//封装获取到的用户信息
weixinUserInfo = new WeiXinUser();
// 用户的标识
weixinUserInfo.setOpenId(jsonObject.getString("openid"));
// 昵称
weixinUserInfo.setNickname(jsonObject.getString("nickname"));
// 用户的性别(1是男性,2是女性,0是未知)
weixinUserInfo.setSex(jsonObject.getInt("sex"));
// 用户所在国家
weixinUserInfo.setCountry(jsonObject.getString("country"));
// 用户所在省份
weixinUserInfo.setProvince(jsonObject.getString("province"));
// 用户所在城市
weixinUserInfo.setCity(jsonObject.getString("city"));
// 用户头像
weixinUserInfo.setHeadImgUrl(jsonObject.getString("headimgurl"));
} catch (Exception e) {
if (0 == weixinUserInfo.getSubscribe()) {
System.out.println("用户并没有关注本公众号");
} else {
int errorCode = jsonObject.getInt("errcode");
String errorMsg = jsonObject.getString("errmsg");
System.out.println("由于"+errorCode +"错误码;错误信息为:"+errorMsg+";导致获取用户信息失败");
}
}
}
return weixinUserInfo;
}
WeiXinUtils
/**
* @desc 用户获取access_token,众号调用各接口时都需使用access_token
**/
public class WeiXinUtils {
/**
* Get请求,方便到一个url接口来获取结果
*
* @param url
* @return
*/
public static JSONObject doGetStr(String url) {
DefaultHttpClient defaultHttpClient = new DefaultHttpClient();
HttpGet httpGet = new HttpGet(url);
JSONObject jsonObject = null;
try {
HttpResponse response = defaultHttpClient.execute(httpGet);
HttpEntity entity = response.getEntity();
if (entity != null) {
String result = EntityUtils.toString(entity, "UTF-8");
jsonObject = JSONObject.fromObject(result);
}
} catch (ClientProtocolException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return jsonObject;
}
/**
* 获取access_token
* @return
*/
public static AccessToken getAccessToken(String projectAppId,String projectAppsecret){
AccessToken accessToken = new AccessToken();
String appid = projectAppId;
//自己的配置APPSECRET;(公众号进行查阅)
String appsecret = projectAppsecret;
String url = ProjectConst.ACCESS_TOKEN_URL.replace("APPID" ,appid).replace("APPSECRET",appsecret);
JSONObject jsonObject = doGetStr(url);
if(jsonObject !=null){
accessToken.setAccess_token(jsonObject.getString("access_token"));
accessToken.setExpires_in(jsonObject.getLong("expires_in"));
}
return accessToken;
}
public static String authorizeUrl(String callbackUrl,String projectAppid){
StringBuilder sb = new StringBuilder();
try {
sb.append("https://open.weixin.qq.com/connect/oauth2/authorize?appid=").
append(projectAppid).
append("&redirect_uri=").
append(URLEncoder.encode(callbackUrl,"UTF-8")).
append("&response_type=code").
//append("&scope=snsapi_base ").
append("&scope=snsapi_userinfo").
append("&state=STATE").
append("#wechat_redirect");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return sb.toString();
}
}
基本代码都再上面.