原文链接: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代码如下:

  1. /** 获取access_token **/
  2. private static Map<String,String> getAccessToken(){
  3. final String CLIENT_ID = "填入你的client_id";
  4. final String CLIENT_SECRET = "填入你的client_secret";
  5. final String REFRESH_TOKEN = "填入上一步获取的refresh_token";
  6. Map<String,String> map = null;
  7. try {
  8. /**
  9. * https://accounts.google.com/o/oauth2/token?refresh_token={REFRESH_TOKEN}
  10. * &client_id={CLIENT_ID}&client_secret={CLIENT_SECRET}&grant_type=refresh_token
  11. */
  12. URL urlGetToken = new URL("https://accounts.google.com/o/oauth2/token");
  13. HttpURLConnection connectionGetToken = (HttpURLConnection) urlGetToken.openConnection();
  14. connectionGetToken.setRequestMethod("POST");
  15. connectionGetToken.setDoOutput(true);
  16. // 开始传送参数
  17. OutputStreamWriter writer  = new OutputStreamWriter(connectionGetToken.getOutputStream());
  18. writer.write("refresh_token="+REFRESH_TOKEN+"&");
  19. writer.write("client_id="+CLIENT_ID+"&");
  20. writer.write("client_secret="+CLIENT_SECRET+"&");
  21. writer.write("grant_type=refresh_token");
  22. writer.close();
  23. //若响应码为200则表示请求成功
  24. if(connectionGetToken.getResponseCode() == HttpURLConnection.HTTP_OK){
  25. StringBuilder sb = new StringBuilder();
  26. BufferedReader reader = new BufferedReader(
  27. new InputStreamReader(connectionGetToken.getInputStream(), "utf-8"));
  28. String strLine = "";
  29. while((strLine = reader.readLine()) != null){
  30. sb.append(strLine);
  31. }
  32. // 取得谷歌回传的信息(JSON格式)
  33. JSONObject jo = JSONObject.fromObject(sb.toString());
  34. String ACCESS_TOKEN = jo.getString("access_token");
  35. Integer EXPIRES_IN = jo.getInt("expires_in");
  36. map = new HashMap<String,String>();
  37. map.put("access_token", ACCESS_TOKEN);
  38. map.put("expires_in", String.valueOf(EXPIRES_IN));
  39. // 带入access_token的创建时间,用于之后判断是否失效
  40. map.put("create_time",String.valueOf((new Date().getTime()) / 1000));
  41. logger.info("包含access_token的JSON信息为: "+jo);
  42. }
  43. } catch (MalformedURLException e) {
  44. logger.error("获取access_token失败,原因是:"+e);
  45. e.printStackTrace();
  46. } catch (IOException e) {
  47. logger.error("获取access_token失败,原因是:"+e);
  48. e.printStackTrace();
  49. }
  50. return map;
  51. }

五. 使用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代码如下:

  1. private static Map<String,String> cacheToken = null;//设置静态变量,用于判断access_token是否过期
  2. public static GooglePlayBuyEntity getInfoFromGooglePlayServer(String packageName,String productId, String purchaseToken) {
  3. if(null != cacheToken){
  4. Long expires_in = Long.valueOf(cacheToken.get("expires_in")); // 有效时长
  5. Long create_time = Long.valueOf(cacheToken.get("create_time")); // access_token的创建时间
  6. Long now_time = (new Date().getTime()) / 1000;
  7. if(now_time > (create_time + expires_in - 300)){ // 提前五分钟重新获取access_token
  8. cacheToken = getAccessToken();
  9. }
  10. }else{
  11. cacheToken = getAccessToken();
  12. }
  13. String access_token = cacheToken.get("access_token");
  14. GooglePlayBuyEntity buyEntity = null;
  15. try {
  16. /**这是写这篇博客时间时的最新API,v2版本。
  17. * https://www.googleapis.com/androidpublisher/v2/applications/{packageName}
  18. * /purchases/products/{productId}/tokens/{purchaseToken}?access_token={access_token}
  19. */
  20. String url = "https://www.googleapis.com/androidpublisher/v2/applications";
  21. StringBuffer getURL = new StringBuffer();
  22. getURL.append(url);
  23. getURL.append("/" + packageName);
  24. getURL.append("/purchases/products");
  25. getURL.append("/" + productId   );
  26. getURL.append("/tokens/" + purchaseToken);
  27. getURL.append("?access_token=" + access_token);
  28. URL urlObtainOrder = new URL(getURL.toString());
  29. HttpURLConnection connectionObtainOrder = (HttpURLConnection) urlObtainOrder.openConnection();
  30. connectionObtainOrder.setRequestMethod("GET");
  31. connectionObtainOrder.setDoOutput(true);
  32. // 如果认证成功
  33. if (connectionObtainOrder.getResponseCode() == HttpURLConnection.HTTP_OK) {
  34. StringBuilder sbLines = new StringBuilder("");
  35. BufferedReader reader = new BufferedReader(new InputStreamReader(
  36. connectionObtainOrder.getInputStream(), "utf-8"));
  37. String strLine = "";
  38. while ((strLine = reader.readLine()) != null) {
  39. sbLines.append(strLine);
  40. }
  41. // 把上面取回來的資料,放進JSONObject中,以方便我們直接存取到想要的參數
  42. JSONObject jo = JSONObject.fromObject(sbLines.toString());
  43. Integer status = jo.getInt("purchaseState");
  44. if(status == 0){ //验证成功
  45. buyEntity = new GooglePlayBuyEntity();
  46. buyEntity.setConsumptionState(jo.getInt("consumptionState"));
  47. buyEntity.setDeveloperPayload(jo.getString("developerPayload"));
  48. buyEntity.setKind(jo.getString("kind"));
  49. buyEntity.setPurchaseState(status);
  50. buyEntity.setPurchaseTimeMillis(jo.getLong("purchaseTimeMillis"));
  51. }else{
  52. // 购买无效
  53. buyEntity = new GooglePlayBuyEntity();
  54. buyEntity.setPurchaseState(status);
  55. logger.info("从GooglePlay账单校验失败,原因是purchaseStatus为" + status);
  56. }
  57. }
  58. } catch (Exception e) {
  59. e.printStackTrace();
  60. buyEntity = new GooglePlayBuyEntity();
  61. buyEntity.setPurchaseState(-1);
  62. }
  63. return buyEntity;
  64. }

到这里就写完了,如果有什么疑问可以留言。

另外,iOS应用内支付,苹果商店AppStore购买信息校验的博客在这里:http://blog.csdn.net/hjun01/article/details/44039939

【实例图文详解】OAuth 2.0 for Web Server Applications的更多相关文章

  1. Windows-007-进程相关命令(netstat、tasklist、taskkill、tskill)实战实例图文详解

    本节主要讲述 Windows 系统下,nestat.tasklist.tskill 三个 CMD 命令的参数,及使用方法:以及如何利用三者结合查看进程信息和结束进程.敬请亲们参阅,希望能对亲们有所帮助 ...

  2. 访问Storm ui界面,出现Nimbus Summary或Supervisor Summary时有时无的问题解决(图文详解)

    不多说,直接上干货! 前期博客 apache-storm-0.9.6.tar.gz的集群搭建(3节点)(图文详解) apache-storm-1.0.2.tar.gz的集群搭建(3节点)(图文详解)( ...

  3. 访问Storm ui界面,出现org.apache.storm.utils.NimbusLeaderNotFoundException: Could not find leader nimbus from seed hosts ["master"]. Did you specify a valid list of nimbus hosts for confi的问题解决(图文详解)

    不多说,直接上干货! 前期博客 apache-storm-0.9.6.tar.gz的集群搭建(3节点)(图文详解) apache-storm-1.0.2.tar.gz的集群搭建(3节点)(图文详解)( ...

  4. Android EventBus 3.0 实例使用详解

    EventBus的使用和原理在网上有很多的博客了,其中泓洋大哥和启舰写的非常非常棒,我也是跟着他们的博客学会的EventBus,因为是第一次接触并使用EventBus,所以我写的更多是如何使用,源码解 ...

  5. CentOS7+CDH5.14.0安装全流程记录,图文详解全程实测-总目录

    CentOS7+CDH5.14.0安装全流程记录,图文详解全程实测-总目录: 0.Windows 10本机下载Xshell,以方便往Linux主机上上传大文件 1.CentOS7+CDH5.14.0安 ...

  6. spark最新源码下载并导入到开发环境下助推高质量代码(Scala IDEA for Eclipse和IntelliJ IDEA皆适用)(以spark2.2.0源码包为例)(图文详解)

    不多说,直接上干货! 前言   其实啊,无论你是初学者还是具备了有一定spark编程经验,都需要对spark源码足够重视起来. 本人,肺腑之己见,想要成为大数据的大牛和顶尖专家,多结合源码和操练编程. ...

  7. hadoop-2.7.3.tar.gz + spark-2.0.2-bin-hadoop2.7.tgz + zeppelin-0.6.2-incubating-bin-all.tgz(master、slave1和slave2)(博主推荐)(图文详解)

    不多说,直接上干货! 我这里,采取的是ubuntu 16.04系统,当然大家也可以在CentOS6.5里,这些都是小事 CentOS 6.5的安装详解 hadoop-2.6.0.tar.gz + sp ...

  8. hadoop-2.6.0.tar.gz + spark-1.6.1-bin-hadoop2.6.tgz + zeppelin-0.5.6-incubating-bin-all.tgz(master、slave1和slave2)(博主推荐)(图文详解)

    不多说,直接上干货! 我这里,采取的是CentOS6.5,当然大家也可以在ubuntu 16.04系统里,这些都是小事 CentOS 6.5的安装详解 hadoop-2.6.0.tar.gz + sp ...

  9. VMware下OSSIM 4.1.0的下载、安装和初步使用(图文详解)

    不多说,直接上干货! 为什么,我写了一篇OSSIM 5.2.0的,还要再来写OSSIM 4.1.0呢,是因为,OSSIM 5.2.0所需内存较大,8G甚至16G,但是,肯定性能和里面集成组件越高级.也 ...

随机推荐

  1. NTP详解-转

    网管实战:Linux时间服务器配置 [IT168 专稿]目前计算机网络中各主机和服务器等网络设备的时间基本处于无序的状态.随着计算机网络应用的不断涌现,计算机的时间同步问题成为愈来愈重要的事情.以Un ...

  2. FPGA 17最佳论文导读 ESE: Efficient Speech Recognition Engine with Compressed LSTM on FPGA

    欢迎转载,转载请注明:本文出自Bin的专栏blog.csdn.net/xbinworld. 技术交流QQ群:433250724,欢迎对算法.机器学习技术感兴趣的同学加入. 后面陆续写一些关于神经网络加 ...

  3. 深度学习方法(十一):卷积神经网络结构变化——Google Inception V1-V4,Xception(depthwise convolution)

    欢迎转载,转载请注明:本文出自Bin的专栏blog.csdn.net/xbinworld. 技术交流QQ群:433250724,欢迎对算法.机器学习技术感兴趣的同学加入. 上一篇讲了深度学习方法(十) ...

  4. CSS3中的矩阵

    CSS3中的矩阵 CSS3中的矩阵指的是一个方法,书写为matrix()和matrix3d(),前者是元素2D平面的移动变换(transform),后者则是3D变换.2D变换矩阵为3*3,如下面矩阵示 ...

  5. mysql数据库和oracle数据库之间互相导入备份

    把从Oracle数据库导出的数据导入到MySql数据库中1. 使用默认的结束符号导入到MySql数据库中:    LOAD DATA LOCAL INFILE 'd:/oracle.txt' IGNO ...

  6. 洛谷P1615 西游记公司 题解

    题目传送门 这道题题面写得非常好. 但好像并没有什么ruan用. 这道题貌似就是把时间差求出来,乘上猪八戒能偷的电脑数就好了.(注意long long) #include<bits/stdc++ ...

  7. js中post中文参数转码和解码

    作为基础知识和血淋林的教训,前端一定要记得post请求时将参数中带有中文的部分进行转码!! var str='宋宇·特弱夫斯基'; //转码: encodeURI(encodeURI(str)) ; ...

  8. 某PCBA企业应用易普优APS实现高级计划排程案例

    一.项目介绍 1.生产计划现状 某PCBA企业(以下简称A企业)的产品生产是典型的多品种.小批量.多变化的生产模式.其中产品种类有1000多种,主流的200多种,每个月数百个生产订单,分解到工序以后的 ...

  9. 记录自己在 cmd 中执行 jar 文件遇到的一些错误

    记录自己在 cmd 中执行 jar 文件遇到的一些错误 场景: 请求接口,解析接口返回的 JSON 字符串并插入到我们的数据库里面. 情况: 项目在 eclipse 中正常运行,打成 jar 包后在 ...

  10. C++快速文件输入输出

    转载请注明: 仰望高端玩家的小清新 http://www.cnblogs.com/luruiyuan/ C语言可以获得接近汇编的性能,而输入输出常常是最为耗时的过程,因此可以使用 C 语言中的 fre ...