前言
基于后端服务,操作微信公众号,定制化菜单、消息推送、消息回复等功能,都需要通过服务端进行统一的 Token 认证,这样双方才能形成绑定的关系,从而实现相互认知后进行通信.
公众号服务配置 Token认证
接入服务器配置的目的:接入微信第三方开发平台,在微信公众平台提供的基础服务上开发微网站、微商城、活动应用zd、娱乐插件等扩展功能
如何配置
服务器基本配置
URL必须是以 HTTP 或 HTTPS 开头,必须是在线上可访问的域名地址,部署在本地不可以,/wx/auth
是后台的接口地址,Token 是在后端服务配置的一个固定值,必须要一致相互才能 ping 通,其他如上图所示,点击提交后进行启用
Token 认证接口
/wx/auth
接口源码如下:
/** * @author vnjohn * @since 2023/2/19 */ @Slf4j @RestController("/wx") @Api(value = "微信服务器 token 认证", tags = "微信服务器 token 认证 接口API") public class WxPortalController { @GetMapping("/auth") public void authPost(HttpServletRequest request, HttpServletResponse response) throws IOException { if (StringUtils.isNotBlank(request.getParameter("signature"))) { String signature = request.getParameter("signature"); String timestamp = request.getParameter("timestamp"); String nonce = request.getParameter("nonce"); String echostr = request.getParameter("echostr"); log.info("signature[{}], timestamp[{}], nonce[{}], echostr[{}]", signature, timestamp, nonce, echostr); if (SignUtil.checkSignature(signature, timestamp, nonce)) { log.info("数据源为微信后台,将echostr[{}]返回!", echostr); response.getOutputStream().println(echostr); } } } }
Token 认证类源码如下:
/** * @author vnjohn * @since 2023/2/19 */ public class SignUtil { /** * 这里是自定义 Token,需和你在公众号后台提交的 Token 保持一致 */ private static final String TOKEN = "familySchool"; /** * 校验签名 * * @param signature 签名 * @param timestamp 时间戳 * @param nonce 随机数 * @return 布尔值 */ public static boolean checkSignature(String signature, String timestamp, String nonce) { String checktext = null; if (null != signature) { // 对ToKen,timestamp,nonce 按字典排序 String[] paramArr = new String[]{TOKEN, timestamp, nonce}; Arrays.sort(paramArr); // 将排序后的结果拼成一个字符串 String content = paramArr[0].concat(paramArr[1]).concat(paramArr[2]); try { MessageDigest md = MessageDigest.getInstance("SHA-1"); // 对接后的字符串进行sha1加密 byte[] digest = md.digest(content.getBytes()); checktext = byteToStr(digest); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } } // 将加密后的字符串与signature进行对比 return checktext != null && checktext.equals(signature.toUpperCase()); } /** * 将字节数组转化为16进制字符串 * * @param byteArrays 字符数组 * @return 字符串 */ private static String byteToStr(byte[] byteArrays) { String str = ""; for (int i = 0; i < byteArrays.length; i++) { str += byteToHexStr(byteArrays[i]); } return str; } /** * 将字节转化为十六进制字符串 * * @param myByte 字节 * @return 字符串 */ private static String byteToHexStr(byte myByte) { char[] Digit = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; char[] tampArr = new char[2]; tampArr[0] = Digit[(myByte >>> 4) & 0X0F]; tampArr[1] = Digit[myByte & 0X0F]; String str = new String(tampArr); return str; } }
公众号获取网页授权及用户信息
导向
获取网页授权前提是我们需要在公众号上配置好网页授权的域名,配置好以后,我们需要一个触发点去触发这个网页(H5)以便用户点击我们设置好的触发按钮或事件时能拿到其对应的用户信息,本篇文章我们采用被关注自动回复消息,点击消息触发
此处介绍公众号UNION_ID和OPEN_ID区别:
- UNION_ID:在同一个主体下,公众号、小程序、APP 涉及到微信应用的时候,所拿到的 UNIONID 都是一样的,打个比方:相当于身份证,在哪个地方都可以用到而且是唯一的
- OPEN_ID:公众号、小程序都会有一个会话 ID 就是 OPEN_ID,一拿到这个会话 ID 我们就能在这个应用作为唯一标识通行,打个比方:
社保卡
在我们购买药品时可以用到、驾驶证在我们出行时可以使用
同一个微信开放平台下的相同主体的 App、公众号、小程序,如果用户已经关注公众号,或者曾经登录过 App 或公众号,则用户打开小程序时,开发者可以直接通过 wx.login
获取到该用户 UNION_ID,无须用户再次授权。如果用户未关注公众号,可以通过 wx.getUserInfo
获取到该用户 UNION_ID
网页授权
首先要拿到网页授权的接口权限
修改网页授权接口时,要修改 JS 接口安全域名以及网页授权域名
上图需要下载对应的文件,前提是域名/文件名需要访问到你对应的文件内容才能配好网页授权域名,代码如下:
/** * @author vnjohn * @since 2023/2/19 */ @RestController public class DomainAuthController { /** * 网页授权域名,需要把用到的文件下载下来,文件名是需要请求的接口 * 返回的是文件对应的内容 */ @GetMapping("MP_verify_zxgx6O368tVx5sDK.txt") private String returnConfigFile() { //把 MP_verify_xxxxxx.txt 中的内容返回 return "zxgx6O368tVx5sDK"; } }
关注后消息触发授权
在导向中提及到的 被关注后自动回复消息点击触发
URL 地址:<a href="https://open.weixin.qq.com/connect/oauth2/authorize?appid=wx6a14ea8110b086af&redirect_uri=http%3A%2F%2Fxxxx%2Fwx%2FwxLogin&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect">点击绑定你的个人信息</a>
域名是我们配置好网页授权域名,接口地址是我们后台处理的接口
官方介绍:微信网页授权认证
redirect_uri 回调的链接地址是需要通过 utf-8
编码过后的,如下:
java.net.URLEncoder.encode(source, "utf-8"); • 1
WxConstant 类是自定义微信常量类:存放请求 TOKEN Address、APPID、SECRET
/** * @author vnjohn * @since 2023/2/19 */ public class WxConstant { /** * 小程序授权grant_type */ public static final String AUTHORIZATION_CODE = "023VMFlQ0fMEp72wK7kQ0FIqlQ0VMFlu"; /** * 打卡模板id */ public final static String PUNCH_TEMPLATE_ID = "0pMwyHxbobrotPdcbkIUt3fY03pnVm9Ril4KEqLmeNE"; /** * 收款模板id */ public final static String COLLECTION_TEMPLATE_ID = "7vdNCysQL64KBQtHG4xy2wsTQWyvQo4P66HPDvXbIHE"; /** * 学校通知模板id */ public final static String MSG_TEMPLATE_ID = "DVBa7LpPssglj5DYgNtenxDhZ-8DBuxnctVgiwnhGxs"; /** * 公众号APP_ID */ public final static String GZH_APP_ID = "公众号APP_ID"; /** * 公众号SECRET */ public final static String GZH_SECRET = "公众号SECRET"; /** * 公众号获取用户信息的 URL */ public final static String GZH_USER_INFO = "https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID"; /** * 小程序APP_ID */ public final static String APPLET_APP_ID = "小程序APP_ID"; /** * 小程序APP_SECRET */ public final static String APPLET_SECRET = "小程序APP_SECRET"; /** * 小程序获取token凭证获取(GET)url、公众号推送消息获取token凭证 */ public final static String TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + "APPID&secret=SECRET"; /** * 生成小程序二维码 */ public final static String QRCODE_URL = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=ACCESSTOKEN"; /** * 公众号推送模板消息URL */ public final static String MSG_API = "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=ACCESSTOKEN"; /** * 获取网页授权凭证URL */ public final static String AUTH_URL = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code"; }
@Data public class Oauth2Token { /** * 网页授权接口调用凭证 */ private String accessToken; /** * 凭证有效时长 */ private int expiresIn; /** * 用于刷新凭证 */ private String refreshToken; /** * 用户标识 */ private String openId; /** * 用户授权作用域 */ private String scope; }
/wx/login
接口源码如下:
/** * @author vnjohn * @since 2023/2/19 */ @Slf4j @RestController @RequestMapping("/wx/wxLogin") public class WxLoginController { @GetMapping public void weixinLogin(HttpServletRequest request, HttpServletResponse response) throws Exception { // 用户同意授权后,能获取到code String code = request.getParameter("code");//拿到code的值 logger.info("****************code:" + code); // 用户同意授权 if (!"authdeny".equals(code)) { // 获取网页授权access_token Oauth2Token oauth2Token = getOauth2AccessToken(WxConstant.GZH_APP_ID, WxConstant.GZH_SECRET, code); logger.info("***********************************oauth2Token信息:" + oauth2Token.toString()); // 网页授权接口访问凭证 String accessToken = oauth2Token.getAccessToken(); // 用户标识 String openId = oauth2Token.getOpenId(); // 获取用户信息 WxUserInfo wxUserInfo = getWxUserInfo(accessToken, openId); logger.info("***********************************用户信息unionId:" + wxUserInfo.getUnionid() + "***:" + wxUserInfo.getNickname()); //到数据库查询 该 用户信息是否存在 int result = userService.queryIsExists(wxUserInfo.getUnionid()); if (result < 1) {//添加一条新的记录 User communityUser = new User(); communityUser.setNickName(wxUserInfo.getNickname()); communityUser.setOpenId(wxUserInfo.getOpenId()); communityUser.setUnionId(wxUserInfo.getUnionid()); result = userService.addUser(communityUser); if (result > 0) { logger.info("绑定用户信息成功"); } else { logger.info("绑定用户信息失败"); } } else { // 这种情况出现:已关注->取关->再次关注(数据库肯定存在记录,此时重新删除,再次添加) userService.delInfo(wxUserInfo.getUnionid()); User communityUser = new User(); communityUser.setNickName(wxUserInfo.getNickname()); communityUser.setOpenId(wxUserInfo.getOpenId()); communityUser.setUnionId(wxUserInfo.getUnionid()); result = userService.addUser(communityUser); if (result > 0) { logger.info("绑定用户信息成功"); } else { logger.info("绑定用户信息失败"); } } // 设置要传递的参数 //具体业务start logger.info("openId:" + openId); //具体业务end logger.info(wxUserInfo.toString()); //重定向到我们需要跳转的地址 由前端编写的h5页面 必须是部署到服务器上的 response.sendRedirect("跳转的网页地址"); return; } return; } /** * 获取网页授权凭证 * * @param appId 公众账号的唯一标识 * @param appSecret 公众账号的密钥 * @param code * @return Oauth2Token */ public static Oauth2Token getOauth2AccessToken(String appId, String appSecret, String code) { Oauth2Token wat = null; // 拼接请求地址 String requestUrl = WxConstant.AUTH_URL.replace("APPID", appId).replace("SECRET", appSecret).replace("CODE", code); // 获取网页授权凭证 JSONObject jsonObject = JSON.parseObject(HttpUtil.get(requestUrl)); if (null != jsonObject) { try { wat = new Oauth2Token(); wat.setAccessToken(jsonObject.getString("access_token")); wat.setExpiresIn(jsonObject.getInteger("expires_in")); wat.setRefreshToken(jsonObject.getString("refresh_token")); wat.setOpenId(jsonObject.getString("openid")); wat.setScope(jsonObject.getString("scope")); } catch (Exception e) { wat = null; int errorCode = jsonObject.getInteger("errcode"); String errorMsg = jsonObject.getString("errmsg"); log.error("获取网页授权凭证失败 errcode:{} errmsg:{}", errorCode, errorMsg); } } return wat; } /** * 通过网页授权获取用户信息 * * @param accessToken 网页授权接口调用凭证 * @param openId 用户标识 * @return WxUserInfo */ public static WxUserInfo getWxUserInfo(String accessToken, String openId) { WxUserInfo wxUserInfo = null; // 拼接请求地址 String requestUrl = WxConstant.GZH_USER_INFO.replace("ACCESS_TOKEN", accessToken).replace("OPENID", openId); // 通过网页授权获取用户信息 JSONObject jsonObject = JSON.parseObject(HttpUtil.get(requestUrl)); if (null != jsonObject) { try { wxUserInfo = new WxUserInfo(); // 用户的标识 wxUserInfo.setOpenId(jsonObject.getString("openid")); // 昵称 wxUserInfo.setNickname(jsonObject.getString("nickname")); // 性别(1是男性,2是女性,0是未知) wxUserInfo.setSex(jsonObject.getInteger("sex")); // 用户所在国家 wxUserInfo.setCountry(jsonObject.getString("country")); // 用户所在省份 wxUserInfo.setProvince(jsonObject.getString("province")); // 用户所在城市 wxUserInfo.setCity(jsonObject.getString("city")); // 用户头像 wxUserInfo.setHeadImgUrl(jsonObject.getString("headimgurl")); // 用户特权信息 List<String> list = JSON.parseArray(jsonObject.getString("privilege"), String.class); wxUserInfo.setPrivilegeList(list); // 与开放平台共用的唯一标识,只有在用户将公众号绑定到微信开放平台帐号后,才会出现该字段。 wxUserInfo.setUnionid(jsonObject.getString("unionid")); // 此处需要进入数据库 需要将 UnionId、openId 存入数据库 } catch (Exception e) { wxUserInfo = null; int errorCode = jsonObject.getInteger("errcode"); String errorMsg = jsonObject.getString("errmsg"); log.error("获取用户信息失败 errcode:{} errmsg:{}", errorCode, errorMsg); } } return wxUserInfo; } /** * URL编码(utf-8) * * @param source * @return */ public static String urlEncodeUTF8(String source) { String result = source; try { result = java.net.URLEncoder.encode(source, "utf-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return result; } }
微信网页授权成功并拿到用户信息以后跳转的 H5 网页是一个简单的显示跳转成功页面,让公司的前端大佬做一下就 OK
关注后会显示出来,点击绑定你的个人信息
点击后跳转成功,完成测试 ,注意:全部操作需要部署到线上操作,或者通过内网穿透的方式来部署,否则无法进行测试