Android使用Bmob移動後端雲Restful API需要注意的問題
如果你自己想做一個用戶端玩玩,但是又不想去搭建後台伺服器,顯然Bmob移動後端雲是你的最佳選擇。官方地址見bmob,他提供了Android的sdk,同樣也提供了Restful Api,但是個人建議Restful Api還是不適合直接在用戶端使用,畢竟會暴露一下一些key的資訊,但是本篇文章就是在android中使用它的restful api,原因嘛很簡單,我想網路層自己控制,不想用它提供的android sdk,對於安全方面,同樣給出了這種情況的解決方案。
建立應用
編寫代碼
我們使用OkHttp,還需要用到Gson,增加依賴
compile 'com.squareup.okhttp:okhttp:2.5.0' compile 'com.google.code.gson:gson:2.3.1'
增加網路存取權限
data-snippet-id=ext.a07a4cf67b90ad77df941de12b11b97b data-snippet-saved=false data-csrftoken=wlzdJGob-PUBlORu9Wxf4eDhkE0NYjNzHse4 data-codota-status=done>
編寫一個網路的請求,插入一條資料
實體類
public class Person { private String name; private String address; private int age; public Person(String name, String address, int age) { this.name = name; this.address = address; this.age = age; } public Person() { } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return Person{ + name=' + name + ''' + , address=' + address + ''' + , age= + age + '}'; }}
進行請求,這部分代碼是java平台的,在android上你需要開啟一個線程。
private static final String URL_INSERT =https://api.bmob.cn/1/classes/Person;private static final String APPLICATION_ID=8dcb9fee2f******14ab19e7dfd9d;private static final String API_KEY=aebe3b71c9b2***********430ac2de560b1;private static final MediaType JSON = MediaType.parse(application/json; charset=utf-8);private static final Gson gson=new Gson();private static final OkHttpClient client=new OkHttpClient();Person person=new Person(張三,杭州,20);RequestBody body = RequestBody.create(JSON, gson.toJson(person));Request insert=new Request.Builder() .url(URL_INSERT) .addHeader(Content-Type,application/json) .addHeader(X-Bmob-Application-Id, APPLICATION_ID) .addHeader(X-Bmob-REST-API-Key,API_KEY) .post(body) .build();try { Response execute = client.newCall(insert).execute(); System.out.println(execute.body().string());} catch (IOException e) { e.printStackTrace();}
注意要求標頭裡面的幾個參數,必須設定。
這時候如果你進行請求,你會發現會報一個異常
javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
原來是https請求,我們需要獲得認證。
當然這時候你有兩個選擇,一個是信任所有認證。
public class MyX509TrustManager implements X509TrustManager { public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { } public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { } public X509Certificate[] getAcceptedIssuers() { return null; }}try { // 建立SSLContext對象,並使用我們指定的信任管理器初始化 TrustManager[] tm = { new MyX509TrustManager() }; SSLContext sslContext = SSLContext.getInstance(SSL, SunJSSE); sslContext.init(null, tm, new java.security.SecureRandom()); // 從上述SSLContext對象中得到SSLSocketFactory對象 SSLSocketFactory ssf = sslContext.getSocketFactory(); client.setSslSocketFactory(ssf);} catch (NoSuchAlgorithmException e) { e.printStackTrace();} catch (NoSuchProviderException e) { e.printStackTrace();} catch (KeyManagementException e) { e.printStackTrace();}
但是這種方法有安全隱患,我們還是使用認證吧。
首先用Chrome開啟https://api.bmob.cn/,然後點選連結左邊的鎖的圖形,切到串連項,點擊認證資訊,如
然後將認證匯出即可,之後一直下一步即可。
這裡假設匯出的認證名字為bmob.cer,將其放到assets目錄下。
然後編寫一個https驗證的工具類。
/** * User:lizhangqu(513163535@qq.com) * Date:2015-09-23 * Time: 17:45 */public class SSLUtil { //使用命令keytool -printcert -rfc -file srca.cer 匯出認證為字串,然後將字串轉換為輸入資料流,如果使用的是okhttp可以直接使用new Buffer().writeUtf8(s).inputStream() /** * 返回SSLSocketFactory * * @param certificates 認證的輸入資料流 * @return SSLSocketFactory */ public static SSLSocketFactory getSSLSocketFactory(InputStream... certificates) { return getSSLSocketFactory(null,certificates); } /** * 雙向認證 * @param keyManagers KeyManager[] * @param certificates 認證的輸入資料流 * @return SSLSocketFactory */ public static SSLSocketFactory getSSLSocketFactory(KeyManager[] keyManagers, InputStream... certificates) { try { CertificateFactory certificateFactory = CertificateFactory.getInstance(X.509); KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); keyStore.load(null); int index = 0; for (InputStream certificate : certificates) { String certificateAlias = Integer.toString(index++); keyStore.setCertificateEntry(certificateAlias, certificateFactory.generateCertificate(certificate)); try { if (certificate != null) certificate.close(); } catch (IOException e) { } } SSLContext sslContext = SSLContext.getInstance(TLS); TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); trustManagerFactory.init(keyStore); sslContext.init(keyManagers, trustManagerFactory.getTrustManagers(), new SecureRandom()); SSLSocketFactory socketFactory = sslContext.getSocketFactory(); return socketFactory; } catch (Exception e) { e.printStackTrace(); } return null; } /** * 獲得雙向認證所需的參數 * @param bks bks認證的輸入資料流 * @param keystorePass 秘鑰 * @return KeyManager[]對象 */ public static KeyManager[] getKeyManagers(InputStream bks, String keystorePass) { KeyStore clientKeyStore = null; try { clientKeyStore = KeyStore.getInstance(BKS); clientKeyStore.load(bks, keystorePass.toCharArray()); KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); keyManagerFactory.init(clientKeyStore, keystorePass.toCharArray()); KeyManager[] keyManagers = keyManagerFactory.getKeyManagers(); return keyManagers; } catch (KeyStoreException e) { e.printStackTrace(); } catch (UnrecoverableKeyException e) { e.printStackTrace(); } catch (CertificateException e) { e.printStackTrace(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return null; }}
如果你不想使用檔案,則你可以匯出認證的內容,使用命令進行匯出
keytool -printcert -rfc -file bmob.cer
然後將其賦值給一個字串
private static final String CERT=-----BEGIN CERTIFICATE----- + MIIGLjCCBRagAwIBAgIDFCb6MA0GCSqGSIb3DQEBCwUAMIGMMQswCQYDVQQGEwJJTDEWMBQGA1UE + ChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2ln + bmluZzE4MDYGA1UEAxMvU3RhcnRDb20gQ2xhc3MgMSBQcmltYXJ5IEludGVybWVkaWF0ZSBTZXJ2 + ZXIgQ0EwHhcNMTQxMTA3MDExNzM0WhcNMTUxMTA4MDIxMDQ2WjBJMQswCQYDVQQGEwJDTjEUMBIG + A1UEAxMLYXBpLmJtb2IuY24xJDAiBgkqhkiG9w0BCQEWFWhlc2hhb3l1ZUBmb3htYWlsLmNvbTCC + ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANCEvBFYJmhW+8iixdK0zlzwprsuytUGW5BH + ye9EEkJzGzYfVnEO/v4wC3vEvlWqkwTxY/ydnneH+yo0msAN6IEt6IA+3eO55PAlooAF8b8I2e83 + usRTK4YmooZc/2GYNk2WBXvVlMuWABMKJ/oQMXlM46gffd3Z+evbbptZ5vm+QEWjUlw8fsTALakq + JgKsrmGSNBVngx1qnm00DL/3yfR2DZHro4CDzRp4toQV3ofcnt6Nz43Z4YkAXZr5gqxge8BZ2n8P + raQo/5wSfWoPW79Z8lPvZSZv5UIGCUAXdt0qYb3awSDsPSnMrRl03V4XmOK3RDdYDPrWMvii+YrC + /vUCAwEAAaOCAtkwggLVMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgOoMBMGA1UdJQQMMAoGCCsGAQUF + BwMBMB0GA1UdDgQWBBR8ztcEh/lE/9fxcga6p7/b+x+pUTAfBgNVHSMEGDAWgBTrQjTQmLCrn/Qb + awj3zGQu7w4sRTAfBgNVHREEGDAWggthcGkuYm1vYi5jboIHYm1vYi5jbjCCAVYGA1UdIASCAU0w + ggFJMAgGBmeBDAECATCCATsGCysGAQQBgbU3AQIDMIIBKjAuBggrBgEFBQcCARYiaHR0cDovL3d3 + dy5zdGFydHNzbC5jb20vcG9saWN5LnBkZjCB9wYIKwYBBQUHAgIwgeowJxYgU3RhcnRDb20gQ2Vy + dGlmaWNhdGlvbiBBdXRob3JpdHkwAwIBARqBvlRoaXMgY2VydGlmaWNhdGUgd2FzIGlzc3VlZCBh + Y2NvcmRpbmcgdG8gdGhlIENsYXNzIDEgVmFsaWRhdGlvbiByZXF1aXJlbWVudHMgb2YgdGhlIFN0 + YXJ0Q29tIENBIHBvbGljeSwgcmVsaWFuY2Ugb25seSBmb3IgdGhlIGludGVuZGVkIHB1cnBvc2Ug + aW4gY29tcGxpYW5jZSBvZiB0aGUgcmVseWluZyBwYXJ0eSBvYmxpZ2F0aW9ucy4wNQYDVR0fBC4w + LDAqoCigJoYkaHR0cDovL2NybC5zdGFydHNzbC5jb20vY3J0MS1jcmwuY3JsMIGOBggrBgEFBQcB + AQSBgTB/MDkGCCsGAQUFBzABhi1odHRwOi8vb2NzcC5zdGFydHNzbC5jb20vc3ViL2NsYXNzMS9z + ZXJ2ZXIvY2EwQgYIKwYBBQUHMAKGNmh0dHA6Ly9haWEuc3RhcnRzc2wuY29tL2NlcnRzL3N1Yi5j + bGFzczEuc2VydmVyLmNhLmNydDAjBgNVHRIEHDAahhhodHRwOi8vd3d3LnN0YXJ0c3NsLmNvbS8w + DQYJKoZIhvcNAQELBQADggEBAF/t9Bc14BV0OwXcFf4Bs8y+p1AdbMqualCvLzjS95Z9HbPGcbRl + W76XwaM7iFE1R4mR1lGBQsacbBHOCNeZURYWGAG5c/yqhqCmWCzVJxM88AhCzkEv98uKa3IqE1zY + lOpYn4cMVqpPgg47QXqUfQlRoh21UTTORgiHEUY+JYNIlIXLoHtHVR0886+pIAq5fFrCwMHF45Df + r8tuTASazhYJUlOiGQTVv5p8Kg1wJ0ftMs9xJpThcnpEWrngmnNH/8H05rvJ9dEHkpnAU4mL46Bb + rmQe3oNoGE5EISL9KGVUMeS9wcR2kx+VmGhnAh7kjn5KuEidgfajS3XlcJ5o9t0= + -----END CERTIFICATE-----;
然後使用OkIO裡的Buffer類進行讀取並設定
SSLSocketFactory sslSocketFactory = SSLUtil.getSSLSocketFactory(new Buffer().writeUtf8(CERT).inputStream());client.setSslSocketFactory(sslSocketFactory);
當然之前我們已將將其放到assets目錄下了,就不用這麼麻煩的匯出字串,賦值等操作,我們之間使用該檔案即可
try { SSLSocketFactory sslSocketFactory = SSLUtil.getSSLSocketFactory(getAssets().open(CERT_FILENAME)); client.setSslSocketFactory(sslSocketFactory);} catch (IOException e) { e.printStackTrace();}
這時候你請求一下,就會發現可以成功請求了,並且伺服器返回了資訊
請求的問題解決了,那麼還遺留了一個重要的問題,就是如果使用Restful API,我們的APPLICATION_ID和API_KEY就直接暴露在用戶端了。有沒有一種方法可以提高一定的安全性呢,方法是有的,只不過只是相對來說安全一點,但是如果人家想搞你,那也是沒有辦法的,方法就是使用jni獲得這兩個值,由jni層返回這兩個字串。
Android Studio下ndk的開發環境搭建見Android Studio使用新的Gradle構建工具配置NDK環境,這裡不再累贅。
聲明java層方法
public class KeyUtil { static { System.loadLibrary(key); } public static native String getApplicationId(); public static native String getAPIKey();}
使用alt+enter產生jni方法,並在裡面返回這兩個值
JNIEXPORT jstring JNICALLJava_cn_edu_zafu_bmobdemo_util_KeyUtil_getApplicationId(JNIEnv *env, jclass instance) { char returnValue[]=8dcb9fe*************ab19e7dfd9d; return (*env)->NewStringUTF(env, returnValue);}JNIEXPORT jstring JNICALLJava_cn_edu_zafu_bmobdemo_util_KeyUtil_getAPIKey(JNIEnv *env, jclass instance) { char returnValue[]=aebe3b71c9b*****************e560b1; return (*env)->NewStringUTF(env, returnValue);} data-snippet-id=ext.5eb349e2ad3e9fd820357c64c10973e8 data-snippet-saved=false data-csrftoken=JNlvlUwD-f2fRHXXRHfvw_8E266uWldmU0fQ data-codota-status=done>#include JNIEXPORT jstring JNICALLJava_cn_edu_zafu_bmobdemo_util_KeyUtil_getApplicationId(JNIEnv *env, jclass instance) { char returnValue[]=8dcb9fe*************ab19e7dfd9d; return (*env)->NewStringUTF(env, returnValue);}JNIEXPORT jstring JNICALLJava_cn_edu_zafu_bmobdemo_util_KeyUtil_getAPIKey(JNIEnv *env, jclass instance) { char returnValue[]=aebe3b71c9b*****************e560b1; return (*env)->NewStringUTF(env, returnValue);}
在java層要獲得這兩個值只要使用對應的靜態方法即可。
KeyUtil.getApplicationId();KeyUtil.getAPIKey();
最終,我們的代碼也就成了這樣子
public class MainActivity extends AppCompatActivity { private static final MediaType JSON = MediaType.parse(application/json; charset=utf-8); private static final String URL =https://api.bmob.cn/1/classes/Person; private static final String CERT_FILENAME =bmob.cer; private static final Gson gson=new Gson(); private static final OkHttpClient client=new OkHttpClient(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { try { SSLSocketFactory sslSocketFactory = SSLUtil.getSSLSocketFactory(getAssets().open(CERT_FILENAME)); client.setSslSocketFactory(sslSocketFactory); } catch (IOException e) { e.printStackTrace(); } Person person=new Person(張三,杭州,20); RequestBody body = RequestBody.create(JSON, gson.toJson(person)); Request insert=new Request.Builder() .url(URL) .addHeader(Content-Type,application/json) .addHeader(X-Bmob-Application-Id, KeyUtil.getApplicationId()) .addHeader(X-Bmob-REST-API-Key,KeyUtil.getAPIKey()) .post(body) .build(); client.newCall(insert).enqueue(new Callback() { @Override public void onFailure(Request request, IOException e) { } @Override public void onResponse(Response response) throws IOException { Log.e(TAG,response.body().string()); } }); } }); }}
這種方法只能提高一定的安全性但是不能完全避免,人家只要反組譯碼你的so就能看到這兩個值,你要再加強安全性就只能在jni層進行加密解密等操作了,總之就是不要直接返回字串。
之後你在Bmob後台就能看到資料
Bmob為懶人提供了很好的後端解決方案,我們幾乎不用寫一句代碼,就可以搭建一個強大的後台服務。