目录
前言:
准备条件:
用到的微信接口
代码:
效果:
总结:
前言:
最近其他项目组需要在微信公众号里面调用微信的扫一扫功能,于是就帮忙一起搞了搞,一开始以为挺简单,从网上搜一下教程弄一下就可以了,没成想还挺麻烦的,网上的教程呢好多都是会了不用看,不会的看了也没啥用的。于是自己在弄完以后想着写一篇博客,造福一下大家。希望对大家能有所帮助。
准备条件:
1、ICP备案的域名,并且绑定对应的外网服务器。
用处:你写的扫一扫功能的代码只有放到这个域名所对应的服务器中运行才能成功调起扫一扫功能。
如果你准备好了域名,那么按照以下操作添加:
(1)、打开微信公众号——设置——公众号设置——功能设置——JS接口安全域名中添加上你的域名。
(2)、接下来的步骤是非常重要的,如下图,仔细阅读下图红框中的文字,明白它的意思。需要你下载红框中的文件,然后上传到域名绑定服务器的根目录下面,只有这样你的域名在此处才能添加成功。举个例子:通常访问域名会默认访问80端口,你可以在服务器上安装tomcat并设置端口号为80,然后将此处下载的文件放到tomcat中的webapp文件夹下面,也就和你的项目相同路径的文件夹下。如果你还不懂,博客下面会有我的微信,欢迎咨询。
ps:由于微信开发中很多地方都用了域名,所以这里提供一个不用阿里云或者腾讯云备案的方法,利用natapp内网穿透,直接将本机设置为服务器并获得域名。填写域名时不要带着http://,例如http://www.baidu.com,只写www.baidu.com就行。
https://natapp.cn/member/dashborad具体设置方法可以看里面的教程,不过这里需要购买VIP_1型隧道,免费的会被微信屏蔽。
直接映射本地的80端口,然后用nginx做内部转发,就可以访问h5页面或者本地的后台服务。
2、微信公众好的AppID和AppSecret,查看地方在:开发者——基本配置中,如下图。用于生成access_token使用
3、IP白名单设置,查看地方在:开发者——基本配置中,如下图。在此处添加你的服务器IP,访问生成access_token的接口需要在添加了白名单的服务中访问,否则无法获取access_token
以上条件准备好之后才能实现调用微信扫一扫功能,我用的是阿里云服务器,并且备案了对应的域名,然后使用自己的微信公众号。
用到的微信接口:
1、获取access_token接口:GET https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
其中grant_type参数的固定值是:client_credential
微信开发者文档地址:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html
2、获取ticket接口:http请求方式: GET https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=ACCESS_TOKEN&type=wx_card
微信开发者文档地址:https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html#49
PS:通过grant_type、appid、secret参数获取access_token;再根据access_token获取ticket。
access_token和ticket的有效期是7200秒(两小时),后续代码中会提供解决它们时效性的方法。
代码:
1、JSSDK使用步骤,参考微信开发者文档:https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html#3
对应我自己的代码:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<meta name="viewport" content="width=device-width, initial-scale=1 user-scalable=0">
<script src="http://res.wx.qq.com/open/js/jweixin-1.2.0.js"> </script>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.js"></script>
</head>
<body>
<div class="lbox_close wxapi_form">
<h3>微信扫一扫</h3>
<input id="codeValue">
<button id="scanQRCode">扫码</button>
</div>
<script type="text/javascript">
$(function() {
//需要把当前页面的url地址传到后台,生成签名信息时需要使用到。下面的地址是controller层调用当前页面的路径
var tokenUrl= "http://服务器域名/jsapi/ape/v1/qrcode";
//获取签名的后台接口
var _getWechatSignUrl = '/jsapi/getSign';
$(document).ready(function(){
//获取签名
$.ajax({
url:_getWechatSignUrl,
data:{tokenUrl:tokenUrl},
dataType:"json",
success:function(res){
console.log(res);
//获得签名之后传入配置中进行配置
wxConfig(res.appId,res.timestamp,res.nonceStr,res.signature);
}
})
})
function wxConfig(_appId,_timestamp, _nonceStr, _signature) {
console.log('获取数据:' + _timestamp +'\n' + _nonceStr +'\n' + _signature);
wx.config({
debug:true,// 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
appId: _appId,// 必填,公众号的唯一标识
timestamp: _timestamp,// 必填,生成签名的时间戳
nonceStr: _nonceStr,// 必填,生成签名的随机串
signature: _signature,// 必填,签名,见附录1
jsApiList: ['checkJsApi','scanQRCode']
// 必填,需要使用的JS接口列表,所有JS接口列表见附录2
});
}
$("#scanQRCode").click(function(event){
wx.scanQRCode({
desc: 'scanQRCode desc',
needResult : 1, // 默认为0,扫描结果由微信处理,1则直接返回扫描结果,
scanType : [ "qrCode", "barCode" ], // 可以指定扫二维码还是一维码,默认二者都有
success : function(res) {
console.log("调用扫描成功",res);
var result = res.resultStr; // 当needResult 为 1 时,扫码返回的结果
$("#codeValue").val(result);//显示结果
alert("扫码结果为:" + result);
},
error:function(res){
console.log(res)
}
});
})
});
</script>
</body>
</html>
代码解析:
ps:调用微信扫一扫接口文档:https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html#49
后台代码:
1、QRCodeController.java。仔细看代码中的注释
package com.lyd.controller;
import com.alibaba.fastjson.JSONObject;
import com.lyd.pojo.JsonData;
import com.lyd.service.JsApiService;
import com.lyd.utils.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
/**
* @author Lyd
* @date 2020/5/11 22:32
*/
@Controller
@RequestMapping("/jsapi")
public class QRCodeController {
private static final Logger log = LoggerFactory.getLogger(WechatUtil.class);
@Autowired
private RedisClient redisClient;
@Autowired
@Qualifier("wxJsService")
private JsApiService jsApiService;
/**
* 用于返回 appId: _appId,// 必填,公众号的唯一标识
* timestamp: _timestamp,// 必填,生成签名的时间戳
* nonceStr: _nonceStr,// 必填,生成签名的随机串
* signature: _signature,// 必填,签名,见附录1
* 该方法会调用获取token、ticket两个方法,token和ticket需要从redis中获取
* 该方法会调用redis并将生成的token、ticket保存到redis中同时设置失效时间
* @param tokenUrl:微信扫一扫页面的路径
* @param request
* @return
*/
@GetMapping(value = "/getSign")
@ResponseBody
public Map<String, String> scanJsApi(@Param("tokenUrl") String tokenUrl, HttpServletRequest request) {
boolean redisAccess_token = redisClient.hasKey("access_token");
boolean redisTicket = redisClient.hasKey("ticket");
//判断从redis中获取的两个key值是否失效,如果没有则调用生成access_token和ticket的方法重新生成并保存到redis中
if (redisAccess_token == false || redisTicket == false) {
//调用生成access_token的方法
JSONObject jsonObject=WechatUtil.getAccessToken();
//如果access_token生成成功,则获取生成的access_token,并且保存到redis中,设置access_token的有效时间为7200秒
if(jsonObject.containsKey("access_token")){
String access_token = jsonObject.getString("access_token");
log.info("=========获取access_token成功:"+access_token);
redisClient.setKeyValueAndTimeOut("access_token",access_token,7200);
log.info("=========access_token保存成功================");
//调用生成ticket的方法,获取ticket,并保存到redis中,设置ticket的有效时间为7200秒
JSONObject ticketJsonObject= WechatUtil.getJsApiTicket(redisClient.getRedis("access_token"));
if(ticketJsonObject.containsKey("ticket")){
String ticket = ticketJsonObject.getString("ticket");
log.info("=========获取ticket成功:"+ticket);
redisClient.setKeyValueAndTimeOut("ticket",ticket,7200);
log.info("=========ticket保存成功================");
}else {
log.info("=========ticket保存失败,失败信息:"+ticketJsonObject);
}
}else{
log.info("=========获取token失败,失败信息:"+jsonObject);
}
}
//调用获取签名的方法
Map<String, String> res = jsApiService.sign(tokenUrl);
return res;
}
/**
* 微信扫一扫页面,访问路径
* @return 微信扫一扫页面
*/
@RequestMapping(value="/ape/v1/qrcode")
public Object test(){
return "user/qrcode";
}
}
2、JsApiService.java:获取签名的实际实现类
package com.lyd.service;
import com.lyd.utils.*;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Formatter;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* @author Lyd
* @date 2020/5/11 22:34
*/
@Component("wxJsService")
@Transactional
public class JsApiService{
private static final Logger log = LoggerFactory.getLogger(WechatUtil.class);
@Autowired
private RedisClient redisClient;
/**
* 获取签名
* @param url
* @return
*/
public Map<String, String> sign(String url) {
Map<String, String> resultMap = new HashMap<>(16);
//这里的jsapi_ticket是获取的jsapi_ticket。
//String jsapiTicket = this.getJsApiTicket();
String jsapiTicket =redisClient.getRedis("ticket");
log.info("###########读取ticket:"+jsapiTicket);
//这里签名中的nonceStr要与前端页面config中的nonceStr保持一致,所以这里获取并生成签名之后,还要将其原值传到前端
String nonceStr = createNonceStr();
//nonceStr
String timestamp = createTimestamp();
String string1;
String signature = "";
//注意这里参数名必须全部小写,且必须有序
string1 = "jsapi_ticket=" + jsapiTicket +
"&noncestr=" + nonceStr +
"×tamp=" + timestamp +
"&url=" + url;
System.out.println("string1:"+string1);
try
{
MessageDigest crypt = MessageDigest.getInstance("SHA-1");
crypt.reset();
crypt.update(string1.getBytes("UTF-8"));
signature = byteToHex(crypt.digest());
}
catch (NoSuchAlgorithmException e)
{
e.printStackTrace();
}
catch (UnsupportedEncodingException e)
{
e.printStackTrace();
}
resultMap.put("url", url);
resultMap.put("jsapi_ticket", jsapiTicket);
resultMap.put("nonceStr", nonceStr);
resultMap.put("timestamp", timestamp);
resultMap.put("signature", signature);
resultMap.put("appId", Constants.GZH_APPID);
log.info("###########打印resultMap:"+resultMap.get("jsapi_ticket")+","+resultMap.get("url"));
return resultMap;
}
private static String byteToHex(final byte[] hash) {
Formatter formatter = new Formatter();
for (byte b : hash)
{
formatter.format("%02x", b);
}
String result = formatter.toString();
formatter.close();
return result;
}
private static String createNonceStr() {
return UUID.randomUUID().toString();
}
private static String createTimestamp() {
return Long.toString(System.currentTimeMillis() / 1000);
}
}
3、Constants.java存放常量的类
package com.lyd.utils;
/**
* @author Lyd
* @date 2020/5/11 22:35
*/
public class Constants {
/**
* 换取ticket的url
*/
public static final String JSAPI_TICKET = "https://api.weixin.qq.com/cgi-bin/ticket/getticket";
/**
* 换取token的url
*/
public static final String JSAPI_TOKEN = " https://api.weixin.qq.com/cgi-bin/token";
public static final String GZH_APPID = "公众号的AppID";
public static final String GZH_SECURET = "公众号的AppSecret";
}
4、HttpClientUtil .java用于访问获取access_token、ticket接口的工具类
package com.lyd.utils;
/**
* @author Lyd
* @date 2020/5/11 22:37
*/
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
public class HttpClientUtil {
public static String doGet(String url, Map<String, String> param) {
// 创建Httpclient对象
CloseableHttpClient httpclient = HttpClients.createDefault();
String resultString = "";
CloseableHttpResponse response = null;
try {
// 创建uri
URIBuilder builder = new URIBuilder(url);
if (param != null) {
for (String key : param.keySet()) {
builder.addParameter(key, param.get(key));
}
}
URI uri = builder.build();
// 创建http GET请求
HttpGet httpGet = new HttpGet(uri);
// 执行请求
response = httpclient.execute(httpGet);
// 判断返回状态是否为200
if (response.getStatusLine().getStatusCode() == 200) {
resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (response != null) {
response.close();
}
httpclient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return resultString;
}
public static String doGet(String url) {
return doGet(url, null);
}
public static String doPost(String url, Map<String, String> param) {
// 创建Httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
String resultString = "";
try {
// 创建Http Post请求
HttpPost httpPost = new HttpPost(url);
// 创建参数列表
if (param != null) {
List<NameValuePair> paramList = new ArrayList<>();
for (String key : param.keySet()) {
paramList.add(new BasicNameValuePair(key, param.get(key)));
}
// 模拟表单
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(paramList);
httpPost.setEntity(entity);
}
// 执行http请求
response = httpClient.execute(httpPost);
resultString = EntityUtils.toString(response.getEntity(), "utf-8");
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return resultString;
}
public static String doPost(String url) {
return doPost(url, null);
}
public static String doPostJson(String url, String json) {
// 创建Httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
String resultString = "";
try {
// 创建Http Post请求
HttpPost httpPost = new HttpPost(url);
// 创建请求内容
StringEntity entity = new StringEntity(json, ContentType.APPLICATION_JSON);
httpPost.setEntity(entity);
// 执行http请求
response = httpClient.execute(httpPost);
resultString = EntityUtils.toString(response.getEntity(), "utf-8");
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return resultString;
}
}
5、RedisClient.java用于操作redis的工具类,方便添加、修改、删除redis中的相关key-value
package com.lyd.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**Redis工具类,用于存贮key-value和读取keyvalue
* @Author:Lyd
* @Date 2019/12/30 21:22
*/
@Component
public class RedisClient {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 设置key-value到redis中
* @param key
* @param value
* @return
*/
public boolean setRedis(String key,String value){
try{
redisTemplate.opsForValue().set(key,value);
return true;
}catch (Exception e){
return false;
}
}
/**
* 实现命令:SET key value EX seconds,设置key-value和超时时间(秒)
*
* @param key
* @param value
* @param timeout (以秒为单位)
*/
public void setKeyValueAndTimeOut(String key, String value, long timeout) {
redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
}
/**
* 从Redis中获取key对应的value
* @param key
* @return
*/
public String getRedis(String key){
return redisTemplate.opsForValue().get(key);
}
/**
* 从Redis中获取key对应的value
* @param key
* @return
*/
public boolean hasKey(String key){
return redisTemplate.hasKey(key);
}
}
6、WechatUtil.java获取access_token、ticket的实现方法
package com.lyd.utils;
/**
* @author Lyd
* @date 2020/5/11 22:37
*/
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.lang3.StringUtils;
import java.util.HashMap;
import java.util.Map;
/**微信工具类
* @author:Lyd
* @Date 2020/5/13 10:02
*/
public class WechatUtil {
/**
* 获得jsapi_ticket
*/
public static JSONObject getJsApiTicket(String token) {
String url = Constants.JSAPI_TICKET
+ "?access_token=" + token
+ "&type=jsapi";
String msg = HttpClientUtil.doGet(url);
if (StringUtils.isBlank(msg)) {
return null;
}
JSONObject jsonObject = JSONObject.parseObject(msg);
return jsonObject;
}
/**
* 获取token
* @return msg
*/
public static JSONObject getAccessToken() {
String url = Constants.JSAPI_TOKEN;
Map<String, String> param = new HashMap<>(16);
param.put("grant_type", "client_credential");
param.put("appid", Constants.GZH_APPID);
param.put("secret", Constants.GZH_SECURET);
String msg = HttpClientUtil.doGet(url, param);
if (StringUtils.isBlank(msg)) {
return null;
}
JSONObject jsonObject = JSONObject.parseObject(msg);
return jsonObject;
}
}
配置文件:
pom.xml中需要引入的相关jar
<!--调用微信扫一扫所需要的jar包-->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.44</version>
</dependency>
<dependency>
<groupId>com.vaadin.external.google</groupId>
<artifactId>android-json</artifactId>
<version>0.0.20131108.vaadin1</version>
<scope>compile</scope>
</dependency>
application.properties
#=========redis基础配置=========
spring.redis.database=0
#redis的安装地址
spring.redis.host=localhost
spring.redis.port=6379
# 连接超时时间 单位 ms(毫秒)
spring.redis.timeout=3000
#=========redis线程池设置=========
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.jedis.pool.max-active=200
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.jedis.pool.max-wait=-1
# 连接池中的最大空闲连接
spring.redis.jedis.pool.max-idle=200
# 连接池中的最小空闲连接
spring.redis.jedis.pool.min-idle=200
ps:我遇到的一个问题,我的redis也装在域名对应的外网服务器上。在本地可以通过IP访问通,但是当我把项目放到服务器上的时候,用IP不行,用localhost就没有问题。
redis的安装和集成到springboot可以参考我的博客:
效果:
总结:
用了大概两天的时间将全流程弄通了,期间还是很有意义的,即使以后不再用到也写一个帖子记录以下吧。有不懂的小伙伴可以加我微信幺,欢迎咨询。
相关源码在我的码云中:https://gitee.com/lydzyw/springbootLyd。该项目是我用来自己学习用的,里面还有其他的东西,大家有兴趣可以下载下来自己学习使用。