【微信公众号开发】【2】注册搭建属于自己的公众号
前言:
1,正式的公众号需要在微信公众平台申请(https://mp.weixin.qq.com/);
也可以先申请测试号进行开发,测试帐号申请(https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login);
微信接口调用都是有次数限制的
我这边是先申请的测试号(测试号的功能全,但接口调用的次数少,且不能被搜索到,会过期(但我的目前还没过期))
2,公众号有两种模式,两种模式不能并存
编辑模式:针对非编程人员及信息发布类公众帐号使用。公众平台就相当于该公众号的管理后台。
开发模式:针对具备开发能力的人使用。相关的功能是通过调用微信的接口来实现,公众平台就像是负责授权的接口中心。
3,我的开发环境为:Eclipse + Java + Spring MVC + Spring Boot + Mybatis + Redis + Oracle + Maven
4,请先申请测试号,再看接下来的内容
正文:
1,测试号管理
进入上面的测试号网址,可得到测试号相关的信息,
其中的较为重要的是:appID,appsecret
其中的需要我们处理的有:
(1)接口配置信息
URL :外部能访问到的处理微信交互的地址(例:http://xxx.com/coreServlet,访问该页面报500错误即为调通)
Token:自己指定,保持程序中与该页面一致即可
(2)JS接口安全域名
域名 :在该域名下通过验证方可调用JSSDK(例:xxx.com)
(3)OAuth2.0网页授权
体验接口权限表-网页服务-网页帐号-网页授权获取用户基本信息
授权回调页面域名:与JS接口安全域名一致即可(例:xxx.com)(使用OAuth2.0的url时带的参数要和这里一致)
2,基本要素
公众号调用接口并不是无限制的!(认证帐号可以对实时调用量清零;测试账号次数少很多)
接口调用视情况会需要两个参数(access_token,jsapi_ticket)(有效时间为7200秒,且新获取的会使旧的失效)
解决方案:
(1)可存储到数据库或Redis
(2)将参数存到properties文件中,使用时直接读取,重启服务及每两小时更新一次文件
注:fileName为properties文件的名称(如:tt.properties) ;Config.AppId()为上文的appID
//重启后加载
@PostConstruct
public void init() {
refreshProperties();
}
//更新access_token及jsapi_ticket
@Scheduled(cron = "0 0 0/2 * * ?") //每两小时
public void refreshProperties() {
try {
writeProperties(fileName, Config.AppId(), Config.AppSecret());
} catch (Exception e) {
logger.info("update access_token & jsapi_ticket error:" + e);
}
}
更新properties文件中的数据
public static void writeProperties(String fileName, String appId, String appSecret) throws Exception
{
String url = "https://api.weixin.qq.com/cgi-bin/token";
String ticketUrl = "https://api.weixin.qq.com/cgi-bin/ticket/getticket";
String jsonStrToken = HttpRequest.sendGet(url, "grant_type=client_credential&appid="+ appId + "&secret=" + appSecret);
JSONObject json = JSONObject.fromObject(jsonStrToken);
String access_token = (String) json.getString("access_token");
logger.info("access_token:"+access_token);
String jsonStrTicket = HttpRequest.sendGet(ticketUrl, "access_token=" + access_token + "&type=jsapi");
JSONObject ticketJson = JSONObject.fromObject(jsonStrTicket);
String ticket = (String) ticketJson.get("ticket");
logger.info("ticket:"+ticket);
PropertiesUtil.writeData(fileName, ACCESS_TOKEN, access_token);
PropertiesUtil.writeData(fileName, JSAPI_TICKET, ticket);
}
/** * 修改或添加键值对 如果key存在,修改, 反之,添加。 * @param filePath 文件路径,即文件所在包的路径,例如:java/util/config.properties * @param key 键 * @param value 键对应的值 */ public synchronized static void writeData(String filePath, String key, String value) { //获取绝对路径 OutputStream fos = null; Properties prop = new Properties(); try { logger.info("begin writeData"); File file = new File(filePath); InputStream fis = new FileInputStream(file); prop.load(fis); //一定要在修改值之前关闭fis fis.close(); fos = new FileOutputStream(filePath); prop.setProperty(key, value); //保存,并加入注释 prop.store(fos, "Update '" + key + "' value"); // fos.close(); } catch (IOException e) { logger.error("写token文件异常", e); } finally { try { fos.close(); } catch (IOException e) { logger.info("close:" + filePath + "——error:"+e); } } }
//向指定URL发送GET方法的请求
public static String sendGet(String url, String param)
{
String result = "";
BufferedReader in = null;
try {
String urlNameString = url + "?" + param;
URL realUrl = new URL(urlNameString);
// 打开和URL之间的连接
URLConnection connection = realUrl.openConnection();
// 设置通用的请求属性
connection.setRequestProperty("accept", "*/*");
connection.setRequestProperty("connection", "Keep-Alive");
// 建立实际的连接
connection.connect();
// 获取所有响应头字段
Map<String, List<String>> map = connection.getHeaderFields();
// 遍历所有的响应头字段
for (String key : map.keySet()) {
logger.info(key + "--->" + map.get(key));
}
// 定义 BufferedReader输入流来读取URL的响应
in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String line;
while ((line = in.readLine()) != null) {
result += line;
}
} catch (Exception e) {
logger.info("发送GET请求出现异常!" + e);
e.printStackTrace();
}
// 使用finally块来关闭输入流
finally {
try {
if (in != null) {in.close();}
} catch (Exception e2) {
e2.printStackTrace();
}
}
return result;
}
读取:
public static String getAccessToken(String fileName, String appId, String appSecret)
{
String access_token = PropertiesUtil.readData(fileName, ACCESS_TOKEN);
return access_token;
}
public static String getWeiXinTicket(String fileName, String appId,String appSecret)
{
String ticket = PropertiesUtil.readData(fileName, JSAPI_TICKET);
return ticket;
}
从properties文件中读取数据
public static String readData(String fileName, String key)
{
filePath = PropertiesUtil.class.getResource("/" + fileName).toString(); //获取绝对路径
filePath = filePath.substring(6); //截掉路径的"file:"前缀
Properties props = new Properties();
try {
InputStream in = new BufferedInputStream(new FileInputStream(fileName));
props.load(in);
in.close();
String value = props.getProperty(key);
return value;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
tt.properties
jsapi_ticket=XXXXXX
access_token=XXXX
检验授权凭证(access_token)是否有效
//请求方法 http:GET(请使用https协议) https://api.weixin.qq.com/sns/auth?access_token=ACCESS_TOKEN&openid=OPENID
正确的JSON返回结果:{ "errcode":0,"errmsg":"ok"}
错误时的JSON返回示例:{ "errcode":40003,"errmsg":"invalid openid"}
3,代码(Java)
(1)接口配置信息 URL 需经过微信验证
@ResponseBody
@RequestMapping(value = "coreServlet", method = RequestMethod.GET)
public void coreServletGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
//微信验证地址
String signature = request.getParameter("signature"); //加密签名
String timestamp = request.getParameter("timestamp"); // 时间戳
String nonce = request.getParameter("nonce"); // 随机数
String echostr = request.getParameter("echostr"); // 随机字符串
PrintWriter out = response.getWriter();
// 通过检验signature对请求进行校验,若校验成功则原样返回echostr,表示接入成功,否则接入失败
if (Sign.checkSignature(Config.token(), signature, timestamp, nonce)) {
out.print(echostr);
}
out.close();
out = null;
}
验证签名 上文中的Sign.checkSignature
public static boolean checkSignature(String token, String signature, String timestamp, String nonce)
{
String[] arr = new String[] { token, timestamp, nonce };
// 将token、timestamp、nonce三个参数进行字典序排序
Arrays.sort(arr);
StringBuilder content = new StringBuilder();
for (int i = 0; i < arr.length; i++) {
content.append(arr[i]);
}
MessageDigest md = null;
String tmpStr = null;
try {
md = MessageDigest.getInstance("SHA-1");
// 将三个参数字符串拼接成一个字符串进行sha1加密
byte[] digest = md.digest(content.toString().getBytes());
tmpStr = byteToStr(digest);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
content = null;
// 将sha1加密后的字符串可与signature对比,标识该请求来源于微信
return tmpStr != null ? tmpStr.equals(signature.toUpperCase()) : false;
}
//将字节数组转换为十六进制字符串
private static String byteToStr(byte[] byteArray)
{
String strDigest = "";
for (int i = 0; i < byteArray.length; i++) {
strDigest += byteToHexStr(byteArray[i]);
}
return strDigest;
}
private static String byteToHexStr(byte mByte)
{
char[] Digit = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
char[] tempArr = new char[2];
tempArr[0] = Digit[(mByte >>> 4) & 0X0F];
tempArr[1] = Digit[mByte & 0X0F];
String s = new String(tempArr);
return s;
}
发布后在浏览器里访问CoreServlet,如报500则表示通过
(2)JS接口安全域名 JS发送请求到后台进行验证
前台JS: 要用JSSDK都需要先引用JS文件,(支持https):http://res.wx.qq.com/open/js/jweixin-1.2.0.js
var wxUrl= location.href.split('#')[0];
$.ajax({
type : "post",
url : "/getTicket",
data : {
"url":wxUrl
},
dataType : "json",
success : function(data){
var obj = data;
wx.config({
debug: false, //开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
appId: obj.appId, //必填,公众号的唯一标识
timestamp: obj.timestamp, //必填,生成签名的时间戳
nonceStr: obj.nonceStr, //必填,生成签名的随机串
signature: obj.signature,//必填,签名,见微信开发文档附录1
jsApiList: ['chooseImage','previewImage','uploadImage','downloadImage'] // 必填,需要使用的JS接口列表,所有JS接口列表见附录2
});
}
});
后台验证:
//调用JSJDK前的验证
@ResponseBody
@RequestMapping(value = "getTicket", method = RequestMethod.POST)
public Map<String, String> getTicket(String url) throws Exception
{
//获取jsapi_ticket,是公众号用于调用微信JS接口的临时票据。正常情况下,jsapi_ticket的有效期为7200秒,通过access_token来获取。
String jsapi_ticket = WeixinUtil.getWeiXinTicket(Config.joinTokenFileName, Config.appId(), Config.appSecret());
//URL为要调用接口的页面路径
Map<String, String> ret = Sign.sign(Config.appId(), jsapi_ticket, url);
return ret;
}
WeixinUtil.getWeiXinTicket(见上文2中的获取jsapi_ticket方法)
Sign.sign
public static Map<String, String> sign(String appId, String jsapi_ticket, String url) { Map<String, String> ret = new HashMap<String, String>(); String nonce_str = create_nonce_str();//签名的随机串 String timestamp = create_timestamp();//生成签名的时间戳 String string1; String signature = "";//签名 //注意这里参数名必须全部小写,且必须有序 string1 = "jsapi_ticket=" + jsapi_ticket + "&noncestr=" + nonce_str + "×tamp=" + timestamp + "&url=" + url; 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(); } ret.put("url", url); ret.put("jsapi_ticket", jsapi_ticket); ret.put("nonceStr", nonce_str); ret.put("timestamp", timestamp); ret.put("signature", signature); ret.put("appId", appId); return ret; } 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 create_nonce_str() { return UUID.randomUUID().toString(); } private static String create_timestamp() { return Long.toString(System.currentTimeMillis() / 1000); }