最近一直在做微信小程序的项目,使用到了微信扫码的功能。原本可以直接使用小程序提供的api,但是由于涉及到一些其他原因,需要在h5页面中来调用该功能。所以就需要使用微信公众平台提供的JSSDK来调用扫一扫功能。仅以此来记录自己的学习过程,也希望能帮助到有此需求的一些朋友。
过程中参考文章有:https://blog.csdn.net/u011327333/article/details/50439462
https://www.jianshu.com/p/a4aea4d12c23
长话短说,我们先来做一些准备工作!
一、准备工作
官方接口文档:微信JS-SDK说明文档
1,首先看一下说明文档,第一步,绑定域名:
登录公众号 -》公众号设置 -》功能设置 -》JS接口安全域名
由于微信开发中很多地方都用了域名,所以这里提供一个不用阿里云或者腾讯云备案的方法,利用natapp内网穿透,直接将本机设置为服务器并获得域名。填写域名时不要带着http://,例如http://www.baidu.com,只写www.baidu.com就行。
https://natapp.cn/member/dashborad
具体设置方法可以看里面的教程,不过这里需要购买VIP_1型隧道,免费的会被微信屏蔽。
直接映射本地的80端口,然后用nginx做内部转发,就可以访问h5页面或者本地的后台服务。
2,引入JS文件
根据文档说明,在需要调用JS接口的页面引入如下JS文件,(支持https):http://res.wx.qq.com/open/js/jweixin-1.4.0.js、
3,通过config接口注入权限验证配置
这个配置需要在调用扫一扫功能之前就配置好,例如在页面初始化的时候。
其余的在教程中说的很明白,下面我们直接上代码,这里的前后端代码只是写了个demo,真要用到项目中肯定还需要好好封装一下。
我的前端页面与后台服务是前后端分离的,前端页面就是一个单独的html页面,后台服务用的是springboot,都是在本机上启动的,通过nginx做的反向代理,natapp将域名映射到本机80端口,后台服务为8080端口,指定带有某个字符串的请求路由到8080端口去请求后台,而前端页面则直接放到nginx下进行访问。
二、前端页面
需要把当前页面的url传到后台去,生成签名信息时需要使用到,其他信息都可以在后台设置,最终配置的时候config中的各字段与后台生成签名时的字段值能对应起来就行了。
Title
微信扫一扫
三、后台服务
生成签名和其他信息的controller
import com.imooc.service.JsApiService;
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 javax.servlet.http.HttpServletRequest;
import java.util.Map;
@Controller
@RequestMapping("/jsapi")
public class jsApiController {
@Autowired
@Qualifier("wxJsService")
private JsApiService jsApiService;
@GetMapping(value = "/getSign")
@ResponseBody
public Map scanJsApi(@Param("tokenUrl") String tokenUrl, HttpServletRequest request) {
Map res = jsApiService.sign(tokenUrl);
return res;
}
}
处理请求的service
import com.imooc.common.CacheObjectType;
import com.imooc.common.Constants;
import com.imooc.common.RedisOperator;
import com.imooc.common.WechatUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
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;
@Component("wxJsService")
@Transactional
public class JsApiService{
@Autowired
private RedisOperator redis;
/**
* 获取签名
* @param url
* @return
*/
public Map sign(String url) {
Map resultMap = new HashMap<>(16);
//这里的jsapi_ticket是获取的jsapi_ticket。
String jsapiTicket = this.getJsApiTicket();
//这里签名中的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);
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);
}
public String getJsApiTicket(){
CacheObjectType cacheObject = CacheObjectType.WX_TOKEN;
String ticket = redis.get(cacheObject.getPrefix()+"jsapi_ticket");
if(StringUtils.isBlank(ticket)){
ticket = WechatUtil.getJsApiTicket(getToken());
redis.set(cacheObject.getPrefix()+"jsapi_ticket",ticket,
cacheObject.getExpiredTime());
}
return ticket;
}
private String getToken(){
String token = redis.get(Constants.GZH_TOKEN);
return token;
}
}
CacheObjectType
public enum CacheObjectType {
VERIFY_CODE("sms:S:code:", 60 * 5),
WX_TOKEN("wx:S:token", 7000);
private String prefix;
private int expiredTime;
CacheObjectType(String prefix, int expiredTime) {
this.prefix = prefix;
this.expiredTime = expiredTime;
}
public String getPrefix() {
return prefix;
}
public int getExpiredTime() {
return expiredTime;
}
}
Constants
public class Constants {
//换取ticket的url
public static final String JSAPI_TICKET = "https://api.weixin.qq.com/cgi-bin/ticket/getticket";
//微信公众号token在redis中存储时的key值
public static final String GZH_TOKEN = "wxgzh-access-token";
//手动写死一下域名
public static final String AppDomain = "XXX.natapp1.cc";
public static final String GZH_APPID = "XXXXXXXXX";
public static final String GZH_SECURET = "XXXXXXXX";
}
RedisOperator,token不一定必须存到redis中
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
/**
* @Description: 使用redisTemplate的操作实现类
*/
@Component
public class RedisOperator {
// @Autowired
// private RedisTemplate redisTemplate;
@Autowired
private StringRedisTemplate redisTemplate;
// String(字符串)
/**
* 实现命令:SET key value,设置一个key-value(将字符串值 value关联到 key)
*
* @param key
* @param value
*/
public void set(String key, String value) {
redisTemplate.opsForValue().set(key, value);
}
/**
* 实现命令:SET key value EX seconds,设置key-value和超时时间(秒)
*
* @param key
* @param value
* @param timeout (以秒为单位)
*/
public void set(String key, String value, long timeout) {
redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
}
/**
* 实现命令:GET key,返回 key所关联的字符串值。
*
* @param key
* @return value
*/
public String get(String key) {
return (String) redisTemplate.opsForValue().get(key);
}
}
WechatUtil
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.lang3.StringUtils;
/**
* 微信工具类
*/
public class WechatUtil {
/**
* 获得jsapi_ticket
*/
public static String getJsApiTicket(String token) {
String url = Constants.JSAPI_TICKET
+ "?access_token=" + token
+ "&type=jsapi";
String response = HttpClientUtil.doGet(url);
// WXSessionModel model = JsonUtils.jsonToPojo(response, WXSessionModel.class);
// String response = OkHttpUtil.doGet(url);
if (StringUtils.isBlank(response)) {
return null;
}
JSONObject jsonObject = JSONObject.parseObject(response);
System.out.println(response);
String ticket = jsonObject.getString("ticket");
return ticket;
}
}
HttpClientUtil
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 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 param) {
// 创建Httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
String resultString = "";
try {
// 创建Http Post请求
HttpPost httpPost = new HttpPost(url);
// 创建参数列表
if (param != null) {
List 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;
}
}
后端代码略多,因为这里使用了一些工具类。
获取jsApiTicket的时候还需要当前公众号的access_token,可以用下面这个方法获取,这里写入redis的时候的key要与获取jsApiTicket时取值的key对应起来。
/**
* 获取公众号的access_token
* @return
*/
@GetMapping("/getAccessToken")
public IMoocJSONResult getAccessToken() {
String url = "https://api.weixin.qq.com/cgi-bin/token";
Map param = new HashMap<>(16);
param.put("grant_type", "client_credential");
param.put("appid", Constants.GZH_APPID);
param.put("secret", Constants.GZH_SECURET);
String wxResult = HttpClientUtil.doGet(url, param);
System.out.println(wxResult);
WXTokenModel model = JsonUtils.jsonToPojo(wxResult, WXTokenModel.class);
redis.set(Constants.GZH_TOKEN, model.getAccess_token(), Long.parseLong(model.getExpires_in()));
return IMoocJSONResult.ok(model);
}
WXTokenModel
public class WXTokenModel {
private String access_token;
private String expires_in;
private String errcode;
private String errmsg;
public String getAccess_token() {
return access_token;
}
public void setAccess_token(String access_token) {
this.access_token = access_token;
}
public String getExpires_in() {
return expires_in;
}
public void setExpires_in(String expires_in) {
this.expires_in = expires_in;
}
public String getErrcode() {
return errcode;
}
public void setErrcode(String errcode) {
this.errcode = errcode;
}
public String getErrmsg() {
return errmsg;
}
public void setErrmsg(String errmsg) {
this.errmsg = errmsg;
}
}
四,效果
在自己的微信中访问前端页面,因为开启了debug模式,所以每一个请求都弹出请求结果,如果不想查看请求结果,可以将
wx.config({
debug:true,// 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
});
中的debug设置为false
弹出config:ok,表示我们签名和配置信息验证成功,点击扫码,扫码成功之后会将结果会先到前面的输入框中。
大功告成!手动耶!