天天看点

android平台接入服务器总结(二)腾讯qq应用宝接入

腾讯开放平台的接入是非常麻烦的, open.qq.com,腾讯开放平台的文档很多很杂,社交功能的api接口也很多还有。我现在只接了他的登录跟支付。

一、登录。

登录相对来讲还是比较简单的,首先前端sdk要正确接入获取access_token  跟 openid ,然后需要一个https  方式的get请求来取得进一步的信息。

url :https://graph.qq.com/user/get_simple_userinfo?oauth_consumer_key=%s&access_token=%s&openid=%s&clientip=&oauth_version=2.a&scope=all  

填写好自己应用的所有内容。https协议的java实现

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Map;
import java.util.Map.Entry;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSession;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


public class MyhttpService {
    private  int read_time_out = 10000;
    private Logger logger = LoggerFactory.getLogger(MyhttpService.class);
    
    public MyhttpService() {
        super();
    }
    
    public MyhttpService(int time_out) {
        read_time_out = time_out;
    }
    
    public String doPost(String url, Map<String,String> params){
        StringBuilder postData = new StringBuilder();
        for(Entry<String,String> entry:params.entrySet()){
            if(postData.length()!=0){
                postData.append("&");
            }
            postData.append(entry.getKey()).append("=").append(entry.getValue());
        }
        return service(false, url, postData.toString(), "POST", null);
    }
    public String doPost(String url, Map<String,String> params,Map<String,String> headers){
        StringBuilder postData = new StringBuilder();
        for(Entry<String,String> entry:params.entrySet()){
            if(postData.length()!=0){
                postData.append("&");
            }
            postData.append(entry.getKey()).append("=").append(entry.getValue());
        }
        return service(false, url, postData.toString(), "POST", headers);
    }
    public String doPost(String url,String body){
        return service(false, url, body, "POST", null);
    }
    
    public String doPost(String url, String postData, Map<String,String> headers){
        return service(false, url, postData, "POST", headers);
    }
    
    public String doGet(String url, Map<String,String> headers){
        return service(false, url, null, "GET", headers);
    }
    
    public String doGet(String url){
        return service(false, url, null, "GET", null);
    }
    
    public String doHttpsPost(String url, String postData) {
        return service(true, url, postData, "POST",null);
    }
    public String doHttpsPost(String url, Map<String,String> params){
        return  doHttpsPost(url,params,null);
    }
    
    public String doHttpsPost(String url, Map<String,String> params,Map<String,String> headers){
        StringBuilder postData = new StringBuilder();
        for(Entry<String,String> entry:params.entrySet()){
            if(postData.length()!=0){
                postData.append("&");
            }
            postData.append(entry.getKey()).append("=").append(entry.getValue());
        }
        return service(true, url, postData.toString(), "POST", headers);
    }
    public String doHttpsGet(String url) {
        return service(true, url, null, "GET",null);
    }
    
    private String service(boolean isHttps, String url, String postData, String method, Map<String,String> headers){
        
        HttpURLConnection conn = null;
        try {
            boolean doOutput = postData != null && postData.equals("");
            conn = isHttps ? createHttpsConn(url, method, doOutput) : createHttpConn(url, method, doOutput);

            fillProperties(conn, headers);

            if(doOutput) writeMsg(conn, postData);

            String msg = readMsg(conn);

            logger.debug(msg);

            return msg;
        } catch (Exception ex) {
            logger.error(ex.getMessage(), ex);
        } finally {
            if (conn != null) {
                conn.disconnect();
                conn = null;
            }
        }
        return null;
    }

    private HttpURLConnection createHttpConn(String url, String method, boolean doOutput) throws IOException {
        URL dataUrl = new URL(url);
        HttpURLConnection conn = (HttpURLConnection) dataUrl.openConnection();
        conn.setReadTimeout(read_time_out);
        conn.setRequestMethod(method);
        conn.setDoOutput(doOutput);
        conn.setDoInput(true);
        return conn;
    }
    
    public static void main(String[] args) {
//      System.out.println(DigestUtils.md5DigestAsHex("19a98d31-4652-4b94-b7cd-129e8ddaliji11899CNY68appstoreQY7road-16-WAN-0668ddddSHEN-2535-7ROAD-shenqug-lovedede77".getBytes()));
    }

    private String readMsg(HttpURLConnection conn) throws IOException {
        return readMsg(conn, "UTF-8");
    }

    private String readMsg(HttpURLConnection conn, String charSet) throws IOException {
        BufferedReader reader = null;
        try{
            reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), charSet));
            StringBuilder sb = new StringBuilder();

            String line = null;
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }

            return sb.toString();
        } finally {
            if(reader != null){
                reader.close();
            }
        }
    }

    private void writeMsg(HttpURLConnection conn, String postData) throws IOException {
        DataOutputStream dos = new DataOutputStream(conn.getOutputStream());
        dos.write(postData.getBytes());
        dos.flush();
        dos.close();
    }

    private void fillProperties(HttpURLConnection conn, Map<String,String> params) {
        if(params == null||params.isEmpty()){
            return;
        }

        for (Entry<String,String> entry: params.entrySet()) {
            conn.addRequestProperty(entry.getKey(), entry.getValue());
        }
    }
    
    
    public String httpsPost(String url, String postData) {
        HttpURLConnection conn = null;
        try {
            boolean doOutput = (postData != null && postData.equals(""));//!Strings.isNullOrEmpty(postData);
            conn = createHttpsConn(url, "POST", doOutput);
            if (doOutput)
                writeMsg(conn, postData);

            return readMsg(conn);
        } catch (Exception ex) {
            // ingore
            // just print out
            logger.error(ex.getMessage(), ex);
        } finally {
            if (conn != null) {
                conn.disconnect();
                conn = null;
            }
        }
        return null;
    }
    

    private HttpURLConnection createHttpsConn(String url, String method, boolean doOutput) throws Exception {
        HostnameVerifier hv = new HostnameVerifier() {
            public boolean verify(String urlHostName, SSLSession session) {
                return true;
            }
        };

        HttpsURLConnection.setDefaultHostnameVerifier(hv);
        trustAllHttpsCertificates();

        URL dataUrl = new URL(url);
        
        HttpURLConnection conn = (HttpURLConnection) dataUrl.openConnection();
        conn.setReadTimeout(read_time_out);
        conn.setRequestMethod(method);
        conn.setDoOutput(doOutput);
        conn.setDoInput(true);
        return conn;
    }
    
    private static void trustAllHttpsCertificates() throws Exception {

        //  Create a trust manager that does not validate certificate chains:

        javax.net.ssl.TrustManager[] trustAllCerts = new javax.net.ssl.TrustManager[1];
        
        javax.net.ssl.TrustManager tm = new miTM();

        trustAllCerts[0] = tm;

        javax.net.ssl.SSLContext sc = javax.net.ssl.SSLContext.getInstance("SSL");

        sc.init(null, trustAllCerts, null);

        javax.net.ssl.HttpsURLConnection.setDefaultSSLSocketFactory(
                sc.getSocketFactory());

    }
    
    public static class miTM implements javax.net.ssl.TrustManager, javax.net.ssl.X509TrustManager {
        public java.security.cert.X509Certificate[] getAcceptedIssuers() {
            return null;
        }

        public boolean isServerTrusted(
                java.security.cert.X509Certificate[] certs) {
            return true;
        }

        public boolean isClientTrusted(
                java.security.cert.X509Certificate[] certs) {
            return true;
        }

        public void checkServerTrusted(
                java.security.cert.X509Certificate[] certs, String authType) throws
                java.security.cert.CertificateException {
            return;
        }

        public void checkClientTrusted(
                java.security.cert.X509Certificate[] certs, String authType) throws
                java.security.cert.CertificateException {
            return;
        }
    }
    /** 
     * 执行一个HTTP POST请求,返回请求响应的内容
     * @param url        请求的URL地址 
     * @param params 请求的查询参数,可以为null 
     * @return 返回请求响应的内容 
     */ 
    public static String doPostforUC(String url, String body) { 
        StringBuffer stringBuffer = new StringBuffer();
        HttpEntity entity = null;
        BufferedReader in = null;
        HttpResponse response = null;
        try {
            DefaultHttpClient httpclient = new DefaultHttpClient();
            HttpParams params = httpclient.getParams();
            HttpConnectionParams.setConnectionTimeout(params, 20000);
            HttpConnectionParams.setSoTimeout(params, 20000);
            HttpPost httppost = new HttpPost(url);
            httppost.setHeader("Content-Type", "application/x-www-form-urlencoded");

            httppost.setEntity(new ByteArrayEntity(body.getBytes("UTF-8")));
            response = httpclient.execute(httppost);
            
            entity = response.getEntity();
            in = new BufferedReader(new InputStreamReader(entity.getContent(),"UTF-8"));
            String ln;
            while ((ln = in.readLine()) != null) {
                stringBuffer.append(ln);
                stringBuffer.append("\r\n");
            }
            httpclient.getConnectionManager().shutdown();
        } catch (ClientProtocolException e) {
            e.printStackTrace();
        } catch (IOException e1) {
            e1.printStackTrace();
        } catch (IllegalStateException e2) {
            e2.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (null != in) {
                try {
                    in.close();
                    in = null;
                } catch (IOException e3) {
                    e3.printStackTrace();
                }
            }
        }
        return stringBuffer.toString();
    } 

}
           

内容的返回是json格式的,可以从里面找自己需要的内容来解析

JSONParser jsonParser = new JSONParser(JSONParser.DEFAULT_PERMISSIVE_MODE);
                    JSONObject obj;
                    obj = (JSONObject) jsonParser.parse(doHttpsGet);
                    String code = String.valueOf(obj.get("ret"));
                    if(code.equals("0")){
                        String nickName = String.valueOf(obj.get("nickname"));
           

后面就是自己服务器的逻辑了。登录相对来讲还是很简单的。

二、支付

1、腾讯的支付接口不知道是新开发的,还是涉及太多,总之非常乱,他们的开放平台的wiki上面有,四个服务,应用接入,移动接入,网站接入,腾讯云接入。因为我们是移动游戏所以,应该按照移动接入来接,但是实际上还要涉及应用接入这边的文档。只看一边的文档会发现少很多东西。按照文档接入出现问题。正题。

我手中的文档是 移动接入的sdk下载里面的

android平台接入服务器总结(二)腾讯qq应用宝接入

,腾讯的支付有两种:第一种是由腾讯来管理我们的支付,举例就是玩家充值的元宝在腾讯服务器上面,这个蛋疼的地方是,以后你所有的元宝操作都要跟腾讯交互,增加、扣除、赠送等等都要写协议去腾讯云处理。所以我们用了另外一种模式,道具购买模式。道具购买模式是直接花q点或者q币购买我们的道具,这个道具就是元宝。

2、这里有一个问题是,sdk里面自带的文档跟wiki上面的不一致,调用的接口也不是一个

android平台接入服务器总结(二)腾讯qq应用宝接入

这个对我们的影响在于后面的发货接口。发货接口的文档又再wiki上面,所以后面我们回到wiki的时候发现两份文档对不上。sdk文档包括腾讯托管跟我们自己管理元宝两种,第一种因为接口多,所以大部分是将第一种方式的。

3、道具购买服务器需要实现两个接口。购买道具下订单接口。购买结束回调接口。

下单接口需要客户端在登录时候取得  paytoken  openkey  pf  pfkey  然后按照文档以  http  方式连接开放api就可以了。

//qq直接购买道具下单界面
	public String qq_buy_items(String appid,String sessionId ,String openid,String pay_token,String openkey ,String amount,String pf,String pfkey){
        String appkey = PlatformUtil.QQ_APPKEY;
        String apiaddress = "119.147.19.43";//qq测试地址
//        String apiaddress = "openapi.tencentyun.com";//qq正式
        //pf = "qq_m_qq-10000144-android-10000144-1111";
        //pfkey = "pfkey";
        OpenApiV3 openApiV3 = new OpenApiV3(appid, appkey, apiaddress);
        String zoneid="1";
        
        Map<String,String> params = new HashMap<String, String>();
        params.put("openid", openid);
        params.put("openkey", openkey);
        params.put("pf", pf);
        params.put("pfkey",pfkey);
        params.put("ts", String.valueOf(System.currentTimeMillis()/1000));
        params.put("pay_token", pay_token);
        params.put("zoneid", zoneid);
        params.put("appmode", "1");
        params.put("appid", appid);
        int iamount = SCUtils.calcScCount(amount+".0");
        String payitem = String.format("100*1*%s",String.valueOf(iamount));
        String goodsmeta = "元宝*元宝";
        String goodsurl = "http://dragon.dl.hoolaigames.com/other/CH.png";
        String app_metadata = String.format("%s-%s-",sessionId,String.valueOf(amount));
        params.put("payitem", payitem);
        params.put("goodsmeta", goodsmeta);
        params.put("goodsurl", goodsurl);
        params.put("app_metadata",app_metadata);    //这个在最终透传时候会增加腾讯的内容,*qdqd*qq 告诉我们是用什么方式支付的
//        params.put("qq_m_qq",String.format("%s,%s,%s", appid,openid,openkey) );
        try {
            Map<String,String> cookies = new HashMap<String, String>();
            cookies.put("session_id", SnsSigCheck.encodeUrl("openid"));
            cookies.put("session_type", SnsSigCheck.encodeUrl("kp_actoken"));
            cookies.put("org_loc ",SnsSigCheck.encodeUrl("/mpay/buy_goods_m"));
            String api = openApiV3.api("/mpay/buy_goods_m", params,null ,"http");
            return api;
        } catch (OpensnsException e) {
            log.error("openApiV3.api invoke failed",e);
            return "error";
        }
    }
           

其中的  OpenApiV3 其实可以从开放平台下载,是   http://wiki.open.qq.com/wiki/SDK%E4%B8%8B%E8%BD%BD  里面其实是一些验证以及http的访问。实际接入时候可以下载一个最新的看看。返回值也是一个json

JSONParser jsonParser = new JSONParser(JSONParser.DEFAULT_PERMISSIVE_MODE);
                    JSONObject obj;
                    obj = (JSONObject) jsonParser.parse(payurl);
                    int ret = (Integer) obj.get("ret");
                    String msg = (String) obj.get("msg");
                    String token_id = (String) obj.get("token");
                    String url_params = (String) obj.get("url_params");
           

关于返回值里面参数,文档跟实际的返回有些出入,不一致,token 文档中写的是token_id  但实际返回的是token,这个可以实际debug看下再接收参数。得到的这些值需要发送给前端的sdk,前端的sdk会用这个返回的url 处理剩下的逻辑。

4、支付回调。客户端拿到刚才的url 会回传给腾讯,然后腾讯会调用我们在后台配置的回调接口,来通知支付结果,同时我们也要处理道具发放逻辑。这里有一个非常困难的问题 https协议的证书问题。 腾讯的证书最变态的一点是绑定ip地址,当然也是为了安全考虑。腾讯的后台我没有登录,但是应该是配置回调的ip地址,填写回调url  然后腾讯会生成一个绑定ip地址的证书,你需要安装这个证书在那台服务器上面,

发货URL用来给腾讯计费后台回调。用户付费成功后,腾讯计费后台将回调该URL给用户发货。在9001端口后可以是一个cgi或者php的路径。

hosting应用on CVM(即应用部署在腾讯CVM服务器上):

-发货URL只需HTTP协议即可,不需要使用SSL安全协议。

-必须使用9001端口(内网端口,需开发者主动启用,用apache iis或nginx做一个web监听,端口改成9001)。

hosting应用on CEE_V2(即应用部署在腾讯CEE_V2服务器上):

-发货URL只需HTTP协议即可,不需要使用SSL安全协议。

-必须使用9001端口(内网端口,需开发者主动启用,用apache iis或nginx做一个web监听,端口改成9001)。

-路径必须以ceecloudpay开头,即支付相关代码必须都放到应用根目录下的“ceecloudpay”目录下。

-对于CEE其发货URL的IP只能填写为10.142.11.27或者10.142.52.17(详见:CEE_V2访问云支付)。 

non-hosting应用(即应用部署在开发者自己的服务器上):

-发货URL必须使用HTTPS协议。

-必须使用443端口(外网端口)。

-必须填写发货服务器所在运营商(电信/联通)。

这是腾讯官方文档对于这个的解释,总之很麻烦的一个东西,中间遇到什么问题,建议找他们的企业支持。

5、证书的安装。

证书安装很坑爹的一个没有官方文档,官方有一个window浏览器的导入文档,没有linux的。这太无语了。

证书的安装可以安装在apache 或者 nginx 下面,我没有直接安装在tomcat下面,应该也是可以的吧,用apache或者nginx 可以做转发,转发到本地debug什么的。所以,我们用的是nginx做转发。首先腾讯后台下载一个这样的证书包。

android平台接入服务器总结(二)腾讯qq应用宝接入

这个里面带钥匙的那个需要密码,密码在readme里面,但是其实linux下面并没有用到,这个我估计是原始的密钥文件,可以和那个key生成 crt 文件,但是这里已经是生成好了的 crt 文件所以还是直接用比较好,先给第一个最长的那个起个别的名字。然后上传到 nginx 服务器的 conf 目录下面 ,nginx 在安装服务的时候应该是默认为支持https的 ssl 的,所以一般是不需要重新编译的,如果需要重新编译,可以去网上找找相关资料。如果你的  nginx  支持,那么就剩下一步,修改配置文件。 同样是conf目录下面的  nginx.conf   

server {
        listen       443;
        server_name  xxxxxxx;
        ssl                       on;
        ssl_certificate           oem.crt;
        ssl_certificate_key       oem.key;
        ssl_verify_client         off;
        ssl_session_timeout       5m;
        ssl_protocols             SSLv2 SSLv3 TLSv1;
        ssl_ciphers               ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP;
        ssl_prefer_server_ciphers on;
        ssl_client_certificate      ca.crt;
        ssl_verify_depth            1;

        location ~  ^/xxxxxr/* {
                proxy_pass http://xxxxxx3;
                index  index.jsp index.html index.htm;
                proxy_set_header        Host $host;
                proxy_set_header  X-Real-IP  $remote_addr;
                proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
        }
    }
           

里面的

ssl_client_certificate
           
ssl_certificate
           

对应证书里面的名字,修改完成后记得reload    ./nginx -s reload  一下应该就生效了,我是做了转发本地处理的,当然也可以转发到任意服务器或者本机。如果下订单成功但是收不到回调,多半是这个证书的问题,可以看 nginx的log日志看看有没有访问到。如果没有80%都是证书的问题,询问下腾讯的支持让他们帮你查下日志吧,不过等他们反馈,估计你已经找到原因了。

6、支付回调的验证,当你终于能收到回调了,恭喜你你就要成功了。

对于支付回调的处理,其实很简单,但是腾讯的就很蛋疼。这就是上面说的蛋疼的问题,没有文档。sdk里面的文档说去看 wiki ,wiki里面的文档貌似不是这一版的,而且sdk文档里面的连接还是去 wiki 的主页,哎。  这里忍不住吐槽太多太乱,大家看上去都差不多,我哪知道是我需要的接口。最终我看到这个貌似像 :

http://wiki.open.qq.com/wiki/%E5%9B%9E%E8%B0%83%E5%8F%91%E8%B4%A7URL%E7%9A%84%E5%8D%8F%E8%AE%AE%E8%AF%B4%E6%98%8E_V3

这个文档上面的参数回调,大部分都是正确的。注意是大部分,因为收到的所有参数都要参与 HmacSHA1 签名,所以一个参数错误就悲剧了,你都不知道去哪里找,贴一下我最终的回调处理。

//qq支付回调接口,根据http://wiki.open.qq.com/wiki/%E5%9B%9E%E8%B0%83%E5%8F%91%E8%B4%A7URL%E7%9A%84%E5%8D%8F%E8%AE%AE%E8%AF%B4%E6%98%8E_V3   编写
    protected void processRequest(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException  {
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        Map<String, Object> obj = new HashMap<String, Object>();
        try {
            String openid = request.getParameter("openid");     //根据APPID以及QQ号码生成,即不同的appid下,同一个QQ号生成的OpenID是不一样的。
            String appid = request.getParameter("appid");       //应用的唯一ID。可以通过appid查找APP基本信息。
            String ts = request.getParameter("ts");             //linux时间戳。 注意开发者的机器时间与腾讯计费开放平台的时间相差不能超过15分钟。
            String payitem = request.getParameter("payitem");   //接收标准格式为ID*price*num   G001*10*1
            String token = request.getParameter("token");       //应用调用v3/pay/buy_goods接口成功返回的交易token
            String billno = request.getParameter("billno");     //支付流水号(64个字符长度。该字段和openid合起来是唯一的)。
            String version = request.getParameter("version");   //协议版本 号,由于基于V3版OpenAPI,这里一定返回“v3”。
            String zoneid = request.getParameter("zoneid");     //在支付营销分区配置说明页面,配置的分区ID即为这里的“zoneid”。  如果应用不分区,则为0。
            String providetype = request.getParameter("providetype");//发货类型   0表示道具购买,1表示营销活动中的道具赠送,2表示交叉营销任务集市中的奖励发放。
            //Q点/Q币消耗金额或财付通游戏子账户的扣款金额。可以为空 若传递空值或不传本参数则表示未使用Q点/Q币/财付通游戏子账户。注意,这里以0.1Q点为单位。即如果总金额为18Q点,则这里显示的数字是180。
            String amt = request.getParameter("amt");           
            String payamt_coins = request.getParameter("payamt_coins");//扣取的游戏币总数,单位为Q点。
            String pubacct_payamt_coins = request.getParameter("pubacct_payamt_coins");//扣取的抵用券总金额,单位为Q点。
            String appmeta = request.getParameter("appmeta");
            String clientver = request.getParameter("clientver");
            String sig = request.getParameter("sig");
            
            String url = "/xxx/xxxx";
            
            Map<String, String> params = createCallbackParamsMap(openid, appid, ts, payitem, token, billno, version, zoneid,
                    providetype, amt, payamt_coins, pubacct_payamt_coins, appmeta,clientver);
            if(SnsSigCheck.verifySig(request.getMethod(), url,params, PlatformUtil.QQ_APPKEY+"&", sig)){
                
                if(ok){
                    
                }else{
                    
                    obj.put("ret", 0);
                    obj.put("msg", "ok");
                }
            }else{
                log.info("qqPayCallback SnsSigCheck fail.");
                obj.put("ret", -5);
                obj.put("msg", "签名错误");
            }
                String resp = JSONObject.toJSONString(obj);
                out.println(resp);
        } catch (SQLException | DbException | ProtocolException | NumberFormatException | OpensnsException e) {
            e.printStackTrace();
        } finally {
            out.close();
        }
           

中间标红的地方都是有问题的地方,都是坑。首先 

pubacct_payamt_coins
           

是有可能传空的,因为你没有用抵用券对吧,但是记住这个也需要加入签名。   clientver  神坑。我最终也没再文档或者哪里找到这个参数为什么给我传过来,但是你就是传过来了,而且你还必须接收,必须加入签名中去,也许我水平太菜,反正我是没找到这个参数在那个文档上面写了。

android平台接入服务器总结(二)腾讯qq应用宝接入

createCallbackParamsMap  字面意思就是把参数弄到 map里面 

SnsSigCheck.verifySig  这也是上面下载的那个工具项目中自带的功能,其实就是一个   HmacSHA1  的 utf 格式的签名。可以下载,有兴趣的也可以自己写写。

  (三)总结

腾讯的支付接口应该做的不难,困难在于没有一个明确的文档。

继续阅读