腾讯开放平台的接入是非常麻烦的, 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下载里面的
,腾讯的支付有两种:第一种是由腾讯来管理我们的支付,举例就是玩家充值的元宝在腾讯服务器上面,这个蛋疼的地方是,以后你所有的元宝操作都要跟腾讯交互,增加、扣除、赠送等等都要写协议去腾讯云处理。所以我们用了另外一种模式,道具购买模式。道具购买模式是直接花q点或者q币购买我们的道具,这个道具就是元宝。
2、这里有一个问题是,sdk里面自带的文档跟wiki上面的不一致,调用的接口也不是一个
这个对我们的影响在于后面的发货接口。发货接口的文档又再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做转发。首先腾讯后台下载一个这样的证书包。
这个里面带钥匙的那个需要密码,密码在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 神坑。我最终也没再文档或者哪里找到这个参数为什么给我传过来,但是你就是传过来了,而且你还必须接收,必须加入签名中去,也许我水平太菜,反正我是没找到这个参数在那个文档上面写了。
createCallbackParamsMap 字面意思就是把参数弄到 map里面
SnsSigCheck.verifySig 这也是上面下载的那个工具项目中自带的功能,其实就是一个 HmacSHA1 的 utf 格式的签名。可以下载,有兴趣的也可以自己写写。
(三)总结
腾讯的支付接口应该做的不难,困难在于没有一个明确的文档。