一. 开发准备
微信公众号申请---->实名认证---->服务器开发---->绑定服务器
PS: 这里有一点需要注意的就是, 微信开发必须是80端口或者443端口, 如果我们有云服务器主机一切都好办. 但是如果没有我们还有几个备选方案:
1. 花生壳 , net123 : 这两个都需要实名认证(上传省份证的那种), 可以花6块钱买一个永久
2. ngrok : 这个貌似不能用了, 之前用的是这个, 免费的
3. Xtunnel : 刚发现的端口映射, 免费, 操作简单
这篇文章我就使用Xtunnel来进行端口映射
再加一个: holer , 隧道魔法
二. 用Xtunnel进行端口映射
1.编辑映射
注意这里的映射类型选择网站映射, 内网端口设为80端口
确定之后就会有一个外网地址, 如: abc.d.ef.org, 这就是我们后面填写url所需要的地址了.
PS: 可能不太稳定, 今天挂了, 建议多准备几个端口映射工具或者买一个花生壳服务.
2.本地tomcat配置
我们需要把本地的tomcat服务器也配置为80端口. 把server.xml里的端口改为80, 如果不会请自行百度.
我们最好把服务器备份一个, 把用的服务器放在c盘目录下, 把web工程的war包放在webapps目录下, 启动服务器时会自动解压部署的.
具体的web开发我们下面慢慢说.
三. 微信服务器开发
step1 : token验证原理
先说一下token验证流程原理, 自己的资源服务器和微信服务器进行绑定, 提交的url为token验证的接口(servlet), 微信服务器会把echostr发到自己资源的服务器, 然后我们的服务器进行token验证, 看看是否匹配. 匹配之后再将echostr返回给微信服务器, 这样服务器就绑定成功. 同时, 我们自己的资源服务器和微信服务器的消息收发都是通过验证token的那个接口(servlet)来实现的.
绑定流程图:
token验证的具体实现:
import java.io.IOException;
import java.io.PrintWriter;import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;import org.apache.log4j.Logger;/*** 网关控制层* @author later**/
@WebServlet("/TokenServlet") //这就是待会我们访问的接口
public class TokenServlet extends HttpServlet { private static final long serialVersionUID = 1L;private static Logger log = Logger.getLogger("TokenServlet"); /*** 验证信息是否来自微信*/protected void doGet(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 (SignUtil.checkSignature(signature, timestamp, nonce)) {out.print(echostr); //向微信返回echostr}else{log.error("未能通过token验证_TokenServlet");System.out.println("未能通过token验证_TokenServlet");}out.close(); //关闭输出流通道out = null;}/*** 处理微信服务器发来的消息*/protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {request.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8"); // 调用核心业务类接收消息、处理消息 String respMessage = GatewayService.processRequest(request);// 响应消息 PrintWriter out = response.getWriter(); out.print(respMessage); out.close(); }}
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;public class SignUtil {private static String token = "token"; //微信绑定页面填入的token/** * 验证签名 * * @param signature * @param timestamp * @param nonce * @return */ public static boolean checkSignature(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; } /** * 将字节数组转换为十六进制字符串 * * @param byteArray * @return */ private static String byteToStr(byte[] byteArray) { String strDigest = ""; for (int i = 0; i < byteArray.length; i++) { strDigest += byteToHexStr(byteArray[i]); } return strDigest; }/** * 将字节转换为十六进制字符串 * * @param mByte * @return */ 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; } }
step2 : 服务器结构
这里的结构和我的前一篇文章类似 : java web接口开发笔记
1. main包:
main方法1: 获取token管理
AccessTokenManager---->TokenThread---->WeixinUtil>>getAccessToken()
功能: 每3600s获取微信access_token并保存至本地mysql数据库
main方法2: 菜单管理
public static String menu_create_url = "https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN";// 菜单创建(POST) 限100(次/天)
微信创建菜单接口:
- 用获取getAccessToken()方法获取access_token, 保存至数据库, 在从数据库查询access_token写入这个url去创建菜单.
- getMenu() 作为createMenu(getMenu(), at.getToken()); 这个方法的实参具体构建菜单, getMenu()是我们提前写好的菜单.
2. service包(业务层):
这层和下面一层涉及到微信服务器发送给我们的消息, 可供我们直观的看见, 所以我们也应该好好理解一下.
dao : 与数据库的操作,增删改查等方法
model : 一般都是javabean对象,例如与数据库的某个表相关联。
service : 供外部调用,等于对dao,model等进行了包装。
impl : 定义的接口
util : 通常都是工具类,如字符串处理、日期处理等
AccessTokenService.java : 包含两种方法: 获取token, 保存token
TokenService.java : 这是网关业务层 : 用于处理微信发过来的请求
先解析微信发过来的请求request, 解析成我们需要的数据流或者文件流. 这里使用了MessageUtil类, 处理微信发到服务器的消息. 并且新建一个文本消息, 把我们要发给微信的消息转换成xml, 通过servlet的doPost方法发送给微信.
3. utils包:
这里是一些常用的网络方法, 和数据库连接操作类
额(⊙o⊙)…思路乱了, 感觉自己解释不清楚, 继续尝试解释吧. 有错误欢迎交流.
step3 : 本地mysql
给大家看一下我的本地mysql的设计, 这里就很简单不必多说了.
DBHelper.java
其实这个数据库操作类可灵活了, 连接好本地mysql数据库, 写入基本的操作方法, 在写入自己需要的方法, 后面直接拿过来调用方便的很. 大家自行添加就OK啦.
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;/*** JDBC封装* @author later**/
//本地mysql数据库: restful_api 表名: access_token
public class DBHelper {private static final String DRIVENAME = "com.mysql.jdbc.Driver";private static final String URL = "jdbc:mysql://127.0.0.1:3306/restful_api";private static final String USER = "root";private static final String PASSWORD = "root";private Connection conn = null;private Statement st = null;private PreparedStatement ppst = null;private ResultSet rs = null;/*** 加载驱动*/static{try {Class.forName(DRIVENAME).newInstance();} catch (Exception e) {System.out.println("驱动加载失败:"+e.getMessage());}}/*** 连接数据库* @return*/public Connection getConn(){try {conn = DriverManager.getConnection(URL,USER,PASSWORD);} catch (SQLException e) {System.out.println("数据库连接失败:"+e.getMessage());}return conn;}/*** 获取结果集(无参)* @param sql* @return*/private ResultSet getRs(String sql){conn = this.getConn();try { st = conn.createStatement();rs = st.executeQuery(sql);} catch (SQLException e) {System.out.println("查询(无参)出错:"+e.getMessage());}return rs;}/*** 获取结果集* @param sql* @param params* @return*/private ResultSet getRs(String sql,Object[] params){conn = this.getConn();try { ppst = conn.prepareStatement(sql);if(params!=null){for(int i = 0;i<params.length;i++){ppst.setObject(i+1, params[i]);}} rs = ppst.executeQuery();} catch (SQLException e) {System.out.println("查询出错:"+e.getMessage());}return rs;}/*** 查询* @param sql* @param params* @return*/public List<Object> query(String sql,Object[] params){List<Object> list = new ArrayList<Object>();ResultSet rs = null;if(params!=null){rs = getRs(sql, params);}else{rs = getRs(sql);}ResultSetMetaData rsmd = null;int columnCount = 0; try { rsmd = rs.getMetaData(); columnCount = rsmd.getColumnCount(); while(rs.next()){Map<String, Object> map = new HashMap<String, Object>();for(int i = 1;i<=columnCount;i++){map.put(rsmd.getColumnLabel(i), rs.getObject(i)); }list.add(map);}} catch (SQLException e) {System.out.println("结果集解析出错:"+e.getMessage());} finally {closeConn();}return list;}/*** 更新(无参)* @param sql*/public int update(String sql){ int affectedLine = 0;//受影响的行数conn = this.getConn();try { st = conn.createStatement();affectedLine = st.executeUpdate(sql);} catch (SQLException e) {System.out.println("更新(无参)失败:"+e.getMessage());} finally {closeConn();}return affectedLine;}/*** 更新* @param sql* @param params* @return*/public int update(String sql,Object[] params){int affectedLine = 0;//受影响的行数conn = this.getConn();try {ppst = conn.prepareStatement(sql);if(params!=null){for(int i = 0;i<params.length;i++){ppst.setObject(i+1, params[i]);}}affectedLine = ppst.executeUpdate();} catch (SQLException e) {System.out.println("更新失败:"+e.getMessage());} finally {closeConn();}return affectedLine;}private void closeConn(){if(rs!=null){try {rs.close();} catch (SQLException e) {System.out.println(e.getMessage());}}if(st!=null){try {st.close();} catch (SQLException e) {System.out.println(e.getMessage());}}if(ppst!=null){try {ppst.close();} catch (SQLException e) {System.out.println(e.getMessage());}}if(conn!=null){try {conn.close();} catch (SQLException e) {System.out.println(e.getMessage());}}}}
四. 实验结果
测试需要在微信公众号网页里启用服务器, 然后根据我们服务器具体的实现一层一层的测试.
下图是第一次测试的结果, 我们可以看见还要很多的功能并未具体实现. 以及未到达预期的一些效果, 这时候我们就该回去继续修改我们服务器端的代码, 并继续测试直至实现自己想要的功能.
测试不全, 服务器开发完善后继续测试.