资讯专栏INFORMATION COLUMN

一个AccessToken引发的思考

rainyang / 2848人阅读

摘要:最近在做一个微信预约洗车的项目,其中有个功能是预约完成后给用户发一个模板消息,发送模板消息需要以及格式的消息内容,接口如下。关于微信的介绍是公众号的全局唯一票据,公众号调用各接口时都需使用。

最近在做一个微信预约洗车的项目,其中有个功能是预约完成后给用户发一个模板消息,发送模板消息需要AccessToken以及json格式的消息内容,接口如下。

发送模板消息

接口调用请求说明

http请求方式: POST
 
https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=ACCESS_TOKEN

POST数据说明

POST数据示例如下:

  {
       "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数据包示例:

{
       "errcode":0,
       "errmsg":"ok",
       "msgid":200228332
   }

我而同事已经写过这个功能了,索性就直接拿来用了。但是在使用的过程中,发现第一次可以成功发送模板消息,第二次就返回 errcode 40001,token验证失败。

关于微信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 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 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 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串,解析起来也很方便。这就是为什么我第二次操作失败的原因啦。

第一次写技术类得文章,文笔不好多多见谅。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/66936.html

相关文章

  • XXL-CONF v1.6.0 发布,分布式配置管理平台。废弃ZK轻量级架构升级

    摘要:访问令牌为提升系统安全性,配置中心和客户端进行安全性校验,双方匹配才允许通讯启动时,优先全量加载镜像数据到层,避免逐个请求耗时简介是一个轻量级分布式配置管理平台,拥有轻量级秒级动态推送多环境多语言配置监听权限控制版本回滚等特性。 Release Notes 1、轻量级改造:废弃ZK,改为 DB + 磁盘 + long polling 方案,部署更轻量,学习更简单;集群部署更方便,与单...

    Pandaaa 评论0 收藏0
  • Vue.js 2.0 基于OAuth2.0第三方登录组件

    摘要:第三方登录是现在常见的登录方式,免注册且安全方便快捷。大部分的第三方登录都参考了的认证方法。这里我主要总结一下第三方登录组件的设计流程。身份认证组件,需解耦,至少要唤起登录和登出事件。认证成功唤起登录事件并将用户信息传递出去。 第三方登录是现在常见的登录方式,免注册且安全方便快捷。 本篇文章将以Github为例,介绍如何在自己的站点添加第三方登录模块。 OAuth2.0 OAuth(开...

    RancherLabs 评论0 收藏0
  • 基于vuenuxt框架cnode社区服务端渲染

    摘要:基于的框架仿的社区服务端渲染,主要是为了优化以及首屏加载速度线上地址地址技术栈目录结构配置文件封装工具函数滚动条操作函数静态资源实例化之前执行的插件注册全局组件注册全局服务端渲染时保存供服务端请求时的获取页面级组件首页登录页未读消 nuxt-cnode 基于vue的nuxt框架仿的cnode社区服务端渲染,主要是为了seo优化以及首屏加载速度 线上地址 http://nuxt-cnod...

    tainzhi 评论0 收藏0
  • Spring Cloud OAuth 微服务内部Token传递源码实现解析

    摘要:源码非常简单谈谈实现的问题当请求上线文没有如果调用会直接,这个肯定会报错,因为上下文失败如果设置线程隔离,这里也会报错。导致安全上下问题传递不到子线程中。欢迎关注我们获得更多的好玩实践 背景分析 showImg(https://segmentfault.com/img/remote/1460000018899024?w=494&h=245); 1.客户端携带认证中心发放的token,...

    Michael_Ding 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<