天天看點

【執行個體圖文詳解】OAuth 2.0 for Web Server Applications

        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.

【執行個體圖文詳解】OAuth 2.0 for Web Server Applications

通過原文和圖解我們可以知道這樣一個流程(下文會詳細說明):

        一. 在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中設定為中文顯示~)

【執行個體圖文詳解】OAuth 2.0 for Web Server Applications

        其中4.可以随意填寫。建立完成後可以看下下圖所示:

【執行個體圖文詳解】OAuth 2.0 for Web Server Applications

        在這裡我們拿到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