前段时间游戏急于在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请求等,最终完成用户在安卓应用内支付购买信息的校验。
通过原文和图解我们可以知道这样一个流程(下文会详细说明):
一. 在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; }