原文鏈接:http://blog.csdn.net/hjun01/article/details/42032841
OAuth 2.0 for Web Server Applications, verifying a user's Android in-app subscription
在寫本文之前先說些題外話。
前段時間游戲急於在GoolePlay上線,明知道如果不加Auth2.0的校驗是不安全的還是暫時略過了這一步,果然沒幾天就發現后台記錄與玩家實際付費不太一致,懷疑有玩家盜刷游戲元寶等,並且真實的走過了GooglePlay的所有支付流程完成道具兌換,時間一長嚴重性可想而知。經過查閱大量google官方文檔后把代碼補上,並在這里記錄下OAuth 2.0 的使用,Google提供了OAuth2.0的好幾種使用用途,每種使用方法都有些不同,具體可以看下這篇博客。在這里只寫OAuth 2.0 for Web Server Applications的使用,涉及refresh_token, access_token等的獲取和使用,以及如何向google發送GET和POST請求等,最終完成用戶在安卓應用內支付購買信息的校驗。
先貼下關於Using OAuth 2.0 for Web Server Applications的解釋的谷歌官方原文:
The authorization sequence begins when your application redirects a browser to a Google URL; the URL includes query parameters that indicate the type of access being requested. As in other scenarios, Google handles user authentication, session selection, and user consent. The result is an authorization code, which Google returns to your application in a query string.
After receiving the authorization code, your application can exchange the code (along with a client ID and client secret) for an access token and, in some cases, a refresh token.
The application can then use the access token to access a Google API.
If a refresh token is present in the authorization code exchange, then it can be used to obtain new access tokens at any time. This is called offline access, because the user does not have to be present at the browser when the application obtains a new access token.
通過原文和圖解我們可以知道這樣一個流程(下文會詳細說明):
一. 在Google Developer Console中創建一個 Web Application賬戶,得到client_id,client_secret 和 redirect_uri,這3個參數后邊步驟常用到(此為前提)
二. 獲取Authorization code
三. 利用code 獲取access_token,refresh_token
四. 進一步可利用refresh_token獲取新的access_token
五. 使用access_token 調用Google API 達到最終目的(如果access_token過時,回到第四步)
需注意的是:在第三步操作,當我們第一次利用code獲取access_token時,谷歌會同時返回給你一個refresh_token,以后再次用code獲取access_token操作將不會再看到refresh_token,所以一定要保存下來。這個refresh_token是長期有效的,如果沒有明確的被應用管理者撤銷是不會過期的,而access_token則只有3600秒的時效,即1個小時,那么問題來了,access_token和refresh_token是什么關系?很明顯的,我們最終是要使用access_token 去調用Google API,而access_token又有時效限制,所以當access_token過期后,我們可以用長效的refresh_token去再次獲取access_token,並且可以可以在任何時間多次獲取,沒有次數限制。其實當我們得到refresh_token后,就是一個轉折點。
下面詳細分解步驟:
一、在Google Developer Console中創建一個Web application賬戶
(這里使用的是新版的Google Developer Console頁面,其實可在Account settings中設置為中文顯示~)
其中4.可以隨意填寫。創建完成后可以看下下圖所示:
在這里我們拿到3個關鍵參數: client_id,client_secret,redirect_uris,,於下邊步驟。
可能會有人有疑問,怎么就能確定在google developer console 建立的project就於Googleplay上線的安卓應用有關聯呢?為什么可以用這些參數得來的access_token去調用谷歌API?其實在Googleplay發布應用時就有關聯project的操作,之后創建project的人可以給其他谷歌賬戶授權,這樣其他谷歌賬戶可以在自己的developer console頁面直接看到該project和以下的web application等, 並且可在下一步操作中登錄自己的谷歌賬戶獲取code。
二. 獲取Authorization code
https://accounts.google.com/o/oauth2/auth?scope=https://www.googleapis.com/auth/androidpublisher &response_type=code &access_type=offline &redirect_uri={REDIRECT_URIS} &client_id={CLIENT_ID}
我們需要將這個URL以瀏覽器的形式打開,這時會跳出提示你Sign in with your Google Account,然后在用有project授權的谷歌賬戶登錄,地址欄會出現我們所需的code。例如:https://www.example.com/oauth2callback?code=4/CpVOd8CljO_gxTRE1M5jtwEFwf8gRD44vrmKNDi4GSS.kr-GHuseD-oZEnp6UADFXm0E0MD3FlAI
三. 利用code 獲取access_token,refresh_token
https://accounts.google.com/o/oauth2/token? code={CODE} &client_id={CLIENT_ID} &client_secret={CLIENT_SECRET} &redirect_uri={REDIRECT} &grant_type=authorization_code
我們這一步的目的是獲取refresh_token,只要有了這個長效token,access_token是隨時可以獲取的,第一次發起請求得到的JSON字符串如下所示,以后再請求將不再出現refresh_token,要保存好。expires_in是指access_token的時效,為3600秒。
{
"access_token": "ya29.3gC2jw5vm77YPkylq0H5sPJeJJDHX93Kq8qZHRJaMlknwJ85595eMogL300XKDOEI7zIsdeFEPY6zg", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "1/FbQD448CdDPfDEDpCy4gj_m3WDr_M0U5WupquXL_o"
}
四. 進一步可利用refresh_token獲取新的access_token
https://accounts.google.com/o/oauth2/token? grant_type=refresh_token &client_id={CLIENT_ID} &client_secret={CLIENT_SECRET} &refresh_token={REFRESH_TOKEN}
這里我們要向谷歌發起POST請求,Java代碼如下:
- /** 獲取access_token **/
- private static Map<String,String> getAccessToken(){
- final String CLIENT_ID = "填入你的client_id";
- final String CLIENT_SECRET = "填入你的client_secret";
- final String REFRESH_TOKEN = "填入上一步獲取的refresh_token";
- Map<String,String> map = null;
- try {
- /**
- * https://accounts.google.com/o/oauth2/token?refresh_token={REFRESH_TOKEN}
- * &client_id={CLIENT_ID}&client_secret={CLIENT_SECRET}&grant_type=refresh_token
- */
- URL urlGetToken = new URL("https://accounts.google.com/o/oauth2/token");
- HttpURLConnection connectionGetToken = (HttpURLConnection) urlGetToken.openConnection();
- connectionGetToken.setRequestMethod("POST");
- connectionGetToken.setDoOutput(true);
- // 開始傳送參數
- OutputStreamWriter writer = new OutputStreamWriter(connectionGetToken.getOutputStream());
- writer.write("refresh_token="+REFRESH_TOKEN+"&");
- writer.write("client_id="+CLIENT_ID+"&");
- writer.write("client_secret="+CLIENT_SECRET+"&");
- writer.write("grant_type=refresh_token");
- writer.close();
- //若響應碼為200則表示請求成功
- if(connectionGetToken.getResponseCode() == HttpURLConnection.HTTP_OK){
- StringBuilder sb = new StringBuilder();
- BufferedReader reader = new BufferedReader(
- new InputStreamReader(connectionGetToken.getInputStream(), "utf-8"));
- String strLine = "";
- while((strLine = reader.readLine()) != null){
- sb.append(strLine);
- }
- // 取得谷歌回傳的信息(JSON格式)
- JSONObject jo = JSONObject.fromObject(sb.toString());
- String ACCESS_TOKEN = jo.getString("access_token");
- Integer EXPIRES_IN = jo.getInt("expires_in");
- map = new HashMap<String,String>();
- map.put("access_token", ACCESS_TOKEN);
- map.put("expires_in", String.valueOf(EXPIRES_IN));
- // 帶入access_token的創建時間,用於之后判斷是否失效
- map.put("create_time",String.valueOf((new Date().getTime()) / 1000));
- logger.info("包含access_token的JSON信息為: "+jo);
- }
- } catch (MalformedURLException e) {
- logger.error("獲取access_token失敗,原因是:"+e);
- e.printStackTrace();
- } catch (IOException e) {
- logger.error("獲取access_token失敗,原因是:"+e);
- e.printStackTrace();
- }
- return map;
- }
五. 使用access_token 調用Google API 達到最終目的(如果access_token過時,回到第四步)
在這里我所需要獲取的是我在應用內給GooglePlay支付的購買信息,此類信息包含以下幾個屬性:(可參考Google Play Developer API下的Purchases.products)
A ProductPurchase resource indicates the status of a user's inapp product purchase.
{ "kind": "androidpublisher#productPurchase", "purchaseTimeMillis": long, "purchaseState": integer, (purchased:0 cancelled:1,我們就是依靠這個判斷購買信息) "consumptionState": integer, "developerPayload": string }
帶着access_token參數向GoogleApi發起GET請求,Java代碼如下:
- private static Map<String,String> cacheToken = null;//設置靜態變量,用於判斷access_token是否過期
- public static GooglePlayBuyEntity getInfoFromGooglePlayServer(String packageName,String productId, String purchaseToken) {
- if(null != cacheToken){
- Long expires_in = Long.valueOf(cacheToken.get("expires_in")); // 有效時長
- Long create_time = Long.valueOf(cacheToken.get("create_time")); // access_token的創建時間
- Long now_time = (new Date().getTime()) / 1000;
- if(now_time > (create_time + expires_in - 300)){ // 提前五分鍾重新獲取access_token
- cacheToken = getAccessToken();
- }
- }else{
- cacheToken = getAccessToken();
- }
- String access_token = cacheToken.get("access_token");
- GooglePlayBuyEntity buyEntity = null;
- try {
- /**這是寫這篇博客時間時的最新API,v2版本。
- * https://www.googleapis.com/androidpublisher/v2/applications/{packageName}
- * /purchases/products/{productId}/tokens/{purchaseToken}?access_token={access_token}
- */
- String url = "https://www.googleapis.com/androidpublisher/v2/applications";
- StringBuffer getURL = new StringBuffer();
- getURL.append(url);
- getURL.append("/" + packageName);
- getURL.append("/purchases/products");
- getURL.append("/" + productId );
- getURL.append("/tokens/" + purchaseToken);
- getURL.append("?access_token=" + access_token);
- URL urlObtainOrder = new URL(getURL.toString());
- HttpURLConnection connectionObtainOrder = (HttpURLConnection) urlObtainOrder.openConnection();
- connectionObtainOrder.setRequestMethod("GET");
- connectionObtainOrder.setDoOutput(true);
- // 如果認證成功
- if (connectionObtainOrder.getResponseCode() == HttpURLConnection.HTTP_OK) {
- StringBuilder sbLines = new StringBuilder("");
- BufferedReader reader = new BufferedReader(new InputStreamReader(
- connectionObtainOrder.getInputStream(), "utf-8"));
- String strLine = "";
- while ((strLine = reader.readLine()) != null) {
- sbLines.append(strLine);
- }
- // 把上面取回來的資料,放進JSONObject中,以方便我們直接存取到想要的參數
- JSONObject jo = JSONObject.fromObject(sbLines.toString());
- Integer status = jo.getInt("purchaseState");
- if(status == 0){ //驗證成功
- buyEntity = new GooglePlayBuyEntity();
- buyEntity.setConsumptionState(jo.getInt("consumptionState"));
- buyEntity.setDeveloperPayload(jo.getString("developerPayload"));
- buyEntity.setKind(jo.getString("kind"));
- buyEntity.setPurchaseState(status);
- buyEntity.setPurchaseTimeMillis(jo.getLong("purchaseTimeMillis"));
- }else{
- // 購買無效
- buyEntity = new GooglePlayBuyEntity();
- buyEntity.setPurchaseState(status);
- logger.info("從GooglePlay賬單校驗失敗,原因是purchaseStatus為" + status);
- }
- }
- } catch (Exception e) {
- e.printStackTrace();
- buyEntity = new GooglePlayBuyEntity();
- buyEntity.setPurchaseState(-1);
- }
- return buyEntity;
- }
到這里就寫完了,如果有什么疑問可以留言。
另外,iOS應用內支付,蘋果商店AppStore購買信息校驗的博客在這里:http://blog.csdn.net/hjun01/article/details/44039939