最近在做微信登錄功能時(shí),微信掃碼頁(yè)和報(bào)錯(cuò)頁(yè)使用同一個(gè)url頁(yè)面:
http://www.test.com/oauth/wechat/login
示例代碼如下:
/**
* 發(fā)起微信oauth登錄
*
* @return
*/
@RequestMapping({"/wechat/login"})
public ModelAndView oauthLogin(HttpServletRequest request, @RequestParam(required = false) String returnUrl,
@ModelAttribute("message") String message, @ModelAttribute("success") String success) {
ModelAndView modelAndView = new ModelAndView();
String state = RandomStringUtils.randomAlphabetic(20);
logger.info("returnUrl={}", StringEscapeUtils.escapeURL(resource.getClient().getPreEstablishedRedirectUri() + "?returnUrl=" + returnUrl));
modelAndView.addObject("appid", resource.getClient().getClientId());
modelAndView.addObject("redirect_uri",
StringEscapeUtils.escapeURL(resource.getClient().getPreEstablishedRedirectUri() + "?returnUrl=" + returnUrl));
modelAndView.addObject("state", state);
modelAndView.addObject("scope", "snsapi_login");
modelAndView.addObject("returnUrl", returnUrl);
//設(shè)置報(bào)錯(cuò)信息
if (StringUtils.isNotBlank(success)) {
modelAndView.addObject("success", success);
modelAndView.addObject("message", message);
}
modelAndView.setViewName("wechat_qr_code");
return modelAndView;
}


用戶(hù)掃描二維碼后,微信回調(diào)地址是:http://www.test.com/oauth/health?code=CODE&state=STATE
因此當(dāng)用戶(hù)登錄時(shí)校驗(yàn)發(fā)現(xiàn)異常,需要重定向到微信掃描頁(yè)面,并且把報(bào)錯(cuò)信息傳輸過(guò)去,最開(kāi)始采用的是RedirectView Flash Attributes來(lái)傳遞信息:
private ModelAndView generateLoginFailureView(RedirectAttributes attributes, String message, String returnUrl) {
ModelAndView modelAndView = new ModelAndView();
attributes.addFlashAttribute("message", message);
attributes.addFlashAttribute("success", "false");
returnUrl = StringEscapeUtils.escapeURL(StringEscapeUtils.unescapeURL(returnUrl));
modelAndView.setView(new RedirectView("/oauth/wechat/login?returnUrl=" + returnUrl));
return modelAndView;
}
出現(xiàn)了一個(gè)問(wèn)題,有時(shí)候報(bào)錯(cuò)能正常顯示,有時(shí)候報(bào)錯(cuò)無(wú)法正常顯示,排查了很長(zhǎng)時(shí)間,才發(fā)現(xiàn)原來(lái)是RedirectView Flash Attributes存在的問(wèn)題:
Flash Attributes:
Flash attributes provide a way for one request to store attributes that are intended for use in another. This is most commonly needed when redirecting?—?for example, the Post-Redirect-Get pattern. Flash attributes are saved temporarily before the redirect (typically in the session) to be made available to the request after the redirect and are removed immediately.
大致含義是:
Flash屬性
Flash屬性為一個(gè)請(qǐng)求提供了一種存儲(chǔ)屬性方式,使其能在另一個(gè)請(qǐng)求中使用該屬性。重定向時(shí)最常需要此操作,例如Post-Redirect-Get模式。 Flash屬性在重定向之前(通常在會(huì)話(huà)中)被臨時(shí)保存,以便在重定向之后可供請(qǐng)求使用,并立即被刪除。
上面介紹了Flash Attributes的功能,需要注意的是該存儲(chǔ)是在臨時(shí)會(huì)話(huà)中的。這樣就存在一個(gè)問(wèn)題,分布式環(huán)境下,重定向存儲(chǔ)的屬性,在用戶(hù)請(qǐng)求時(shí)可能無(wú)法獲取到(多臺(tái)機(jī)器的原因)。文檔也對(duì)此進(jìn)行了說(shuō)明:
The concept of flash attributes exists in many other web frameworks and has proven to sometimes be exposed to concurrency issues. This is because, by definition, flash attributes are to be stored until the next request. However the very “next” request may not be the intended recipient but another asynchronous request (for example, polling or resource requests), in which case the flash attributes are removed too early.
大致含義是:
Flash屬性的概念存在于許多其他Web框架中,并已證明有時(shí)會(huì)遇到并發(fā)問(wèn)題。這是因?yàn)楦鶕?jù)定義,閃存屬性將存儲(chǔ)到下一個(gè)請(qǐng)求。但是,“下一個(gè)”請(qǐng)求可能不是預(yù)期的接收者,而是另一個(gè)異步請(qǐng)求(例如,輪詢(xún)或資源請(qǐng)求),在這種情況下,過(guò)早刪除閃存屬性。
對(duì)于該問(wèn)題,在微信登錄中,本人采用redis緩存進(jìn)行解決。大致代碼如下:
private ModelAndView generateLoginFailureView(RedirectAttributes attributes, String message, String returnUrl) {
ModelAndView modelAndView = new ModelAndView();
returnUrl = StringEscapeUtils.escapeURL(StringEscapeUtils.unescapeURL(returnUrl));
cache.setEx(OAUTH_LOGIN_STATUS_PREFIX + state, "false|" + message, 1, TimeUnit.MINUTES);
modelAndView.setView(new RedirectView("/oauth/wechat/login?state=" + state +"&returnUrl=" + returnUrl));
return modelAndView;
}
@RequestMapping({"/wechat/login"})
public ModelAndView oauthLogin(HttpServletRequest request, @RequestParam(required = false) String returnUrl, @RequestParam(required = false) String state) {
ModelAndView modelAndView = new ModelAndView();
String newState = RandomStringUtils.randomAlphabetic(20);
cache.setEx(OAUTH_LOGIN_STATUS_PREFIX + newState, "1", 5, TimeUnit.MINUTES);
logger.info("returnUrl={}", StringEscapeUtils.escapeURL(resource.getClient().getPreEstablishedRedirectUri() + "?returnUrl=" + returnUrl));
modelAndView.addObject("appid", resource.getClient().getClientId());
modelAndView.addObject("redirect_uri",
StringEscapeUtils.escapeURL(resource.getClient().getPreEstablishedRedirectUri() + "?returnUrl=" + returnUrl));
modelAndView.addObject("state", newState);
modelAndView.addObject("scope", "snsapi_login");
modelAndView.addObject("returnUrl", returnUrl);
if (StringUtils.isNotBlank(state) && cache.exist(OAUTH_LOGIN_STATUS_PREFIX + state)) {
String[] stateValue = jCloudCache.get(OAUTH_LOGIN_STATUS_PREFIX + state).split("\\|");
if (stateValue.length == 2) {
modelAndView.addObject("success", stateValue[0]);
modelAndView.addObject("message", stateValue[1]);
}
cache.del(OAUTH_LOGIN_STATUS_PREFIX + state);
}
modelAndView.setViewName("wechat_qr_code");
return modelAndView;
}