前(单页面)后端完全分离的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; } } }
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(); } }); }
在requestSignature方法的定义可知,基本是微信开放平台要求的标准流程,获取access_token和ticket,并做缓存检查,再就是生成签名等官方已定义的标准流程和示例,这里不再详述。因此,着重说明其中生成.js文件的实现。在这里可以理解,所有的js签名文件的有效期都是access_toke、ticket和签名近似同步的,那么只需约定js签名文件的目录索引的过期时间和签名基本一致就可以了,不需要关心每个js是否有效。如上图generateWechatJs方法的代码定义,依赖前文提到的WechatDataCache类提供的put操作,通过实现开方的doAction方法实现同步写入磁盘文件。
6,总结
本文通过微信平台作为实战实例,后台为Java Web应用,详细地阐述了一种解决前后端完全分离且前端为单页面应用的OAuth2和页面分享的思路。在经历这一系列采坑之路后,不难体会到,在某些方面看似优秀的新的技术方案,在难以预料的后期迭代中,既有可能付出更高的代价,毕竟软件行业也是风云变幻。在本文提到的应用场景里,虽然保存了原始项目的部署架构,实际也是通过破环纯正的前后端完全分离,以及前端单页面应用框架的有点来实现需求的。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!