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和页面分享的思路。在经历这一系列采坑之路后,不难体会到,在某些方面看似优秀的新的技术方案,在难以预料的后期迭代中,既有可能付出更高的代价,毕竟软件行业也是风云变幻。在本文提到的应用场景里,虽然保存了原始项目的部署架构,实际也是通过破环纯正的前后端完全分离,以及前端单页面应用框架的有点来实现需求的。