前(单页面)后端完全分离的OAuth2授权和分享

1,前言
     OAuth2授权已然是互联网开放平台的统一标配,本文不在于赘述已知常见的OAuth2授权,力求通过在微信平台中的实例来阐述,针对对前后端完全分离并且前端是单页面应用的OAuth2授权和分享的一种通用实现方案。本文余下组织如下:第2部分先简要阐述一下OAuth2的授权流程;第3部分说明前后端完全分离和单页面应用会引入的问题;第4部分分析OAuth2授权实现;第5部分则详细地说明前端为单页面应用且前后端完全分离的页面分享;第6部分总结。
 
2, OAuth2授权流程
     在详细描述具体实现前,先简要解释一下OAuth2授权流程。从图1可以看到整个授权流程包括6个来回(在大部分实际应用中,包含7给来回,也即是授权过程发生在用户刚开始进入网站时就发起OAuth2授权,因此在获取完授权信息后,还需要恢复用户访问页面),中间流程的响应操作都是以重定向的方式返回给客户端授权,因此整个流程实际是一步到位的。
图 1
3,前(单页面)后端完全分离引入的问题
  
图 2
     从图2可以看到,对于前后端完全分离的架构,静态资源的请求全部都是Nginx定位直接返回,而对于业务数据则全部是通过Ajax请求来获取的。然而由于类Ajax请求的跨域问题,图1中OAuth2授权流程中的第1步请求须是由微信浏览器发出的request。此外,对于页面分享的处理,由于前端使用的是单页面架构,这意味着微信浏览器客户端,只需要一次.html的页面请求,后续的页面调整全部在客户端内完成。这些给后续做页面分享的引入了另两个问题,那就是因为单页面应用,导致客户端后续的页面调整,后台无法感知,也就不能对分享页面进行微信签名;即便签名成功,前台通过Ajax来获取的签名信息也无法起作用。后续,针对前后端完全分离OAuth2授权和前端单页面应用的页面分享,这两个问题的处理做完整的阐述。
 
4, 前后端完全分离OAuth2授权
     在第3部分中已经提到由于跨域问题类Ajax request无法完成OAuth2授权,只有通过Browser request才能完成授权。因此必须修改反向代理服务器的配置(Nginx),将指定url请求重定向到Web Server的服务器中,如图3所示。下面分Nginx和WebServer来详细说明。
图 3
4.1 Nginx的配置
     如图3所示,Nginx的配置,需作如下修改:
原来的nginx.conf
location /webserver/ {
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_buffering off;
        proxy_pass http://127.0.0.1:8080;
}
location / {
        try_files $uri $uri/ @router;
        index index.html;
}
修改后nginx.conf
location /webserver/ {
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_buffering off;
        proxy_pass http://127.0.0.1:8080;
}
location ^~  /homepage {
        try_files $uri $uri/ @router;
        index index.html;    
}
location ~* /(dist|src)/ {
        try_files $uri $uri/;
}
location / {
        if ($http_user_agent ~* ^.*micromessenger.*$){
            return 302 /webserver/wechat$uri;
        }
        try_files $uri $uri/ @router;
        index index.html;
}
     从修改前的nginx.conf来看,对于前后端完全分离,请求分两个分支,分别是静态资源和业务数据。正如前文提到的,业务数据的请求是通过类Ajax发出,所以为了可以正常通过OAuth2授权,需要将指定url请求做302处理。即如修改后的nginx.conf可知,除了/(dist|src)下的js和图片等,以及、/homepage打头的url页面外,所有的微信发出的请求都会302为/webserver/wechat$uri,如此就将请求转到webserver。特别地需要说明,/homepage打头url出现的意义。由于对于微信浏览器,除了/homepage的页面请求,全部被重定向至后台,一方面,为了保持前后端完全分离的纯粹性,避免后台冗余保存一份前端静态页面;另一方面,在实际微信授权过程中,是用户初始点击页面进入网站的时候就开始触发一系列的OAuth2授权流程,当授权完成以后,自动恢复用户访问页面。因此,webserver无法通过内部的dispatcher来定位到真实的静态页面,只能通过redirect的方式将先前缓存在session中的/url重定向到真正的静态页面文件处,也即是/homepage/url。
 4.2 WebServer实现
     本文使用的后台实现是Java Web应用。在4.1部分已说明,已将需要授权的页面访问请求转到webserver,则需在后台的request interceptor中加入如下代码。
RequestInterceptor.java
public class RequestInterceptor extends HandlerInterceptorAdapter {
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
            Object handler) throws Exception {
        if(WechatAuthorizeManager.isWechatRequest(request)) {
            return WechatAuthorizeManager.wechatDispatcher(request, response);
        }
        return otherDispatcher(request, response)
    }
}
     从RequestInterceptor.java中可以看到,对于微信授权(包括后面的分享)服务,定义了一个完全静态的私有类WechatAuthorizeManager,该类中只定义了常量和静态方法,执行的都是线程安全的操作。在这里,特别地,针对微信端发出的请求做了单独的dispatch处理。下面详细看一下isWechatRequest和wechatDispatcher在WechatAuthorizeManager中的定义。
WechatAuthorizeManager.java
/**
* 从微信发往后台的http请求分两类:
* 1, 微信浏览器自身发出的请求(不存在跨域问题), 该类请求可以理解为静态资源请求,
*    包括.html, .js, .css和图片等资源请求+;
* 2, 类ajax请求(存在跨域问题).
* 如果是微信浏览器发出的请求,要么经过反向代理将重定向过来的静态资源的请求url上加入{@param WechatContextPath},
* 要么对html文件中的静态url加入{@param WechatContextPath}.
*/
public static boolean isWechatRequest(HttpServletRequest request){
    return request.getRequestURI().contains(WechatContextPath) &&
            request.getHeader(userAgent).toLowerCase().contains(WechatBrowerFlag);
}
 
/**
* 在完全的前后端分离应用里, 静态资源通常不会由servlet来返回,而是直接由反向代理服务器直接返回;
* 然而,在特殊情况下,如微信分享的前端页面signature文件需要后台代为生成,以.js文件的形式返回给前端,
* 以及OAuth授权访问等,需要后台服务依据前端的页面请求完成OAuth和页面signature。
* 因此,为了让后台感知到浏览器发出了页面请求(而不仅是ajax发出的数据请求),
* 反向代理服务器应当将某些静态资源请求加入{@param WechatContextPath}路径后,转发至后台服务器;
* 而对于完全前后端分离的单页面应用,用户在做页面切换时,前端的页面路由时,应当主动调起浏览器刷新当前页面,
* 使得后台能感知到页面跳转的动作。
* 此外,只有.js或html页面可能会进入servlet请求:
* 1, 对于html页面的请求,用于OAuth授权访问;
* 2, 对于.js的请求, 用于在前后端完全分离的场景下,用于返回signature等。
* 对于每一个微信客户端发出的页面请求,在当前session中缓存当前请求的页面路径,为后续处理提供依据:
* 1,OAuth获取微信授权后,页面恢复;
* 2,请求signature等由后台动态生成的静态资源文件时,识别宿主页面。
*/
public static boolean wechatDispatcher(HttpServletRequest request, HttpServletResponse response)
        throws Exception {
    String url = request.getRequestURI();
    //微信服务器通过微信浏览器重定向过来并携带auth code的url, 不需要拦截
    if(url.contains(WechatAuthCodeUrl))
        return true;
    if(!url.endsWith(".js")) {
        //既然是html页面请求, 则需要在session中保存当前请求的页面相对路径,
        //一方面可以,在用于OAuth微信授权后,页面重定向恢复;
        //另一方面,可以用于定位后续.js等静态资源文件所归属的宿主页面。
        pushRelativeUrl(request);
    }
    if(!OAuth.existsOpenid(request, response)) {
        //完成OAuth授权动作
        return false;
    }
    //signature js文件
    String wechatJsFilename = PageSignature.WechatJsFilename(request, PageSignature.WechatJs);
    if(url.endsWith(wechatJsFilename) || url.endsWith(PageSignature.WechatJs)) {
        return locateSignatureJs(request, response);
    }
    redirectRequest(request, response);
    return false;
}
     从上图中的代码定义可以看到,只有是通过反向代理重定向过来且微信标识的请求才是需要wechatDispatcher处理的请求。对于wechatDispatcher方法的实现,由于需要对各类可能情形做处理,所以涉及多个if判断。为了更好围绕本部分主题来阐述,把不相关的代码先解释出去。从代码倒数7行开始看,可知该wechatDispatcher不仅适用于OAuth2授权,还有页面分享,这部分在页面分享(第5)部分详细解释,并且在最后两行我看到如果先前的if判断未返回则将统一重定向回客户端。重定向方法定义如下:
/**
* {@link WechatLoginController#wechatCode}
* 获取到微信授权凭证后,会调起该方法。
* 获取微信授权凭证后,在session中获取先前存入的url,并告知微信浏览器重定向到正确的访问页面
* @param request
* @param response
*/
public static void redirectRequest(HttpServletRequest request, HttpServletResponse response){
    String requestUrl = RequestRelativeUrl(request);try {
        response.sendRedirect(Homepage+requestUrl);
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}
从上图代码可以看到,请求都被重定向至Homepage打头的url,实际即是4.1部分中提到的/homepage打头的请求配置。
     下面针对OAuth2授权,对逐个if解释。首先讲解第一个if判断,对于图1中描述的OAuth2授权流程中的第5个流程所请求的WechatAuthCodeUrl直接返回true,依赖内置框架定位WechatAuthCodeUrl对应方法实现,如下所示:
@RequestMapping(WechatAuthorizeManager.WechatAuthCodeUrl)
@ResponseBody
public String wechatCode(String code, HttpServletRequest request, HttpServletResponse response){
  try {
      WechatAuthorizeManager.requestWechat(request,code);
      WechatAuthorizeManager.redirectRequest(request, response);
      return null;
  } catch (Exception e) {
      WebResult result = WebResult.failureResult(e.getMessage());
      return result.toJson();
  }
}
从代码中可以看到wechatCode方法实际是完成了第2个部分提到的第5至7的步骤。然后是第2个if判断,对于不是以.js结尾的请求,都将当前相对url存在session中。原则上,前后端完全分离的架构中,后端服务是不会接收.js资源的请求的,但后文页面分享会提到这样处理的原因。而将页面的相对url存入session的目的,一方面可以,在用于OAuth微信授权后,页面重定向恢复;另一方面,可以用于(页面分享时)定位后续.js等静态资源文件所归属的宿主页面。最后是第3个if判断,看到是利用OAuth的静态方法,检查页面是否完成授权。 OAuth也是一个静态的私有类,主要是代理WechatAuthorizeManager完成OAuth2认证。下面看看OAuth的静态方法existsOpenid的具体实现:
/**
* 检查当前会话是否获取到了openid;
* 如果没有openid,则通过OAuth2从微信服务器获取;
* 如果已有openid,则使用{@param Homepage}响应302;
* 只有用户第一次通过微信进入以及session过期时,
* 会通过微信浏览器获取openid,随后的访问的session都是有openid的。
* 总之,前端通过微信访问的页面,只有两种请求会进入后台:
* 1,用户刚进入网站;2,已获取openid时,前端页面通过微信浏览器刷新。
*/
public static boolean existsOpenid(HttpServletRequest request, HttpServletResponse response){
    String openid = getOpenIdFromSession(request);if(StringUtils.isEmpty(openid)) {
        redirectCodeUri(response);
        return false;
    }
    return true;
}
从existsOpenid的代码实现可知,如果未完成OAuth2授权,则执行图1中的第2个步骤,而图1中3和4的步骤是微信客户端和微信服务器之间完成,因此结合wechatCode方法的实现,即算完成了OAuth2授权。
 
5,单页面应用页面分享
     由于平台出于安全性的考虑,需要对每一个分享页面的url进行签名,这意味着在执行页面分享前,就应当已经生成页面签名等分享页面的静态资源配置。在第3部分也已经提到,单页面的前端应用在做指定页面分享时,无法做到分享页面签名,并且签名数据通过类Ajax来获取是无法起作用的,因为签名信息,必须在页面渲染时,和js代码一起被浏览器解释。为了维持前后端独立部署和开发,并保持前端在其它客户端上的单页面应用,为了克服这两个问题,前后端需做如下工作。
     前端: (1), 在微信客户端执行页面跳转时,通过主动触发浏览器发生页面跳转,而不是通过跨家内置router;(2),使用<script />标签从webserver中获取签名数据,特别地,通过这个标签获取的.js文件不允许从缓存获取(也即是不允许Http304响应码)。
     webserver:(1),对每一个url访问页面在微信服务器进行签名;(2),对以wechat/wechat.js结尾的url动态响应微信浏览器客户端当前访问页面的签名数据;(3),缓存授权和签名数据;。
     本文是以后端开发的角度描述,所以对于前端部分,简要解释一下第2个工作。如图2所示,前端是单页面应用,只有一个静态的index.html文件,而页面签名数据是通过配置在html文件中<script src=".../webserver/wechat/wechat.js" />标签获取的,因此webserver对于以/wechat/wechat.js结尾的请求,都能依据wechat.js的宿主页面,在webserver内部动态的定位到指定签名文件。下面通过代码来详细分析webserver是如何完成上述3个工作的。
     前文4.2部分在讲解WechatAuthorizeManager.java的wechatDispatcher方法的实现时已经提到,关于页面分享部分涉及的代码,如下所示:
 
//signature js文件
String wechatJsFilename = PageSignature.WechatJsFilename(request, PageSignature.WechatJs);
if(url.endsWith(wechatJsFilename) || url.endsWith(PageSignature.WechatJs)) {
     return locateSignatureJs(request, response);
}
如代码所示,对于页面分享签名,同样定义了一个静态私有类PageSignature。先看一下方法WechatJsFilename的实现:
/**
* 实际根据request所属的session中存放js的宿主页面url来生成对应的js签名数据文件
* @return 返回真实的js签名数据文件
*/
public static String WechatJsFilename(HttpServletRequest request, String endWith){
    String relativeUrl = WechatAuthorizeManager.RequestRelativeUrl(request);
    return relativeUrl.replace("/","_")+"_"+endWith;
}
可以知道WechatJsFilename方法是返回wechat.js对应的真实js签名数据文件。
  • 核心逻辑
     下面看,locateSignatureJs方法是如何返回给客户端真正所需的js签名数据文件。代码如下:
private static boolean locateSignatureJs(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
    String url = request.getRequestURI();
    String wechatJsFilename = PageSignature.WechatJsFilename(request, PageSignature.WechatJs);
    if(url.endsWith(wechatJsFilename)) {
        //定位到了真实js文件,则直接正确返回.
        return true;
    }
    /**
    * 前端获取后台生成的{@param WechatJS} signature文件,每一个页面都有对应的一份签名;
    * 因此需要根据不同的页面重定向到不同的signature,
    * 也即是{@link PageSignature#WechatJsFilename}指向的文件。
    */
    if(!PageSignature.exists(wechatJsFilename)) {
         //此处只有signature过期或不存在会进入到此.
         PageSignature.refreshSignature(request);
    }
    String dispatchSignatureJs = url.replace(request.getContextPath(),"/")
             .replace(PageSignature.WechatJs, wechatJsFilename);
    request.getRequestDispatcher(dispatchSignatureJs).forward(request,response);
    return false;
}
从locateSignatureJs方法的代码实现可知,该方法主要是分两块。第一块是判别是当前请求为真实js时,说明定位成功。主要需说明第二块逻辑,该块逻辑就是处理以wechat.js结尾的请求,其处理逻辑即包括前文提到的关于webserver的工作(1)、(2)和(3)。从代码可以看到,逻辑首先判断数据是否缓存(有效),否则刷新缓存数据,最后根据真实js文件名,在webserver内部dispatch到js签名数据文件。在引出下文前,先看一下PageSignature#exists做了什么工作:
/**
* 检查js文件是否缓存(有效)
*/
public static boolean exists(String jsFilename){
    return WechatDataCache.instance().exists(PageSignature.WechatJsAbsPath, jsFilename);
}
从上图代码可以看到,js文件的检查实际是由WechatDataCache这个缓存单例类来代理完成。因此,在接下来的部分,将详细说明WechatDataCache的设计和实现,以及PageSignature#refreshSignature 方法的定义。
  • WechatDataCache
     在详细说明WechatDataCache具体代码逻辑前,先明确WechatDataCache要完成的功能:a,线程安全;b,独立的过期时间管理;c,缓存的一个key可以关联多个value;d,在执行数据缓存操作时,允许客户端程序在原子操作内同步执行其它动作,例如向磁盘写入文件。针对这4个功能点,以下通过代码实例详细说明。WechatDataCache简化的完整代码逻辑展示如下:
WechatDataCache.java
public class WechatDataCache {
 
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private final Lock readLock = lock.readLock();
    private final Lock writeLock = lock.writeLock();
    private static Map<String, CacheObject> CacheMap = new HashMap<>();
 
    private final long expiredTime;
    private final ScheduledExecutorService validationService;
 
    private static class SingletoInstance {
        static WechatDataCache instance = new WechatDataCache(7000 * 1000);
    }
 
    public static WechatDataCache instance(){
        return SingletoInstance.instance;
    }
 
    public WechatDataCache(long expiredTime) {
        this.expiredTime = expiredTime;
        /**
         * 在本地完成expired清理动作
         */
        this.validationService = Executors.newSingleThreadScheduledExecutor();
        this.validationService.scheduleAtFixedRate(cleanJob, this.expiredTime,
             this.expiredTime, TimeUnit.MILLISECONDS);
    }
 
    private Runnable cleanJob = new Runnable() {
        @Override
        public void run() {
            /**
             * 清理过期验证码
             */
            Iterator<String> iter = CacheMap.keySet().iterator();
            while (iter.hasNext()){
                String key = iter.next();
                // 由于并发缘故,{@link WechatDataCache#getCode(String)}
                // 在get/exists中的操作,可能已经把该{@param key}对应的Code, 清理掉了
                CacheObject code = CacheMap.get(key);
                if(code != null && code.isValidation(expiredTime)){
                    iter.remove();
                }
            }
        }
    };
 
    public String get(String key) {
        readLock.lock();
        try {
            CacheObject c = CacheMap.get(key);
            if (c == null)
                return null;
            if (!c.isValidation(expiredTime)) {
                CacheMap.remove(key);
                return null;
            }
            return c.value;
        } finally {
            readLock.unlock();
        }
    }
 
    public boolean exists(String key, String value) {
        readLock.lock();
        try {
            CacheObject c = CacheMap.get(key);
            if (c == null)
                return false;
            if (!c.isValidation(expiredTime)) {
                CacheMap.remove(key);
                return false;
            }
            return c.has(value);
        } finally {
            readLock.unlock();
        }
    }
 
    public void put(String key, String value) {
        writeLock.lock();
        try {
            CacheMap.put(key, new CacheObject(new Date(), value));
        } finally {
            writeLock.unlock();
        }
    }
 
    /**
     * 使用key-value形式缓存具体数据对象的索引。
     * 由于前后端完全分离,signature需要通过后台请求微信服务器生成,
     * 前台请求得到的signature无法在ajax结果中配置(也即是signature应当在浏览器解释js时配置好),
     * 并且每一个页面都需要一份signature,而所有的signature又共享同一个access_token和jsapi_ticket;
     * 因此, key对应的value关联所有signature相关。
     * @param key key:js文件目录
     * @param valueAction value:所有的js文件名
     */
    public void put(String key, FileValuesAction valueAction) {
        writeLock.lock();
        try {
            CacheObject co = valueAction.addAction(key, CacheMap);
            //如果是重复的值,则为null
            if(co != null) {
                CacheMap.put(key, co);
            }
        } finally {
            writeLock.unlock();
        }
    }
 
    public static abstract class FileValuesAction {
        private final String value;
        public FileValuesAction(String value) {
            this.value = value;
        }
        public String value() {
            return this.value;
        }
 
        /**
         * 对已加入的{@param value}返回null,并不做任何处理
         * @param key
         * @param cacheMap
         * @return
         */
        protected CacheObject addAction(String key, Map<String, CacheObject> cacheMap) {
            CacheObject co = cacheMap.get(key);
            if(co != null){
                //已经加入的值,不需要再添加
                if (co.has(value))
                    return null;
            }
            //执行文件更新操作
            String filename = doAction();
            return co == null ? new CacheObject(new Date(), filename, true)
                    : co.addValue(filename);
        }
        /**
         * 在返回文件名之前,允许执行相关的文件操作
         * @return 文件名
         */
        abstract String doAction();
    }
 
    private static class CacheObject {
        final Date createdTime;
        String value;
        final boolean multiValue;
        final static String SPLIT = ",";
 
        public CacheObject(Date createdTime, String value) {
            this(createdTime, value, false);
        }
 
        public CacheObject(Date createdTime, String value, boolean multiValue) {
            this.createdTime = createdTime;
            this.value = value;
            this.multiValue = multiValue;
        }
 
        public boolean isMultiValue() {
            return multiValue;
        }
 
        public boolean has(String v){
            String[] values = value.split(SPLIT);
            for(String value:values){
                if(v.equals(value))
                    return true;
            }
            return false;
        }
 
        public CacheObject addValue(String value){
            Assert.isTrue(this.multiValue, "不允许添加多个值");
            this.value = this.value + SPLIT + value;
            return this;
        }
 
        public boolean isValidation(long expiredTime) {
            long duration = System.currentTimeMillis() - createdTime.getTime();
            return duration < expiredTime;
        }
    }
}
View Code
     1)线程安全
     由于webserver的应用环境是并发环境,而WechatDataCache作为缓存实例,属于热点竞争资源,因此需要保证WechatDataCache线程安全。考虑WechatDataCache要执行的动作是频繁的get操作和极少的put操作,因此读写锁(ReadWriteLock)是很好的选择。正如WechatDataCache.java代码所定义,使用ReentrantReadWriteLock作为读写锁实现,并在所有get/exists操作中使用读锁,所有put操作中使用写锁。
     2)独立的过期时间管理以及一个key可以关联多个value
     为了解决前文提到的功能b和c,在WechatDataCache.java后面部分可以看到,定义了一个CacheObject类,使用该类的实例作为缓存数据的value。从CacheObject的属性定义部分可以看到,属性value允许多个以","分隔的值。而为了管理过期时间,CacheObject也提供了数据创建时间(createdTime)属性和isValidation方法来验证数据的有效性;此外,为了尽早清理掉无效数据,WechatDataCache在创建实例的时候创建了一个单线程、单调度任务的线程池,定期清理缓存中的无效数据。
     3)缓存数据时,允许客户端程序在原子操作内同步执行其它动作
     为了允许在缓存页面签名数据的原子操作内同步完成.js签名数据文件的磁盘写入操作,在WechatDataCache中定义了方法put(String, FileValuesAction),允许put操作是传入FileValuesAction 对象。如 WechatDataCache.java代码定义可见FileValuesAction是一个abstract类,它开放了doAction方法允许客户端实现该方法来执行想要的动作,而它的核心方法addAction正是在doAction方法执行的前后做了必要的同步操作。
  • 刷新js签名数据文件
     再回到locateSignatureJs方法,如果PageSignature#exists判断js文件已经过期失效,则会去请求刷新签名数据,即如下代码实现:
/**
* 如果signature过期或者不存在,则需要刷新signature文件
*/
public static void refreshSignature(final HttpServletRequest request){
    CompositionHttpClient.httpsRequest(new CompositionHttpClient.Action() {
        @Override
        public void doAction(HttpClient httpClient)
                throws InterruptedException, ExecutionException, TimeoutException {
            requestSignature(httpClient, request);
        }
    });
}
从PageSignature#refreshSignature方法的定义可以看到它主要依赖私有方法requestSignature通过HttpClient向微信服务器(由于安全的考虑,实际请求可能是一台在内网的代理服务器)请求签名。然后看一下请求对页面签名的详细操作,如下:
/**
* 请求signature, 生成分享配置js文件
*/
private static void requestSignature(HttpClient httpClient, final HttpServletRequest request)
        throws InterruptedException, ExecutionException, TimeoutException {
    //access token
    Parameter<String> accessToken = WechatAuthorizeManager.requestIfAbsentOpenApiToken(httpClient);
    //ticket
    String ticket = WechatDataCache.instance().get(TicketKey);
    if(StringUtils.isEmpty(ticket)) {
        ticket = requestTicket(httpClient, accessToken);
        WechatDataCache.instance().put(TicketKey, ticket);
    }
    genSignatureAndWechatJs(request, ticket);
}
 
/**
* 在{@param WechatJsAbsPath}目录下生成{@param jsFilename}文件。
* 使用{@param WechatJsAbsPath}作为key,标明该目录下的数据文件,以目录的缓存周期算:
* 也即是说,当{@param WechatJsAbsPath}对应的key被清理时,该目录下的所有缓存数据也就失效。
*/
private static void generateWechatJs(String jsFilename, final SharedInfoParameter parameters){
    if(logger.isInfoEnabled()) {
        logger.info("SharedInfoParameter ==> {}", parameters.toString());
    }
    WechatDataCache.instance().put(WechatJsAbsPath, new WechatDataCache.FileValuesAction(jsFilename) {
        @Override
        String doAction() {
            String sharedConfig = SharedConfig
                    .replace(parameters.timestamp().key(), parameters.timestamp().value())
                    .replace(parameters.nonceStr().key(), parameters.nonceStr().value())
                    .replace(parameters.signature().key(), parameters.signature().value())
                    .replace(parameters.title().key(), parameters.title().value())
                    .replace(parameters.link().key(),
                            parameters.link().value().replace(WechatAuthorizeManager.HomepagePath,""));
            File tmp = new File(WechatJsAbsPath +value());
            if(!tmp.exists()){
                try {
                    if(logger.isInfoEnabled())
                        logger.info("创建文件:{}", tmp.getAbsolutePath());
                    tmp.createNewFile();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
            try (BufferedWriter bw = new BufferedWriter(new FileWriter(tmp))) {
                bw.write(sharedConfig);
            } catch (Exception  e){
                throw new RuntimeException(e);
            }
            return value();
        }
    });
}
View Code
在requestSignature方法的定义可知,基本是微信开放平台要求的标准流程,获取access_token和ticket,并做缓存检查,再就是生成签名等官方已定义的标准流程和示例,这里不再详述。因此,着重说明其中生成.js文件的实现。在这里可以理解,所有的js签名文件的有效期都是access_toke、ticket和签名近似同步的,那么只需约定js签名文件的目录索引的过期时间和签名基本一致就可以了,不需要关心每个js是否有效。如上图generateWechatJs方法的代码定义,依赖前文提到的WechatDataCache类提供的put操作,通过实现开方的doAction方法实现同步写入磁盘文件。
 
6,总结
     本文通过微信平台作为实战实例,后台为Java Web应用,详细地阐述了一种解决前后端完全分离且前端为单页面应用的OAuth2和页面分享的思路。在经历这一系列采坑之路后,不难体会到,在某些方面看似优秀的新的技术方案,在难以预料的后期迭代中,既有可能付出更高的代价,毕竟软件行业也是风云变幻。在本文提到的应用场景里,虽然保存了原始项目的部署架构,实际也是通过破环纯正的前后端完全分离,以及前端单页面应用框架的有点来实现需求的。

posted on 2017-10-13 14:03  神机小道  阅读(20809)  评论(0编辑  收藏  举报

导航