一、准备阶段
需要准备事项:
1.一个能在公网上访问的项目:
见:【 Java微信公众平台开发_01_本地服务器映射外网 】
2.一个微信公众平台账号:
去注册:(https://mp.weixin.qq.com/)
3.策略文件
见:【 Java企业微信开发_Exception_02_java.security.InvalidKeyException: Illegal key size 】
4.微信官方消息加解密工具包
需要下载微信官方的消息加解密的工具包,主要是AES加密工具。由于这是企业微信的加解密包,因此我们后面还需要对这个加解密包进行一些修改。
下载地址:https://wximg.gtimg.com/shake_tv/mpwiki/cryptoDemo.zip
下载完将其添加进工程中:
二、填写服务器配置
1.记住 AppID 和 AppSecret
登录微信公众平台,开发—>基本配置—>公众号开发信息
2.设置IP白名单
(1)登录微信公众平台,开发—>基本配置—>公众号开发信息—>ip白名单—>查看—>修改
(2)需要将服务器的公网ip添加进去。若需要添加多个ip,则每行添加一个ip即可。
(3)查询服务器的公网ip:
window系统: 直接百度搜索ip即可
Linux系统: 参见【 Linux_服务器_01_查看公网IP 】
3.填写服务器配置
登录微信公众平台,开发—>基本配置—>服务器配置—>修改配置
3.1 URL:
开发者用来接收微信消息和事件的接口URL 。在三种情况下会请求这个URL:
(1)回调模式:
填写完服务器配置,点击提交,微信服务器将发送GET请求到填写的服务器地址URL上,并携带上四个参数 signature 、timestamp、nonce、echostr
(2)接收消息:
当用户发送消息给公众号时,消息将被以POST方式推送到到填写的服务器地址URL上,
在安全模式(推荐)下,携带上六个参数 signature 、timestamp、nonce、openid、encrypt_type(加密类型,为aes)和msg_signature(消息体签名,用于验证消息体的正确性)
在明文模式(默认)下,携带上六个参数 signature 、timestamp、nonce、openid
(3)接收事件
当特定的用户操作引发事件推送时,(如用户点击某个click菜单按钮时),事件将被以POST方式推送到到填写的服务器地址URL上。 请求参数 同 接受消息
3.2 Token:
随机填写,要与代码中保持一致。生成加解密工具类、生成签名 时会用到
3.3 EncodingAESKey:
随机生成,要与代码中保持一致,生成加解密工具类时会用到。EncodingAESKey即消息加解密Key,长度固定为43个字符,从a-z,A-Z,0-9共62个字符中选取。
3.4 消息加解密方式
这里选择安全模式,这样在接收消息和事件时,都需要进行消息加解密。若选明文模式,则在接收消息和事件时,都不需要进行消息加解密。
三、验证服务器地址的有效性
1. 设置失败
填写完服务器配置后,这时我们点击提交,会提示设置失败。这是因为我们点击提交后,微信服务器将发送GET请求到填写的服务器地址URL上,并携带上四个参数 signature 、timestamp、nonce、echostr ,开发者接收到者四个参数之后,需要对这四个参数与token一起 进行签名校验。
2.签名校验的流程
(1)将token、timestamp、nonce三个参数进行字典序排序
(2)将三个参数字符串拼接成一个字符串进行sha1加密 得到一个签名signature1
(3)开发者将获得加密后的字符串signature1与signature进行比较
(4)若二者相同,则认为此次GET请求来自微信服务器,可原样返回echostr参数内容,配置成功。
若二者不相同,则认为此次GET请求不是来自微信服务器,不可原样返回echostr参数内容,配置失败。
总结:用 token、timestamp、nonce 生成一个签名signature1,并与signature比较,若相同,则原样返回echostr,若不同则配置失败。
3.微信服务器怎么判断签名校验是否成功?
若微信服务器收到原样的echostr,则任务校验成功。也就是说如果你收到signature 、timestamp、nonce、echostr 后,什么都不做,就只原样返回一个echostr,微信服务器还是认为你校验成功了。
4.签名校验的代码实现
4.1 用SHA1算法生成安全签名 signature1
/**
* 微信公众号SHA1加密算法
* 用SHA1算法生成安全签名
* @param token 票据
* @param timestamp 时间戳
* @param nonce 随机字符串
* @return 安全签名
* @throws AesException
*/
public static String getSHA1_WXGZ(String token, String timestamp, String nonce) throws AesException
{
try {
String[] array = new String[] { token, timestamp, nonce };
StringBuffer sb = new StringBuffer();
//1.将token、timestamp、nonce三个参数进行字典序排序
Arrays.sort(array);
//2.将三个参数字符串拼接成一个字符串进行sha1加密
for (int i = 0; i < 3; i++) {
sb.append(array[i]);
}
String str = sb.toString();
//2.2 SHA1签名生成
MessageDigest md = MessageDigest.getInstance("SHA-1");
md.update(str.getBytes());
byte[] digest = md.digest();
StringBuffer hexstr = new StringBuffer();
String shaHex = "";
for (int i = 0; i < digest.length; i++) {
shaHex = Integer.toHexString(digest[i] & 0xFF);
if (shaHex.length() < 2) {
hexstr.append(0);
}
hexstr.append(shaHex);
}
return hexstr.toString();
} catch (Exception e) {
e.printStackTrace();
throw new AesException(AesException.ComputeSignatureError);
}
}
}
View Code
4.2 验证signature1是否与signature相同
/**
*
* 验证URL
* @param msgSignature
* @param timeStamp 时间戳,对应URL参数的timestamp
* @param nonce 随机串,对应URL参数的nonce
* @param echoStr 随机串,对应URL参数的echostr
*
* @return 解密之后的echostr
* @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
*/
/**
* @desc :微信公众号 验证url
*
* @param msgSignature 签名串,对应URL参数的msg_signature
* @param token 公众平台上,开发者设置的token
* @param timeStamp 时间戳,对应URL参数的timestamp
* @param nonce 随机数,对应URL参数的nonce
* @return
* true 验证成功
* @throws AesException boolean
*/
public boolean verifyUrl_WXGZ(String msgSignature, String token , String timeStamp, String nonce) throws AesException {
//1.进行SHA1加密
String signature = SHA1.getSHA1_WXGZ(token, timeStamp, nonce);
//2.验证 token、timestamp、nonce进行SHA1加密生成的signature 是否与url传过来msgSignature相同
if (!signature.equals(msgSignature)) {
throw new AesException(AesException.ValidateSignatureError);
//3.若相同,则url验证成功,返回true
}else{
return true;
}
}
View Code
4.3 在servlet的doGet方法中进行url的校验
//1.接收 回调模式 的请求
protected void doGet(HttpServletRequest request, HttpServletResponse response) {
logger.info("get--------------");
//一、校验URL
//1.准备校验参数
// 微信加密签名
String msgSignature = request.getParameter("signature");
// 时间戳
String timeStamp = request.getParameter("timestamp");
// 随机数
String nonce = request.getParameter("nonce");
// 随机字符串
String echoStr = request.getParameter("echostr");
PrintWriter out=null;
try {
//2.校验url
//2.1 创建加解密类
WXBizMsgCrypt wxcpt = new WXBizMsgCrypt(Env.TOKEN,Env.ENCODING_AES_KEY,Env.APP_ID);
//2.2进行url校验
//不抛异常就说明校验成功
boolean mark= wxcpt.verifyUrl_WXGZ(msgSignature, Env.TOKEN, timeStamp, nonce);
//2.3若校验成功,则原样返回 echoStr
if (mark) {
out = response.getWriter();
out.print(echoStr);
}
} catch (AesException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
if (out != null) {
out.close();
out = null; //释放资源
}
}
}
View Code
四、代码实现
1.微信配置类—Env.java
微信公众号接入配置类
package com.ray.weixin.gz.config;
/**@desc : 微信公众号接入配置
*
* @author: shirayner
* @date : 2017年9月27日 下午4:57:36
*/
public class Env {
/**
* 1. 企业应用接入秘钥相关
*/
//public static final String APP_ID = "wx4dca3424bebef2cc";
// public static final String APP_SECRET = "068e2599abf88ba78491a07906f3c56e";
//测试号
public static final String APP_ID = "wxa0064ea657f80062";
public static final String APP_SECRET = "fcc960840df869ad1a46af7993784917";
/**
* 2.服务器配置:
* 启用并设置服务器配置后,用户发给公众号的消息以及开发者需要的事件推送,将被微信转发到该URL中
*/
public static final String TOKEN = "weixin";
public static final String ENCODING_AES_KEY = "JvJ1Dww6tjUU2psC3pmokXvOHHfovfWP3LfX1xrriz1";
}
View Code
2.HTTP请求工具类—HttpHelper.java
package com.ray.weixin.gz.util;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import org.apache.http.Header;
import org.apache.http.HeaderElement;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.client.config.RequestConfig;
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.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.util.EntityUtils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
/**
* HTTP请求封装,建议直接使用sdk的API
*/
public class HttpHelper {
/**
* @desc :1.发起GET请求
*
* @param url
* @return JSONObject
* @throws Exception
*/
public static JSONObject doGet(String url) throws Exception {
//1.生成一个请求
HttpGet httpGet = new HttpGet(url);
//2.配置请求的属性
RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(5000).setConnectTimeout(5000).build();//2000
httpGet.setConfig(requestConfig);
//3.发起请求,获取响应信息
//3.1 创建httpClient
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
try {
//3.2 发起请求,获取响应信息
response = httpClient.execute(httpGet, new BasicHttpContext());
//如果返回结果的code不等于200,说明出错了
if (response.getStatusLine().getStatusCode() != 200) {
System.out.println("request url failed, http code=" + response.getStatusLine().getStatusCode()
+ ", url=" + url);
return null;
}
//4.解析请求结果
HttpEntity entity = response.getEntity(); //reponse返回的数据在entity中
if (entity != null) {
String resultStr = EntityUtils.toString(entity, "utf-8"); //将数据转化为string格式
System.out.println("GET请求结果:"+resultStr);
JSONObject result = JSON.parseObject(resultStr); //将String转换为 JSONObject
if(result.getInteger("errcode")==null) {
return result;
}else if (0 == result.getInteger("errcode")) {
return result;
}else {
System.out.println("request url=" + url + ",return value=");
System.out.println(resultStr);
int errCode = result.getInteger("errcode");
String errMsg = result.getString("errmsg");
throw new Exception("error code:"+errCode+", error message:"+errMsg);
}
}
} catch (IOException e) {
System.out.println("request url=" + url + ", exception, msg=" + e.getMessage());
e.printStackTrace();
} finally {
if (response != null) try {
response.close(); //释放资源
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
/** 2.发起POST请求
* @desc :
*
* @param url 请求url
* @param data 请求参数(json)
* @return
* @throws Exception JSONObject
*/
public static JSONObject doPost(String url, Object data) throws Exception {
//1.生成一个请求
HttpPost httpPost = new HttpPost(url);
//2.配置请求属性
//2.1 设置请求超时时间
RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(100000).setConnectTimeout(100000).build();
httpPost.setConfig(requestConfig);
//2.2 设置数据传输格式-json
httpPost.addHeader("Content-Type", "application/json");
//2.3 设置请求参数
StringEntity requestEntity = new StringEntity(JSON.toJSONString(data), "utf-8");
httpPost.setEntity(requestEntity);
//3.发起请求,获取响应信息
//3.1 创建httpClient
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
try {
//3.3 发起请求,获取响应
response = httpClient.execute(httpPost, new BasicHttpContext());
if (response.getStatusLine().getStatusCode() != 200) {
System.out.println("request url failed, http code=" + response.getStatusLine().getStatusCode()
+ ", url=" + url);
return null;
}
//获取响应内容
HttpEntity entity = response.getEntity();
if (entity != null) {
String resultStr = EntityUtils.toString(entity, "utf-8");
System.out.println("POST请求结果:"+resultStr);
//解析响应内容
JSONObject result = JSON.parseObject(resultStr);
if(result.getInteger("errcode")==null) {
return result;
}else if (0 == result.getInteger("errcode")) {
return result;
}else {
System.out.println("request url=" + url + ",return value=");
System.out.println(resultStr);
int errCode = result.getInteger("errcode");
String errMsg = result.getString("errmsg");
throw new Exception("error code:"+errCode+", error message:"+errMsg);
}
}
} catch (IOException e) {
System.out.println("request url=" + url + ", exception, msg=" + e.getMessage());
e.printStackTrace();
} finally {
if (response != null) try {
response.close(); //释放资源
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
/**
* @desc : 3.上传文件
*
* @param url 请求url
* @param file 上传的文件
* @return
* @throws Exception JSONObject
*/
public static JSONObject uploadMedia(String url, File file) throws Exception {
HttpPost httpPost = new HttpPost(url);
CloseableHttpResponse response = null;
CloseableHttpClient httpClient = HttpClients.createDefault();
RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(5000).setConnectTimeout(5000).build();
httpPost.setConfig(requestConfig);
HttpEntity requestEntity = MultipartEntityBuilder.create().addPart("media",
new FileBody(file, ContentType.APPLICATION_OCTET_STREAM, file.getName())).build();
httpPost.setEntity(requestEntity);
try {
response = httpClient.execute(httpPost, new BasicHttpContext());
if (response.getStatusLine().getStatusCode() != 200) {
System.out.println("request url failed, http code=" + response.getStatusLine().getStatusCode()
+ ", url=" + url);
return null;
}
HttpEntity entity = response.getEntity();
if (entity != null) {
String resultStr = EntityUtils.toString(entity, "utf-8");
JSONObject result = JSON.parseObject(resultStr);
//上传临时素材成功
if (result.getString("errcode")== null) {
// 成功
//result.remove("errcode");
//result.remove("errmsg");
return result;
} else {
System.out.println("request url=" + url + ",return value=");
System.out.println(resultStr);
int errCode = result.getInteger("errcode");
String errMsg = result.getString("errmsg");
throw new Exception("error code:"+errCode+", error message:"+errMsg);
}
}
} catch (IOException e) {
System.out.println("request url=" + url + ", exception, msg=" + e.getMessage());
e.printStackTrace();
} finally {
if (response != null) try {
response.close(); //释放资源
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
/**
* @desc : 4.下载文件 -get
*
* @param url 请求url
* @param fileDir 下载路径
* @return
* @throws Exception File
*/
public static File downloadMedia(String url, String fileDir) throws Exception {
//1.生成一个请求
HttpGet httpGet = new HttpGet(url);
//2.配置请求属性
RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(100000).setConnectTimeout(100000).build();
httpGet.setConfig(requestConfig);
//3.发起请求,获取响应信息
//3.1 创建httpClient
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
//4.设置本地保存的文件
//File file = new File(fileDir);
File file = null;
try {
//5. 发起请求,获取响应信息
response = httpClient.execute(httpGet, new BasicHttpContext());
System.out.println("HttpStatus.SC_OK:"+HttpStatus.SC_OK);
System.out.println("response.getStatusLine().getStatusCode():"+response.getStatusLine().getStatusCode());
System.out.println("http-header:"+JSON.toJSONString( response.getAllHeaders() ));
System.out.println("http-filename:"+getFileName(response) );
//请求成功
if(HttpStatus.SC_OK==response.getStatusLine().getStatusCode()){
//6.取得请求内容
HttpEntity entity = response.getEntity();
if (entity != null) {
//这里可以得到文件的类型 如image/jpg /zip /tiff 等等 但是发现并不是十分有效,有时明明后缀是.rar但是取到的是null,这点特别说明
System.out.println(entity.getContentType());
//可以判断是否是文件数据流
System.out.println(entity.isStreaming());
//6.1 输出流
//6.1.1获取文件名,拼接文件路径
String fileName=getFileName(response);
fileDir=fileDir+fileName;
file = new File(fileDir);
//6.1.2根据文件路径获取输出流
FileOutputStream output = new FileOutputStream(file);
//6.2 输入流:从钉钉服务器返回的文件流,得到网络资源并写入文件
InputStream input = entity.getContent();
//6.3 将数据写入文件:将输入流中的数据写入到输出流
byte b[] = new byte[1024];
int j = 0;
while( (j = input.read(b))!=-1){
output.write(b,0,j);
}
output.flush();
output.close();
}
if (entity != null) {
entity.consumeContent();
}
}
} catch (IOException e) {
System.out.println("request url=" + url + ", exception, msg=" + e.getMessage());
e.printStackTrace();
} finally {
if (response != null) try {
response.close(); //释放资源
} catch (IOException e) {
e.printStackTrace();
}
}
return file;
}
/**
* @desc : 5.下载文件 - post
*
* @param url 请求url
* @param data post请求参数
* @param fileDir 文件下载路径
* @return
* @throws Exception File
*/
public static File downloadMedia(String url, Object data, String fileDir) throws Exception {
//1.生成一个请求
HttpPost httpPost = new HttpPost(url);
//2.配置请求属性
//2.1 设置请求超时时间
RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(100000).setConnectTimeout(100000).build();
httpPost.setConfig(requestConfig);
//2.2 设置数据传输格式-json
httpPost.addHeader("Content-Type", "application/json");
//2.3 设置请求参数
StringEntity requestEntity = new StringEntity(JSON.toJSONString(data), "utf-8");
httpPost.setEntity(requestEntity);
//3.发起请求,获取响应信息
//3.1 创建httpClient
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
//4.设置本地保存的文件
//File file = new File(fileDir);
File file = null;
try {
//5. 发起请求,获取响应信息
response = httpClient.execute(httpPost, new BasicHttpContext());
System.out.println("HttpStatus.SC_OK:"+HttpStatus.SC_OK);
System.out.println("response.getStatusLine().getStatusCode():"+response.getStatusLine().getStatusCode());
System.out.println("http-header:"+JSON.toJSONString( response.getAllHeaders() ));
System.out.println("http-filename:"+getFileName(response) );
//请求成功
if(HttpStatus.SC_OK==response.getStatusLine().getStatusCode()){
//6.取得请求内容
HttpEntity entity = response.getEntity();
if (entity != null) {
//这里可以得到文件的类型 如image/jpg /zip /tiff 等等 但是发现并不是十分有效,有时明明后缀是.rar但是取到的是null,这点特别说明
System.out.println(entity.getContentType());
//可以判断是否是文件数据流
System.out.println(entity.isStreaming());
//6.1 输出流
//6.1.1获取文件名,拼接文件路径
String fileName=getFileName(response);
fileDir=fileDir+fileName;
file = new File(fileDir);
//6.1.2根据文件路径获取输出流
FileOutputStream output = new FileOutputStream(file);
//6.2 输入流:从钉钉服务器返回的文件流,得到网络资源并写入文件
InputStream input = entity.getContent();
//6.3 将数据写入文件:将输入流中的数据写入到输出流
byte b[] = new byte[1024];
int j = 0;
while( (j = input.read(b))!=-1){
output.write(b,0,j);
}
output.flush();
output.close();
}
if (entity != null) {
entity.consumeContent();
}
}
} catch (IOException e) {
System.out.println("request url=" + url + ", exception, msg=" + e.getMessage());
e.printStackTrace();
} finally {
if (response != null) try {
response.close(); //释放资源
} catch (IOException e) {
e.printStackTrace();
}
}
return file;
}
/** 5. 获取response header中Content-Disposition中的filename值
* @desc :
*
* @param response 响应
* @return String
*/
public static String getFileName(HttpResponse response) {
Header contentHeader = response.getFirstHeader("Content-Disposition");
String filename = null;
if (contentHeader != null) {
HeaderElement[] values = contentHeader.getElements();
if (values.length == 1) {
NameValuePair param = values[0].getParameterByName("filename");
if (param != null) {
try {
//filename = new String(param.getValue().toString().getBytes(), "utf-8");
//filename=URLDecoder.decode(param.getValue(),"utf-8");
filename = param.getValue();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
return filename;
}
}
View Code
3.Token相关工具类—AuthHelper.java
package com.ray.weixin.gz.util;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Formatter;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.alibaba.fastjson.JSONObject;
import com.ray.weixin.gz.config.Env;
/**
* 微信公众号 Token、配置工具类
* @desc : AccessToken、Jsticket 、Jsapi
*
* @author: shirayner
* @date : 2017年9月27日 下午5:00:25
*/
public class AuthHelper {
private static final Logger logger = LogManager.getLogger(AuthHelper.class);
//1.获取access_token的接口地址,有效期为7200秒
private static final String GET_ACCESSTOKEN_URL="https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET";
//2.获取getJsapiTicket的接口地址,有效期为7200秒
private static final String GET_JSAPITICKET_URL="https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=ACCESS_TOKEN&type=jsapi";
//3.通过code换取网页授权access_token
private static final String GET_ACCESSTOKEN_BYCODE_URL="https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code";
/**
* @desc :1.获取access_token
*
* @param appId 第三方用户唯一凭证
* @param appSecret 第三方用户唯一凭证密钥,即appsecret
*
* @return
* access_token 获取到的凭证
* expires_in 凭证有效时间,单位:秒
* @throws Exception String
*/
public static String getAccessToken(String appId,String appSecret) throws Exception {
//1.获取请求url
String url=GET_ACCESSTOKEN_URL.replace("APPID", appId).replace("APPSECRET", appSecret);
//2.发起GET请求,获取返回结果
JSONObject jsonObject=HttpHelper.doGet(url);
logger.info("jsonObject:"+jsonObject.toJSONString());
//3.解析结果,获取accessToken
String accessToken="";
if (null != jsonObject) {
//4.错误消息处理
if (jsonObject.getInteger("errcode")!=null && 0 != jsonObject.getInteger("errcode")) {
int errCode = jsonObject.getInteger("errcode");
String errMsg = jsonObject.getString("errmsg");
throw new Exception("error code:"+errCode+", error message:"+errMsg);
//5.成功获取accessToken
}else {
accessToken=jsonObject.getString("access_token");
}
}
return accessToken;
}
/**
* @desc :2.获取JsapiTicket
*
* @param accessToken 有效凭证
* @return
* @throws Exception String
*/
public static String getJsapiTicket(String accessToken) throws Exception {
//1.获取请求url
String url=GET_JSAPITICKET_URL.replace("ACCESS_TOKEN", accessToken);
//2.发起GET请求,获取返回结果
JSONObject jsonObject=HttpHelper.doGet(url);
logger.info("jsonObject:"+jsonObject.toJSONString());
//3.解析结果,获取accessToken
String jsapiTicket="";
if (null != jsonObject) {
//4.错误消息处理
if (jsonObject.getInteger("errcode")!=null && 0 != jsonObject.getInteger("errcode")) {
int errCode = jsonObject.getInteger("errcode");
String errMsg = jsonObject.getString("errmsg");
throw new Exception("error code:"+errCode+", error message:"+errMsg);
//5.成功获取jsapiTicket
}else {
jsapiTicket=jsonObject.getString("ticket");
}
}
return jsapiTicket;
}
/**
* @desc : 3.通过code换取网页授权access_token
*
* @param appId 第三方用户唯一凭证
* @param appSecret 第三方用户唯一凭证密钥,即appsecret
* @param Code code作为换取access_token的票据,每次用户授权带上的code将不一样,code只能使用一次,5分钟未被使用自动过期。
*
* @return
* access_token 网页授权接口调用凭证,注意:此access_token与基础支持的access_token不同
* expires_in access_token接口调用凭证超时时间,单位(秒)
* refresh_token 用户刷新access_token
* openid 用户唯一标识,请注意,在未关注公众号时,用户访问公众号的网页,也会产生一个用户和公众号唯一的OpenID
* scope 用户授权的作用域,使用逗号(,)分隔
*
* @throws Exception String
*/
public static JSONObject getAccessTokenByCode(String appId,String appSecret,String code) throws Exception {
//1.获取请求url
String url=GET_ACCESSTOKEN_BYCODE_URL.replace("APPID", appId).replace("SECRET", appSecret).replace("CODE", code);
//2.发起GET请求,获取返回结果
JSONObject jsonObject=HttpHelper.doGet(url);
logger.info("jsonObject:"+jsonObject.toJSONString());
//3.解析结果,获取accessToken
JSONObject returnJsonObject=null;
if (null != jsonObject) {
//4.错误消息处理
if (jsonObject.getInteger("errcode")!=null && 0 != jsonObject.getInteger("errcode")) {
int errCode = jsonObject.getInteger("errcode");
String errMsg = jsonObject.getString("errmsg");
throw new Exception("error code:"+errCode+", error message:"+errMsg);
//5.成功获取accessToken
}else {
returnJsonObject=jsonObject;
}
}
return returnJsonObject;
}
/**
* @desc :4.获取前端jsapi需要的配置参数
*
* @param request
* @return String
*/
public static String getJsapiConfig(HttpServletRequest request){
//1.准备好参与签名的字段
//1.1 url
/*
*以http://localhost/test.do?a=b&c=d为例
*request.getRequestURL的结果是http://localhost/test.do
*request.getQueryString的返回值是a=b&c=d
*/
String urlString = request.getRequestURL().toString();
String queryString = request.getQueryString();
String queryStringEncode = null;
String url;
if (queryString != null) {
queryStringEncode = URLDecoder.decode(queryString);
url = urlString + "?" + queryStringEncode;
} else {
url = urlString;
}
//1.2 noncestr
String nonceStr=UUID.randomUUID().toString(); //随机数
//1.3 timestamp
long timeStamp = System.currentTimeMillis() / 1000; //时间戳参数
String signedUrl = url;
String accessToken = null;
String ticket = null;
String signature = null; //签名
try {
//1.4 jsapi_ticket
accessToken=getAccessToken(Env.APP_ID, Env.APP_SECRET);
ticket=getJsapiTicket(accessToken);
//2.进行签名,获取signature
signature=getSign(ticket,nonceStr,timeStamp,signedUrl);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
logger.info("accessToken:"+accessToken);
logger.info("ticket:"+ticket);
logger.info("nonceStr:"+nonceStr);
logger.info("timeStamp:"+timeStamp);
logger.info("signedUrl:"+signedUrl);
logger.info("signature:"+signature);
logger.info("appId:"+Env.APP_ID);
String configValue = "{signature:'" + signature + "',nonceStr:'" + nonceStr + "',timeStamp:'"
+ timeStamp + "',appId:'" + Env.APP_ID + "'}";
logger.info("configValue:"+configValue);
return configValue;
}
/**
* @desc : 3.生成签名的函数
*
* @param ticket jsticket
* @param nonceStr 随机串,自己定义
* @param timeStamp 生成签名用的时间戳
* @param url 需要进行免登鉴权的页面地址,也就是执行dd.config的页面地址
* @return
* @throws Exception String
*/
public static String getSign(String jsTicket, String nonceStr, Long timeStamp, String url) throws Exception {
String plainTex = "jsapi_ticket=" + jsTicket + "&noncestr=" + nonceStr + "×tamp=" + timeStamp + "&url=" + url;
System.out.println(plainTex);
try {
MessageDigest crypt = MessageDigest.getInstance("SHA-1");
crypt.reset();
crypt.update(plainTex.getBytes("UTF-8"));
return byteToHex(crypt.digest());
} catch (NoSuchAlgorithmException e) {
throw new Exception(e.getMessage());
} catch (UnsupportedEncodingException e) {
throw new Exception(e.getMessage());
}
}
//将bytes类型的数据转化为16进制类型
private static String byteToHex(byte[] hash) {
Formatter formatter = new Formatter();
for (byte b : hash) {
formatter.format("%02x", new Object[] { Byte.valueOf(b) });
}
String result = formatter.toString();
formatter.close();
return result;
}
}
View Code
4.验证URL的servlet—WeiXinServlet
package com.ray.weixin.gz.controller;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.qq.weixin.mp.aes.AesException;
import com.qq.weixin.mp.aes.WXBizMsgCrypt;
import com.ray.weixin.gz.config.Env;
import com.ray.weixin.gz.service.message.ReplyMessageService;
/**
* Servlet implementation class WeiXinServlet
*/
public class WeiXinServlet extends HttpServlet {
private static final Logger logger = LogManager.getLogger(WeiXinServlet.class);
private static final long serialVersionUID = 1L;
/**
* Default constructor.
*/
public WeiXinServlet() {
// TODO Auto-generated constructor stub
}
//1.接收 回调模式 的请求
protected void doGet(HttpServletRequest request, HttpServletResponse response) {
logger.info("get--------------");
//一、校验URL
//1.准备校验参数
// 微信加密签名
String msgSignature = request.getParameter("signature");
// 时间戳
String timeStamp = request.getParameter("timestamp");
// 随机数
String nonce = request.getParameter("nonce");
// 随机字符串
String echoStr = request.getParameter("echostr");
PrintWriter out=null;
try {
//2.校验url
//2.1 创建加解密类
WXBizMsgCrypt wxcpt = new WXBizMsgCrypt(Env.TOKEN,Env.ENCODING_AES_KEY,Env.APP_ID);
//2.2进行url校验
//不抛异常就说明校验成功
String sEchoStr= wxcpt.verifyUrl_WXGZ(msgSignature, Env.TOKEN, timeStamp, nonce,echoStr);
//2.3若校验成功,则原样返回 echoStr
out = response.getWriter();
out.print(sEchoStr);
} catch (AesException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
if (out != null) {
out.close();
out = null; //释放资源
}
}
}
//2.接收 微信消息和事件 的请求
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
logger.info("post--------------");
//1.将请求、响应的编码均设置为UTF-8(防止中文乱码)
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
//2.调用消息业务类接收消息、处理消息
String respMessage = ReplyMessageService.reply(request);
//3.响应消息
PrintWriter out = response.getWriter();
out.print(respMessage);
out.close();
}
}
View Code
5.微信公众号SHA1加密算法工具类—SHA1.java
此类是信官方的消息加解密的工具包中的一个类,我在原来的基础上增加了一个方法 getSHA1_WXGZ(String, String, String)
/**
* 对公众平台发送给公众账号的消息加解密示例代码.
*
* @copyright Copyright (c) 1998-2014 Tencent Inc.
*/
// ------------------------------------------------------------------------
package com.qq.weixin.mp.aes;
import java.security.MessageDigest;
import java.util.Arrays;
/**
* SHA1 class
*
* 计算公众平台的消息签名接口.
*/
class SHA1 {
/**
* 用SHA1算法生成安全签名
* @param token 票据
* @param timestamp 时间戳
* @param nonce 随机字符串
* @param encrypt 密文
* @return 安全签名
* @throws AesException
*/
public static String getSHA1(String token, String timestamp, String nonce, String encrypt) throws AesException
{
try {
String[] array = new String[] { token, timestamp, nonce, encrypt };
StringBuffer sb = new StringBuffer();
// 字符串排序
Arrays.sort(array);
for (int i = 0; i < 4; i++) {
sb.append(array[i]);
}
String str = sb.toString();
// SHA1签名生成
MessageDigest md = MessageDigest.getInstance("SHA-1");
md.update(str.getBytes());
byte[] digest = md.digest();
StringBuffer hexstr = new StringBuffer();
String shaHex = "";
for (int i = 0; i < digest.length; i++) {
shaHex = Integer.toHexString(digest[i] & 0xFF);
if (shaHex.length() < 2) {
hexstr.append(0);
}
hexstr.append(shaHex);
}
return hexstr.toString();
} catch (Exception e) {
e.printStackTrace();
throw new AesException(AesException.ComputeSignatureError);
}
}
/**
* 微信公众号SHA1加密算法
* 用SHA1算法生成安全签名
* @param token 票据
* @param timestamp 时间戳
* @param nonce 随机字符串
* @return 安全签名
* @throws AesException
*/
public static String getSHA1_WXGZ(String token, String timestamp, String nonce) throws AesException
{
try {
String[] array = new String[] { token, timestamp, nonce };
StringBuffer sb = new StringBuffer();
//1.将token、timestamp、nonce三个参数进行字典序排序
Arrays.sort(array);
//2.将三个参数字符串拼接成一个字符串进行sha1加密
for (int i = 0; i < 3; i++) {
sb.append(array[i]);
}
String str = sb.toString();
//2.2 SHA1签名生成
MessageDigest md = MessageDigest.getInstance("SHA-1");
md.update(str.getBytes());
byte[] digest = md.digest();
StringBuffer hexstr = new StringBuffer();
String shaHex = "";
for (int i = 0; i < digest.length; i++) {
shaHex = Integer.toHexString(digest[i] & 0xFF);
if (shaHex.length() < 2) {
hexstr.append(0);
}
hexstr.append(shaHex);
}
return hexstr.toString();
} catch (Exception e) {
e.printStackTrace();
throw new AesException(AesException.ComputeSignatureError);
}
}
}
View Code
6.校验url—WXBizMsgCrypt.java
此类是信官方的消息加解密的工具包中的一个类,我在原来的基础上增加了一个方法 verifyUrl_WXGZ(String msgSignature, String token , String timeStamp, String nonce,String echoStr)
/**
* 对公众平台发送给公众账号的消息加解密示例代码.
*
* @copyright Copyright (c) 1998-2014 Tencent Inc.
*/
// ------------------------------------------------------------------------
/**
* 针对org.apache.commons.codec.binary.Base64,
* 需要导入架包commons-codec-1.9(或commons-codec-1.8等其他版本)
* 官方下载地址:http://commons.apache.org/proper/commons-codec/download_codec.cgi
*/
package com.qq.weixin.mp.aes;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Random;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base64;
/**
* 提供接收和推送给公众平台消息的加解密接口(UTF8编码的字符串).
* <ol>
* <li>第三方回复加密消息给公众平台</li>
* <li>第三方收到公众平台发送的消息,验证消息的安全性,并对消息进行解密。</li>
* </ol>
* 说明:异常java.security.InvalidKeyException:illegal Key Size的解决方案
* <ol>
* <li>在官方网站下载JCE无限制权限策略文件(JDK7的下载地址:
* http://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html</li>
* <li>下载后解压,可以看到local_policy.jar和US_export_policy.jar以及readme.txt</li>
* <li>如果安装了JRE,将两个jar文件放到%JRE_HOME%\lib\security目录下覆盖原来的文件</li>
* <li>如果安装了JDK,将两个jar文件放到%JDK_HOME%\jre\lib\security目录下覆盖原来文件</li>
* </ol>
*/
public class WXBizMsgCrypt {
static Charset CHARSET = Charset.forName("utf-8");
Base64 base64 = new Base64();
byte[] aesKey;
String token;
String corpId;
/**
* 构造函数
* @param token 公众平台上,开发者设置的token
* @param encodingAesKey 公众平台上,开发者设置的EncodingAESKey
* @param corpId 企业的corpid
*
* @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
*/
public WXBizMsgCrypt(String token, String encodingAesKey, String corpId) throws AesException {
if (encodingAesKey.length() != 43) {
throw new AesException(AesException.IllegalAesKey);
}
this.token = token;
this.corpId = corpId;
aesKey = Base64.decodeBase64(encodingAesKey + "=");
}
// 生成4个字节的网络字节序
byte[] getNetworkBytesOrder(int sourceNumber) {
byte[] orderBytes = new byte[4];
orderBytes[3] = (byte) (sourceNumber & 0xFF);
orderBytes[2] = (byte) (sourceNumber >> 8 & 0xFF);
orderBytes[1] = (byte) (sourceNumber >> 16 & 0xFF);
orderBytes[0] = (byte) (sourceNumber >> 24 & 0xFF);
return orderBytes;
}
// 还原4个字节的网络字节序
int recoverNetworkBytesOrder(byte[] orderBytes) {
int sourceNumber = 0;
for (int i = 0; i < 4; i++) {
sourceNumber <<= 8;
sourceNumber |= orderBytes[i] & 0xff;
}
return sourceNumber;
}
// 随机生成16位字符串
String getRandomStr() {
String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
Random random = new Random();
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 16; i++) {
int number = random.nextInt(base.length());
sb.append(base.charAt(number));
}
return sb.toString();
}
/**
* 对明文进行加密.
*
* @param text 需要加密的明文
* @return 加密后base64编码的字符串
* @throws AesException aes加密失败
*/
String encrypt(String randomStr, String text) throws AesException {
ByteGroup byteCollector = new ByteGroup();
byte[] randomStrBytes = randomStr.getBytes(CHARSET);
byte[] textBytes = text.getBytes(CHARSET);
byte[] networkBytesOrder = getNetworkBytesOrder(textBytes.length);
byte[] corpidBytes = corpId.getBytes(CHARSET);
// randomStr + networkBytesOrder + text + corpid
byteCollector.addBytes(randomStrBytes);
byteCollector.addBytes(networkBytesOrder);
byteCollector.addBytes(textBytes);
byteCollector.addBytes(corpidBytes);
// ... + pad: 使用自定义的填充方式对明文进行补位填充
byte[] padBytes = PKCS7Encoder.encode(byteCollector.size());
byteCollector.addBytes(padBytes);
// 获得最终的字节流, 未加密
byte[] unencrypted = byteCollector.toBytes();
try {
// 设置加密模式为AES的CBC模式
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES");
IvParameterSpec iv = new IvParameterSpec(aesKey, 0, 16);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv);
// 加密
byte[] encrypted = cipher.doFinal(unencrypted);
// 使用BASE64对加密后的字符串进行编码
String base64Encrypted = base64.encodeToString(encrypted);
return base64Encrypted;
} catch (Exception e) {
e.printStackTrace();
throw new AesException(AesException.EncryptAESError);
}
}
/**
* 对密文进行解密.
*
* @param text 需要解密的密文
* @return 解密得到的明文
* @throws AesException aes解密失败
*/
String decrypt(String text) throws AesException {
byte[] original;
try {
// 设置解密模式为AES的CBC模式
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
SecretKeySpec key_spec = new SecretKeySpec(aesKey, "AES");
IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(aesKey, 0, 16));
cipher.init(Cipher.DECRYPT_MODE, key_spec, iv);
// 使用BASE64对密文进行解码
byte[] encrypted = Base64.decodeBase64(text);
// 解密
original = cipher.doFinal(encrypted);
} catch (Exception e) {
e.printStackTrace();
throw new AesException(AesException.DecryptAESError);
}
String xmlContent, from_corpid;
try {
// 去除补位字符
byte[] bytes = PKCS7Encoder.decode(original);
// 分离16位随机字符串,网络字节序和corpId
byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20);
int xmlLength = recoverNetworkBytesOrder(networkOrder);
xmlContent = new String(Arrays.copyOfRange(bytes, 20, 20 + xmlLength), CHARSET);
from_corpid = new String(Arrays.copyOfRange(bytes, 20 + xmlLength, bytes.length),
CHARSET);
} catch (Exception e) {
e.printStackTrace();
throw new AesException(AesException.IllegalBuffer);
}
// corpid不相同的情况
if (!from_corpid.equals(corpId)) {
throw new AesException(AesException.ValidateCorpidError);
}
return xmlContent;
}
/**
* 将公众平台回复用户的消息加密打包.
* <ol>
* <li>对要发送的消息进行AES-CBC加密</li>
* <li>生成安全签名</li>
* <li>将消息密文和安全签名打包成xml格式</li>
* </ol>
*
* @param replyMsg 公众平台待回复用户的消息,xml格式的字符串
* @param timeStamp 时间戳,可以自己生成,也可以用URL参数的timestamp
* @param nonce 随机串,可以自己生成,也可以用URL参数的nonce
*
* @return 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串
* @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
*/
public String EncryptMsg(String replyMsg, String timeStamp, String nonce) throws AesException {
// 加密
String encrypt = encrypt(getRandomStr(), replyMsg);
// 生成安全签名
if (timeStamp == "") {
timeStamp = Long.toString(System.currentTimeMillis());
}
String signature = SHA1.getSHA1(token, timeStamp, nonce, encrypt);
// System.out.println("发送给平台的签名是: " + signature[1].toString());
// 生成发送的xml
String result = XMLParse.generate(encrypt, signature, timeStamp, nonce);
return result;
}
/**
* 检验消息的真实性,并且获取解密后的明文.
* <ol>
* <li>利用收到的密文生成安全签名,进行签名验证</li>
* <li>若验证通过,则提取xml中的加密消息</li>
* <li>对消息进行解密</li>
* </ol>
*
* @param msgSignature 签名串,对应URL参数的msg_signature
* @param timeStamp 时间戳,对应URL参数的timestamp
* @param nonce 随机串,对应URL参数的nonce
* @param postData 密文,对应POST请求的数据
*
* @return 解密后的原文
* @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
*/
public String DecryptMsg(String msgSignature, String timeStamp, String nonce, String postData)
throws AesException {
// 密钥,公众账号的app secret
// 提取密文
Object[] encrypt = XMLParse.extract(postData);
// 验证安全签名
String signature = SHA1.getSHA1(token, timeStamp, nonce, encrypt[1].toString());
// 和URL中的签名比较是否相等
// System.out.println("第三方收到URL中的签名:" + msg_sign);
// System.out.println("第三方校验签名:" + signature);
if (!signature.equals(msgSignature)) {
throw new AesException(AesException.ValidateSignatureError);
}
// 解密
String result = decrypt(encrypt[1].toString());
return result;
}
/**
* 验证URL
* @param msgSignature 签名串,对应URL参数的msg_signature
* @param timeStamp 时间戳,对应URL参数的timestamp
* @param nonce 随机串,对应URL参数的nonce
* @param echoStr 随机串,对应URL参数的echostr,在企业微信中是加密过的,需要解密后返回给企业微信官方服务器
*
* @return 解密之后的echostr
* @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
*/
public String VerifyURL(String msgSignature, String timeStamp, String nonce, String echoStr)
throws AesException {
String signature = SHA1.getSHA1(token, timeStamp, nonce, echoStr);
if (!signature.equals(msgSignature)) {
throw new AesException(AesException.ValidateSignatureError);
}
String result = decrypt(echoStr);
return result;
}
/**
* @desc :微信公众号 验证url
*
* @param msgSignature 签名串,对应URL参数的msg_signature
* @param token 公众平台上,开发者设置的token
* @param timeStamp 时间戳,对应URL参数的timestamp
* @param nonce 随机数,对应URL参数的nonce
* @param echoStr 随机串,对应URL参数的echostr,在微信公众号中是明文的,直接原样返回给微信公众平台官方服务器
* @return
* String 验证成功后,原样返回echoStr
* @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
*/
public String verifyUrl_WXGZ(String msgSignature, String token , String timeStamp, String nonce,String echoStr) throws AesException {
//1.进行SHA1加密
String signature = SHA1.getSHA1_WXGZ(token, timeStamp, nonce);
//2.验证 token、timestamp、nonce进行SHA1加密生成的signature 是否与url传过来msgSignature相同
if (!signature.equals(msgSignature)) {
throw new AesException(AesException.ValidateSignatureError);
}
//3.若不抛异常,则url验证成功,原样返回echoStr
String result = echoStr;
return result;
}
}
View Code