用内网穿透软件生成外网代理(一般 80 端口都需要收费)。
├── pom.xml └── src └── main ├── java │ └── com │ └── wdbyte │ └── weixin │ ├── SpringBootApp.java │ ├── config │ │ └── JwtFilter.java # JWT 身份认证拦截器 │ ├── controller │ │ ├── WeixinServerController.java # 微信服务端调用接口 │ │ └── WeixinUserController.java # 浏览器调用接口 │ ├── model │ │ ├── ApiResult.java │ │ ├── ReceiveMessage.java # 微信消息封装类 │ │ └── WeixinQrCode.java # 微信二维码 Ticket 封装类 │ ├── service │ │ ├── WeixinUserService.java # 微信调用处理类 │ │ └── impl │ │ └── WeixinUserServiceImpl.java │ └── util │ ├── AesUtils.java # AES 加密工具类 │ ├── ApiResultUtil.java │ ├── HttpUtil.java # HTTP 工具类 │ ├── JwtUtil.java # JWT 工具类 │ ├── KeyUtils.java │ ├── WeixinApiUtil.java # 微信 API 工具类,如获取 AccessToken │ ├── WeixinMsgUtil.java # 微信消息工具类 │ ├── WeixinQrCodeCacheUtil.java # 微信二维码Ticket缓存 │ └── XmlUtil.java └── resources ├── application.properties #配置文件 ├── static └── templatesWeixinServerController 和 WeixinUserController 暴漏了三个 API。
server.port= weixin.appid= weixin.appsecret= weixin.token= ase.util.secret= key.jwt.secret=4.验证签名
参数 | 描述 |
---|---|
signature | 微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数。 |
timestamp | 时间戳 |
nonce | 随机数 |
echostr | 随机字符串 |
开发者需要对 signature 进行校验,判断是否来自微信服务器,公众号相关的其他事件如消息、关注、扫码等一样会回调配置的 URL ,只不过这时是 POST 请求。
// com.wdbyte.weixin.service.impl.WeixinUserServiceImpl.java // 堆代码 duidaima.com @Value("${weixin.token}") private String token; @Override public void checkSignature(String signature, String timestamp, String nonce) { String[] arr = new String[] {token, timestamp, nonce}; Arrays.sort(arr); StringBuilder content = new StringBuilder(); for (String str : arr) { content.append(str); } String tmpStr = DigestUtils.sha1Hex(content.toString()); if (tmpStr.equals(signature)) { log.info("check success"); return; } log.error("check fail"); throw new RuntimeException("check fail"); }获取 Access Token
// com.wdbyte.weixin.util.WeixinApiUtil.java @Value("${weixin.appid}") public String appId; @Value("${weixin.appsecret}") public String appSecret; private static String ACCESS_TOKEN = null; private static LocalDateTime ACCESS_TOKEN_EXPIRE_TIME = null; /** * 获取 access token * * @return */ public synchronized String getAccessToken() { if (ACCESS_TOKEN != null && ACCESS_TOKEN_EXPIRE_TIME.isAfter(LocalDateTime.now())) { return ACCESS_TOKEN; } String api = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + appId + "&secret=" + appSecret; String result = HttpUtil.get(api); JSONObject jsonObject = JSON.parseObject(result); ACCESS_TOKEN = jsonObject.getString("access_token"); ACCESS_TOKEN_EXPIRE_TIME = LocalDateTime.now().plusSeconds(jsonObject.getLong("expires_in") - 10); return ACCESS_TOKEN; }生成登录二维码
// com.wdbyte.weixin.util.WeixinApiUtil.java private static String QR_CODE_URL_PREFIX = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket="; /** * 二维码 Ticket 过期时间 */ private static int QR_CODE_TICKET_TIMEOUT = 10 * 60; /** * 获取二维码 Ticket * * https://developers.weixin.qq.com/doc/offiaccount/Account_Management/Generating_a_Parametric_QR_Code.html * * @return */ public WeixinQrCode getQrCode() { String api = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=" + getAccessToken(); String jsonBody = String.format("{\n" + " \"expire_seconds\": %d,\n" + " \"action_name\": \"QR_STR_SCENE\",\n" + " \"action_info\": {\n" + " \"scene\": {\n" + " \"scene_str\": \"%s\"\n" + " }\n" + " }\n" + "}", QR_CODE_TICKET_TIMEOUT, KeyUtils.uuid32()); String result = HttpUtil.post(api, jsonBody); log.info("get qr code params:{}", jsonBody); log.info("get qr code result:{}", result); WeixinQrCode weixinQrCode = JSON.parseObject(result, WeixinQrCode.class); weixinQrCode.setQrCodeUrl(QR_CODE_URL_PREFIX + URI.create(weixinQrCode.getTicket()).toASCIIString()); return weixinQrCode; } class WeixinQrCode { private String ticket; private Long expireSeconds; private String url; private String qrCodeUrl; }响应内容格式如下:
{ "ticket": "gQH47joAAAAAAAAAASxodHRwOi8vd2VpeGluLnFxLmNvbS9xL2taZ2Z3TVRtNzJXV1Brb3ZhYmJJAAIEZ23sUwMEmm3sUw==", "expire_seconds": 60, "url": "http://weixin.qq.com/q/kZgfwMTm72WWPkovabbI" }其中 Ticket 就是二维码凭证,用户扫码后微信会把此 Ticket 回调给网站服务端。可以在下面的链接后面拼上 Ticket 换取二维码图片。
<xml> <ToUserName><![CDATA[toUser]]></ToUserName> <FromUserName><![CDATA[FromUser]]></FromUserName> <CreateTime>123456789</CreateTime> <MsgType><![CDATA[event]]></MsgType> <Event><![CDATA[subscribe]]></Event> <EventKey><![CDATA[qrscene_123123]]></EventKey> <Ticket><![CDATA[TICKET]]></Ticket> </xml>其中 FromUserName 是用户身份标识,EventKey 值 qrscene_标识扫码,Ticket 则是二维码的 Ticket。至此,服务端就可以识别出二维码是被哪个用户扫码了。绑定 Ticket 和用户身份标识。
{ "code": -1, "data": "check faild", "message": "error" }微信扫码关注公众号。
{ "code": 200, "data": "mihzE8Z1Y9t2EoppNSzzytV4TOgn+Nc50ORZjsW/oVkxchL4EzGA6rr1tQ0Q7J24Ipm4otjCYf95Nu8JbV31Q/ImKvlta3f5bgvOdWSlO2tNvOwqgzBSItABohbCLVLxjGCci4VtNaEFgQjoDjc1uhwP/GCSohVFc7csO9SxpOm8HKtlRhATjwPrtiQ9iLErfsUs27I0k5OHp55AzuQOYCvza//i3wk8nlv/MDkk7y1nvsZkllyKQGHPB4Ulcraz", "message": "success" }至此,登录完成。