一、准備階段
需要准備事項:
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); } } }
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; } }
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; //釋放資源 } } }
四、代碼實現
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"; }
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; } }
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; } }
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(); } }
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); } } }
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; } }