最近在作一个微信预定洗车的项目,其中有个功能是预定完成后给用户发一个模板消息,发送模板消息须要AccessToken以及json格式的消息内容,接口以下。javascript
发送模板消息 html
接口调用请求说明java
http请求方式: POST https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=ACCESS_TOKENPOST数听说明web
POST数据示例以下:redis
{ "touser":"OPENID", "template_id":"ngqIpbwh8bUfcSsECmogfXcV14J0tQlEpBO27izEYtY", "url":"http://weixin.qq.com/download", "data":{ "first": { "value":"恭喜你购买成功!", "color":"#173177" }, "keynote1":{ "value":"巧克力", "color":"#173177" }, "keynote2": { "value":"39.8元", "color":"#173177" }, "keynote3": { "value":"2014年9月22日", "color":"#173177" }, "remark":{ "value":"欢迎再次购买!", "color":"#173177" } } }返回码说明json
在调用模板消息接口后,会返回JSON数据包。正常时的返回JSON数据包示例:segmentfault
{ "errcode":0, "errmsg":"ok", "msgid":200228332 }
我而同事已经写过这个功能了,索性就直接拿来用了。可是在使用的过程当中,发现第一次能够成功发送模板消息,第二次就返回 errcode 40001,token验证失败。api
关于微信AccessToken的介绍:安全
access_token是公众号的全局惟一票据,公众号调用各接口时都需使用access_token。开发者须要进行妥善保存。access_token的存储至少要保留512个字符空间。access_token的有效期目前为2个小时,需定时刷新,重复获取将致使上次获取的access_token失效。(注:获取access_token接口的每日调用限额为2000次)服务器
初步怀疑是否是别的地方更新了AccessToken,因而我打开他的代码,以下(伪代码):
public String getAccessToken(){ String token = (String)request.getSession().get(Const.ACCESS_TOKEN); if(token 为空){ toekn = getTokenFormWx(); request.getSession().add(Const.ACCESS_TOKEN,token). return token; } return token; }
这样写看起来好像没什么问题,也不是每次都去获取一个新的access_token。但他忽略了一点,session并非只有一份的,系统为每一个会话都建立一个单独的session,最后调用getAccessToken的会话让其余会话的session中的access_token都失效了。
我决定动手把代码修改了一下,由于access_token的有效时间是7200秒,当时想着也放在redis里面好了,能够利用redis的自动过时来保证access_token的有效性,可是项目中没有使用redis,加进来也是大材小用了,最后想一想仍是放在了ServletContext里面。
ServletContext,是一个全局的储存信息的空间,服务器开始,其就存在,服务器关闭,其才释放。request,一个用户可有多个;session,一个用户一个;而servletContext,全部用户共用一个。因此,为了节省空间,提升效率,ServletContext中,要放必须的、重要的、全部用户须要共享的线程又是安全的一些信息。
因而就有了下面这段代码(伪)
public String getAccessToken(){ Map<String,Object> cacheMap = request.getServletContext().getAttr(Const.WX_TOKEN_MAP); if(cacheMap==null || System.currentTimeMillis()-(Date)cacheMap.get(Const.WX_TOKEN_TIME).getTime()>1000*7000){ cacheMap = new HashMap<>(); String token = getTokenFormWx(); if(token 为空){ throw new RuntimeException("AccessToken is null"); } cacheMap.put(Const.WX_TOKEN_VAL,token); cacheMap.put(Const.WX_TOKEN_TIME,new Date()); } return (String)cacheMap.get(Const.WX_TOKEN_VAL); }
这样看起来好像是比以前的代码好了一点,不会为没一个会话都建立一个access_token,并且保证了时效性。但其实仍是存在一点问题的,假若有两个线程同时调用了这一个方法,其中第一个线程进了if在调用getTokenFormWx()的时候由于网络或者其余缘由等在这里了,第二个线程来了仍是进了if,而且成功的调用getTokenFormWx()返回了token给调用者处理业务逻辑,这时候第一个线程执行完毕,刷新了token,这样就致使了第二个线程的token已经失效,在处理业务逻辑的时候必然失败。
咱们有没有办法避免这个问题呢?固然是有的。
你想我直接使用synchronized好了,加在方法上,这样就不会错了。因而方法就变成了这样
public synchronized String getAccessToken(){ Map<String,Object> cacheMap = request.getServletContext().getAttr(Const.WX_TOKEN_MAP); if(cacheMap==null || System.currentTimeMillis()-(Date)cacheMap.get(Const.WX_TOKEN_TIME).getTime()>1000*4800){ cacheMap = new HashMap<>(); String token = getTokenFormWx(); if(token 为空){ throw new RuntimeException("AccessToken is null"); } cacheMap.put(Const.WX_TOKEN_VAL,token); cacheMap.put(Const.WX_TOKEN_TIME,new Date()); } return (String)cacheMap.get(Const.WX_TOKEN_VAL); }
这样是能解决问题,可是解决问题代价也太大了,每个线程想要获取这个token就得等其余线程所有获取完才能拿到,大大下降了效率,不可行的。因此再次改动代码,变成了下面这样。
public String getAccessToken(){ Map<String,Object> cacheMap = request.getServletContext().getAttr(Const.WX_TOKEN_MAP); if(cacheMap==null || System.currentTimeMillis()-(Date)cacheMap.get(Const.WX_TOKEN_TIME).getTime()>1000*4800){ synchronized(this){ if(cacheMap==null || System.currentTimeMillis()-(Date)cacheMap.get(Const.WX_TOKEN_TIME).getTime()>1000*4800){ cacheMap = new HashMap<>(); String token = getTokenFormWx(); if(token 为空){ throw new RuntimeException("AccessToken is null"); } cacheMap.put(Const.WX_TOKEN_VAL,token); cacheMap.put(Const.WX_TOKEN_TIME,new Date()); } } } return (String)cacheMap.get(Const.WX_TOKEN_VAL); }
当第一个线程进了if以后,执行synchronized里面的代码,等待在了getTokenFormWx(),第二个线程也进了if,但因为加了synchronized,因此会等待在那里,等第一个线程处理完它才能执行,第一个线程执行完毕以后返回token去执行业务逻辑,第二个线程进入synchronized代码块,执行这里面的if判断,因为第一个线程已经成功获取token而且刷新了ServletContext中的cacheMap,条件已经不知足,因此第二个线程是没法执行这个if里面的代码了,到此咱们就设计了一个线程安全的获取access_token方案。
看样子好像一切都ok了,可是在测试后仍是会出现同样的问题。
我又仔细检查了两遍代码,仍是没有发现有问题的地方。找不到错误的地方,我决定开始试错。
第一次,我把https://api.weixin.qq.com/cgi...改为https://api.weixin.qq.com/cgi...
参数access_token放入post请求参数里面,其余参数放进request body里面。
结果:第一次就返回了40001 access_token无效。
第二次,我把https://api.weixin.qq.com/cgi...改为https://api.weixin.qq.com/cgi...
参数access_token放入post请求参数里面并使用trim()去除空格,其余参数放进request body里面。
结果:第一次就返回了40001 access_token无效。
第三次,我把https://api.weixin.qq.com/cgi...
其余参数放进request body里面。
结果:一切ok。。。。
为何会多了空格?我也很想知道,但因为调试了过久时间,已经很晚了,而次日就是假期,因此我也就没有深究了。
那为何第二次和第三次都对ACCESS_TOKEN进行了去空格处理,为何返回的结果却不同呢?
这就得不得不说一下Http协议了,但这里不须要讲太多,因此咱们只说一下Http协议之请求消息Request。
客户端发送一个HTTP请求到服务器的请求消息包括如下格式:
请求行(request line)、请求头部(header)、空行和请求数据四个部分组成。
图片描述
Get请求例子(java按得票排序)
GET https://segmentfault.com/t/java?type=votes HTTP/1.1 Host: segmentfault.com Connection: keep-alive Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.104 Safari/537.36 Core/1.53.2372.400 QQBrowser/9.5.10548.400 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,/;q=0.8 Referer: https://segmentfault.com/t/java Accept-Encoding: gzip, deflate, sdch, br Accept-Language: zh-CN,zh;q=0.8 Cookie: 这个我就不贴出来了
Post请求例子(添加笔记)
POST https://segmentfault.com/api/notes/add?_=6e0a1202503bc4d86e63672cff567b81 HTTP/1.1 Host: segmentfault.com Connection: keep-alive Content-Length: 139 Accept: application/json, text/javascript, /; q=0.01 Origin: https://segmentfault.com X-Requested-With: XMLHttpRequest User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.104 Safari/537.36 Core/1.53.2372.400 QQBrowser/9.5.10548.400 Content-Type: application/x-www-form-urlencoded; charset=UTF-8 Referer: https://segmentfault.com/record Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.8 Cookie: 这个真的不能贴 title=%E6%B5%8B%E8%AF%95%E7%AC%94%E8%AE%B0&text=%E6%B5%8B%E8%AF%95%E7%AC%94%E8%AE%B0&id=&draftId=1220000008931250&isPrivate=0&language=text
对比一下你发现了什么?
get请求参数在url后面,使用?看成标志,多个参数使用&分割 相似?a=1&b=2
post参数在请求头部空一行的后面 相似 a=1&b=2
那post提交的json串在哪一个位置呢?
其实你已经知道啦,也是在请求头部空一行的后面 不过是以json的格式,而服务器内部使用&分割参数,使得开发者可使用getParameter获取提交的参数,而其余类型的参数(例如json串和xml)开发者可使用getInputStream来读取到参数而后本身解析。
那post请求可否把参数写在url后面呢?就像 post?a=1&b=2
答案是能够的,服务器能够成功解析到。
那get请求能把参数写在request body里面吗?
答案是否认的,服务器对get请求只解析url后面的,request body里面的他不关心。
那你发送模板消息的参数为何写在request body里面就不行呢?
我也不知道微信内部是怎么作的,可是我以为吧,微信之因此要把access_token写在url后面,由于这个接口request body里面是模板消息的json串 若是再把access_token加进去 数据大概会是这样
access_toke=xxxxxxxxxxx {"touser":"OPENID","template_id":"ngqIpbwh8bUfcSsECmogfXcV14J0tQlEpBO27izEYtY", "url":"http://weixin.qq.com/download", ... }
微信方面也很差分割这个串,因而他们以为要这个access_token写在url后面,他们获取到url后再手动分割处理,request body里面就只放纯json串,解析起来也很方便。这就是为何我第二次操做失败的缘由啦。
第一次写技术类得文章,文笔很差多多见谅。