最近在做微信登錄功能時(shí)式塌,微信掃碼頁和報(bào)錯(cuò)頁使用同一個(gè)url頁面:
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;
}
用戶掃描二維碼后捂敌,微信回調(diào)地址是:http://www.test.com/oauth/health?code=CODE&state=STATE
因此當(dāng)用戶登錄時(shí)校驗(yàn)發(fā)現(xiàn)異常泛烙,需要重定向到微信掃描頁面叹谁,并且把報(bào)錯(cuò)信息傳輸過去矢腻,最開始采用的是RedirectView Flash Attributes來傳遞信息:
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è)問題瓶殃,有時(shí)候報(bào)錯(cuò)能正常顯示敲茄,有時(shí)候報(bào)錯(cuò)無法正常顯示位谋,排查了很長時(shí)間,才發(fā)現(xiàn)原來是RedirectView Flash Attributes存在的問題:
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ì)話中)被臨時(shí)保存赊淑,以便在重定向之后可供請(qǐng)求使用,并立即被刪除仅讽。
上面介紹了Flash Attributes的功能陶缺,需要注意的是該存儲(chǔ)是在臨時(shí)會(huì)話中的。這樣就存在一個(gè)問題洁灵,分布式環(huán)境下饱岸,重定向存儲(chǔ)的屬性,在用戶請(qǐng)求時(shí)可能無法獲取到(多臺(tái)機(jī)器的原因)徽千。文檔也對(duì)此進(jìn)行了說明:
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ā)問題。這是因?yàn)楦鶕?jù)定義双抽,閃存屬性將存儲(chǔ)到下一個(gè)請(qǐng)求百框。但是,“下一個(gè)”請(qǐng)求可能不是預(yù)期的接收者牍汹,而是另一個(gè)異步請(qǐng)求(例如铐维,輪詢或資源請(qǐng)求)柬泽,在這種情況下,過早刪除閃存屬性方椎。
對(duì)于該問題聂抢,在微信登錄中,本人采用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;
}