關於OAuth
官方教程:https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140842&token=&lang=zh_CN
原理及基本開發思路:http://www.cnblogs.com/szw/p/3764275.html
現象
在使用公眾號的OAuth過程中,我們有時會碰到40029(invalid code,不合法的oauth_code)的錯誤。
原因
其實通過官方提供的API獲取的CODE通常是不會有問題的,不可用是因為這個CODE被悄悄地用掉了。
通過微信Web開發工具跟蹤可以看到,微信發起了2次“相同”的請求,第一次請求被其終止掉了(也就是我們主動發起的這一次):
這兩次請求的Url還是有差別的,第一次我們通過“GetAuthorizeUrl”介面獲取到的url如下:
這次請求會被微信伺服器中斷,然後由微信再自動發起一次:
對比可以發現第二次請求多了兩個引數:
&uin=MTMyMjE0NDU%3D&key=63e987ba88ddfb44972f00256f262220434daa5ef8d27994eeb1cf525b6ddf2c5fc2aeb69d08087f5c139292417a774e
&pass_ticket=SISrTjV8ln27168AZM/sFXkc2yp5i2lm+rwItExPL+PxZBu2/GXq1MbUp6BvmnWAB0KtpV9nypybsh41CV2SQA=
這兩個引數應該也是出於的安全的需要,但是這麼一來,給開發者的伺服器就會帶來困擾:
第一次請求雖然微信伺服器終止了,但是開發者伺服器還在執行,多數情況下已經使用了redirect_uri,並把傳遞過來的code使用掉了(code是一次性的),
當第二次請求進來的時候,我們用相同的code自然就失效了。
解決方案一(推薦★☆☆☆☆)
從圖中可以看到,其實兩次請求的發起者是不一樣的,可以從這個角度入手,鑑別正確的請求。
當然這個方法有一定的風險:兩次請求發生的時間間隔非常小(上圖為19毫秒),仍然需要處理非同步的問題。
解決方案二(推薦★★☆☆☆)
這也是網傳的一個方案:在正常獲取了微信官方的url後面,加上&connect_redirect=1這個引數,微信就不會發起第二次,
但是本人測試沒有成功,然收到了兩次。
解決方案三(推薦★★★★☆)
既然第二次請求的引數和第一次不一樣,就可以從uin和pass_ticket兩個引數進行判斷,只接受有這兩個引數的請求。
這種做法的缺點是這個請求引數並沒有體現在官方文件中,或許會悄悄地進行變化,所以需要時刻關注其有效性。
此方案作為一個條件加入到其他方案中還是不錯的。
解決方案四(推薦★★★★★)
利用同步鎖,判斷code的使用情況,這是最粗獷也是最徹底的方法,以 C# 使用 Senparc.Weixin SDK 為例,直接上程式碼:
定義靜態變數:
static Dictionary<string, OAuthAccessTokenResult> OAuthCodeCollection = new Dictionary<string, OAuthAccessTokenResult>();
static object OAuthCodeCollectionLock = new object();
回撥方法內:
string openId;
OAuthAccessTokenResult result = null;
try
{
//通過,用code換取access_token
var isSecondRequest = false;
lock (OAuthCodeCollectionLock)
{
isSecondRequest = OAuthCodeCollection.ContainsKey(code);
}
if (!isSecondRequest)
{
//第一次請求
LogUtility.Weixin.DebugFormat("第一次微信OAuth到達,code:{0}", code);
lock (OAuthCodeCollectionLock)
{
OAuthCodeCollection[code] = null;
}
}
else
{
//第二次請求
LogUtility.Weixin.DebugFormat("第二次微信OAuth到達,code:{0}", code);
lock (OAuthCodeCollectionLock)
{
result = OAuthCodeCollection[code];
}
}
try
{
try
{
result = result ?? OAuthApi.GetAccessToken(SiteConfig.YourAppId, SiteConfig.YourAppSecret, code);
}
catch (Exception ex)
{
return Content("OAuth AccessToken錯誤:" + ex.Message);
}
if (result != null)
{
lock (OAuthCodeCollectionLock)
{
OAuthCodeCollection[code] = result;
}
}
}
catch (ErrorJsonResultException ex)
{
if (ex.JsonResult.errcode == ReturnCode.不合法的oauth_code)
{
//code已經被使用過
lock (OAuthCodeCollectionLock)
{
result = OAuthCodeCollection[code];
}
}
}
openId = result != null ? result.openid : null;
}
catch (Exception ex)
{
return Content("授權過程發生錯誤:" + ex.Message);
}
//使用result繼續操作
說明:
1、上述靜態Dicitonary的儲存方式適用於單臺伺服器,如果是分散式的系統,這裡的Dictionary請使用公共快取(如Redis),並使用分佈鎖,否則如果兩次請求命中了兩臺不同的伺服器仍然會失效。
2、請注意做好快取清理工作
解決方案總結
以上解決方案沒有絕對的好壞之分,要看具體的環境,因為都不會涉及到影響效率和安全性的問題,可以視情況組合使用。推薦指數更多傾向於通用性。
參考資料
微信網頁授權:https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140842&token=&lang=zh_CN
Senparc.Weixin.MP SDK 微信公眾平臺開發教程(十二):OAuth2.0說明:http://www.cnblogs.com/szw/p/3764275.html