前言: 现在大多数网站项目都支持微信登录,付款,以及支付宝登录付款,这种方式也是能够让用户很快速便捷的注册本网站的账号,进行登录,以及后续的操作。相信小伙伴们看完之后,会对怎么与微信或者支付宝服务器打交道有很深的理解,就当做是一个敲门砖吧。那么本篇主要针对微信的验证登录来打开通往微信服务器的大门,下一篇会主要讲解一下支付宝付款验证对接。
本篇为原创,转载请标出处:http://www.cnblogs.com/gudu1/p/8087130.html
下面我会使用大量的代码,代码中添加完整的注释来解释与微信服务器对接的详细步骤:
首先呢,需要有一个微信公众号,当然是收费的,不过呢,微信给我们开发人员提供的有微信公众测试号来进行开发测试,虽然提供功能不是很全,开发测试是够用了,还是很到位的
微信测试号地址:https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login
扫码登陆后呢,就是下面这个样子了:
上面这个appID 和 appsecret 很重要,正常的公众号appsecret 一定不要暴露出去,不然是很危险的。
>> URL: 规则 http://服务器地址或者域名/项目验证接口路由,比如: http://www.mmm.cn/testProject/weChat.do,这个weChat.do 就是要跟微信服务器验证对接的接口
>> Token:这个是由用户自己来定义,主要是我们的项目跟微信服务器对接时候需要进行SHA1加密和解密的字段之一,详细请看官方文档:https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421135319
>> 域名: 这里就需要使用你自己的域名了,当然域名是要指向能够被外网访问到的服务器地址。
当我们在接口配置信息这里点击提交,微信服务器就会向我们的服务器接口发送消息来进行验证是否正确。
当然还需要修改一处,这里需要的还是服务器域名:
接下来呢,就要看我们的程序跟微信验证的代码流程:
这里呢,我使用的 Spring 来做:
import java.io.IOException; import java.io.PrintWriter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import com.util.wechat.SignUtil; @Controller @RequestMapping("wechat") public class WechatController { private static Logger log = LoggerFactory.getLogger(WechatController.class); @RequestMapping(method = { RequestMethod.GET }) public void doGet(HttpServletRequest request, HttpServletResponse response) { log.debug("weixin get..."); // 微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数。 String signature = request.getParameter("signature"); // 时间戳 String timestamp = request.getParameter("timestamp"); // 随机数 String nonce = request.getParameter("nonce"); // 随机字符串 String echostr = request.getParameter("echostr"); // 通过检验signature对请求进行校验,若校验成功则原样返回echostr,表示接入成功,否则接入失败 PrintWriter out = null; try { out = response.getWriter(); if (SignUtil.checkSignature(signature, timestamp, nonce)) { log.debug("weixin get success...."); // 我们验证通过,确定是微信发过来的消息,就将 随机字符串原样返回给微信 out.print(echostr); } } catch (IOException e) { e.printStackTrace(); } finally { if (out != null) out.close(); } } }
微信发过来的消息会到这里,可以看到我们的Controller的RequestMappiing 路由为 "wechat", 这就是我们在微信测试号中配置的接口路由地址,很明显,这是一个Servlet ,请求的是doGet 方法。
上面代码只是我们的入口,主要验证逻辑在下面:
import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; /** * 微信请求校验工具类 */ public class SignUtil { // 与接口配置信息中的Token要一致 private static String token = "myo2o"; /** * 验证签名 * * @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 { // 获得SHA-1 加密的对象 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; } }
细心看的小伙伴肯定已经注意到了, 在方法上面有一个字段就是token,这里一定要跟微信测试号中配置的保持 一致,不相信的同学可以测试着改一个不一致的,保证过不了。
这里的流程就是,微信首先 把我们的 token+时间戳+随机数 进行一次SHA-1 加密,然后发送给我们的程序,然后我们再进行一次SHA-1加密,然后跟微信发送过来的加密数据进行匹配。我们想象一下,有人故意破坏我们的程序,然后模拟发送数据,能吗?答案是能,不过他首先得拿到我们的token字段,才能使我们的程序给出正确响应,所以这里的token 配置一定要和微信测试号中配置保持一致。
好了,既然极影和微信服务器连通了,那么就开始我们的业务需求登录操作吧。
用户使用微信登录,肯定是需要向微信服务器发送登录请求,然后微信再回调我们的接口,接口获取微信服务器传递过来的信息,确定用户是否登录成功,包括用户的一些数据,比如:昵称,头像地址 等等。
业务流程:
第一步:请求CODE
首先要知道这些接口是谁定义的,肯定是微信定义的了,那我们怎么知道向哪个接口发送消息,这就需要查看微信给我们提供的官方文档了。
https://open.weixin.qq.com/connect/qrconnect?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect , 该地址是程序向微信发送的第一条请求,请求参数中携带code
下面来分析一下这条URL:
appid:应用唯一标识,也就是微信测试号中的第一条 appid
redirect_uri:使用urlEncode对链接进行处理,也就是微信服务器回调地址,地址就指向我们程序中处理用户是否登录的接口
response_type: 就填写 code
scope:应用授权作用域,拥有多个作用域用逗号(,)分隔,网页应用目前仅填写snsapi_login即可
state:用于保持请求和回调的状态,授权请求后原样带回给第三方。该参数可用于防止csrf攻击(跨站请求伪造攻击),建议第三方带上该参数,可设置为简单的随机数加session进行校验
接下来就贴一下我的程序代码帮助理解:
import java.io.IOException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod;import com.util.wechat.WechatUtil; /** * 获取关注公众号之后的微信用户信息的接口,如果在微信浏览器里访问 * https://open.weixin.qq.com/connect/oauth2/authorize?appid=wxd7f6c5b8899fba83&redirect_uri=http://o2o.yitiaojieinfo.com/o2o/wechatlogin/logincheck&response_type=code&scope=snsapi_userinfo&state=1#wechat_redirect * 则这里将会获取到code,之后再可以通过code获取到access_token 进而获取到用户信息 * @author xiangze * */ @Controller @RequestMapping("wechatlogin") public class WechatLoginController { private static Logger log = LoggerFactory.getLogger(WechatLoginController.class); private static final String FRONTEND = "1"; private static final String SHOPEND = "2"; @Autowired private PersonInfoService personInfoService; @Autowired private WechatAuthService wechatAuthService; @RequestMapping(value = "/logincheck", method = { RequestMethod.GET }) public String doGet(HttpServletRequest request, HttpServletResponse response) { log.debug("weixin login get..."); // 获取微信公众号传输过来的code,通过code可获取access_token,进而获取用户信息 String code = request.getParameter("code"); // 这个state可以用来传我们自定义的信息,方便程序调用,这里也可以不用 String roleType = request.getParameter("state"); log.debug("weixin login code:" + code); WechatUser user = null; String openId = null; WechatAuth auth = null; if (null != code) { UserAccessToken token; try { // 通过code获取access_token token = WechatUtil.getUserAccessToken(code); log.debug("weixin login token:" + token.toString()); // 通过token获取accessToken String accessToken = token.getAccessToken(); // 通过token获取openId openId = token.getOpenId(); // 通过access_token和openId获取用户昵称等信息 user = WechatUtil.getUserInfo(accessToken, openId); log.debug("weixin login user:" + user.toString()); request.getSession().setAttribute("openId", openId); auth = wechatAuthService.getWechatAuthByOpenId(openId); } catch (IOException e) { log.error("error in getUserAccessToken or getUserInfo or findByOpenId: " + e.toString()); e.printStackTrace(); } } // 若微信帐号为空则需要注册微信帐号,同时注册用户信息 if (auth == null) { PersonInfo personInfo = WechatUtil.getPersonInfoFromRequest(user); auth = new WechatAuth(); auth.setOpenId(openId); if (FRONTEND.equals(roleType)) { personInfo.setUserType(1); } else { personInfo.setUserType(2); } auth.setPersonInfo(personInfo); WechatAuthExecution we = wechatAuthService.register(auth); if (we.getState() != WechatAuthStateEnum.SUCCESS.getState()) { return null; } else { personInfo = personInfoService.getPersonInfoById(auth.getPersonInfo().getUserId()); request.getSession().setAttribute("user", personInfo); } } else { request.getSession().setAttribute("user", auth.getPersonInfo()); } // 若用户点击的是前端展示系统按钮则进入前端展示系统 if (FRONTEND.equals(roleType)) { return "frontend/index"; } else { return "shop/shoplist"; } } }
第二步:通过code获取用户access_token
调用接口:https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code
代码:
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.ConnectException; import java.net.URL; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper;/** * 微信工具类 * * @author xiangze * */ public class WechatUtil { private static Logger log = LoggerFactory.getLogger(WechatUtil.class); /** * 获取UserAccessToken实体类 * * @param code * @return * @throws IOException */ public static UserAccessToken getUserAccessToken(String code) throws IOException { // 测试号信息里的appId String appId = "你的微信测试号appid"; log.debug("appId:" + appId); // 测试号信息里的appsecret String appsecret = "你的微信测试号appsecret"; log.debug("secret:" + appsecret); // 根据传入的code,拼接出访问微信定义好的接口的URL String url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=" + appId + "&secret=" + appsecret + "&code=" + code + "&grant_type=authorization_code"; // 向相应URL发送请求获取token json字符串 String tokenStr = httpsRequest(url, "GET", null); log.debug("userAccessToken:" + tokenStr); UserAccessToken token = new UserAccessToken(); ObjectMapper objectMapper = new ObjectMapper(); try { // 将json字符串转换成相应对象 token = objectMapper.readValue(tokenStr, UserAccessToken.class); } catch (JsonParseException e) { log.error("获取用户accessToken失败: " + e.getMessage()); e.printStackTrace(); } catch (JsonMappingException e) { log.error("获取用户accessToken失败: " + e.getMessage()); e.printStackTrace(); } catch (IOException e) { log.error("获取用户accessToken失败: " + e.getMessage()); e.printStackTrace(); } if (token == null) { log.error("获取用户accessToken失败。"); return null; } return token; } /** * 发起https请求并获取结果 * * @param requestUrl * 请求地址 * @param requestMethod * 请求方式(GET、POST) * @param outputStr * 提交的数据 * @return json字符串 */ public static String httpsRequest(String requestUrl, String requestMethod, String outputStr) { StringBuffer buffer = new StringBuffer(); try { // 创建SSLContext对象,并使用我们指定的信任管理器初始化 TrustManager[] tm = { new MyX509TrustManager() }; SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE"); sslContext.init(null, tm, new java.security.SecureRandom()); // 从上述SSLContext对象中得到SSLSocketFactory对象 SSLSocketFactory ssf = sslContext.getSocketFactory(); URL url = new URL(requestUrl); HttpsURLConnection httpUrlConn = (HttpsURLConnection) url.openConnection(); httpUrlConn.setSSLSocketFactory(ssf); httpUrlConn.setDoOutput(true); httpUrlConn.setDoInput(true); httpUrlConn.setUseCaches(false); // 设置请求方式(GET/POST) httpUrlConn.setRequestMethod(requestMethod); if ("GET".equalsIgnoreCase(requestMethod)) httpUrlConn.connect(); // 当有数据需要提交时 if (null != outputStr) { OutputStream outputStream = httpUrlConn.getOutputStream(); // 注意编码格式,防止中文乱码 outputStream.write(outputStr.getBytes("UTF-8")); outputStream.close(); } // 将返回的输入流转换成字符串 InputStream inputStream = httpUrlConn.getInputStream(); InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8"); BufferedReader bufferedReader = new BufferedReader(inputStreamReader); String str = null; while ((str = bufferedReader.readLine()) != null) { buffer.append(str); } bufferedReader.close(); inputStreamReader.close(); // 释放资源 inputStream.close(); inputStream = null; httpUrlConn.disconnect(); log.debug("https buffer:" + buffer.toString()); } catch (ConnectException ce) { log.error("Weixin server connection timed out."); } catch (Exception e) { log.error("https request error:{}", e); } return buffer.toString(); } }
因为我们发送的是https请求,所以还需要有这么一个工具方法,发起 https 请求。。。
OK,以上就是微信验证以及登录的全过程,可能并不是很详细,不过理解这个过程应该是可以了,那么下篇会讲解一下支付宝付款的流程以及代码。