android和nodejs搭建一个应用


背景

为什么想写这一篇文章呢?做android的开发也有两年的时间了,就想把以前学到的一些东西记录下来。于是首先就想在github.com上开一个项目MVPDemo,将一些自己认为比较好的知识点都串联起来。

主要目的:
1、初步认识和使用MVP、dagger2和rxJava2
2、使用对称和非对称加密加强前端与后台的安全机制
3、前后台的socket交互实现

其中3、中的socket实现,我专门建了一个github仓库NodeTestDemo,这个仓库不仅仅实现了前端的普通接口,还提供了一个socket服务。

android端实现

1、采用了MVP架构,使用dagger2对象依赖注入框架解耦MVP的各个组件
2、界面采用了autolayout进行兼容适配,UI尺寸标准是720*1080.页面效果仿微信。
3、rxjava2、rxlifecycle2,rxbinding2等Rx系列的初级使用
4、与后台服务器接口交互使用了retrofit2,交互的数据格式为json
5、自定义retrofit2的ConverterFactory和Interceptor实现统一加解密交互的数据流程
6、事件总线eventbus3、控件注入框架butterknife、GreenDao3对象关系映射数据库的使用
7、socket的前端简单实现
8、PDF文档库android-pdf-viewer的使用
9、使用jsoup解析csdn网站的html页面获取博主的博客信息
10、接入bugly。可以使用budly跟踪异常奔溃信息和bugly基于tinker的热修复。
11、接入腾讯X5内核浏览器服务代替原生的webview
12、页面路由Arouter的初步使用
13、app端出现异常,在杀死应用前,启动异常页面并允许用户点击重启
14、Cmake的使用。可以将敏感或者需要保密的数据使用jni保护,如第三方开发者平台的appid等

后台安全数据安全交互机制

1、后台服务器使用了leancloud和nodejs搭建。nodejs服务器源码
2、android端的数据加密流程:

nodejs使用的是node-rsa模块

(1)生成RSA加解密的公钥和私钥

1
2
3
4
5
6
var rsa = require('node-rsa');
//create RSA-key
var key = new rsa({b: 1024});

console.log("私:\n" + key.exportKey('private'));
console.log("公:\n" + key.exportKey('public'));

将服务器公钥分发给前端,私钥保存好放到服务器端。

(2)后台为一个前端生成一对AppId和AppScrect。前后端各保存一份,建议在android端将它们放到JNI中保护。

AppId用于在前端参与参数签名,AppScrect用于服务器返回数据的AES加密密钥。

(3)在Android端,应用每次启动时生成用于参数AES加密的密钥。这样可以使AES加密密钥是动态变化的。

(4)、将请求参数按照key的自然顺序进行排序,构造源串。然后在源串追加AppId得到签名字符串signString,用AES密钥加密signString,得到签名sign。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/** 按照key的自然顺序进行排序,并返回 */
private Map<String, Object> getSortedMapByKey(Map<String, Object> map) {
Comparator<String> comparator = new Comparator<String>() {
@Override
public int compare(String lhs, String rhs) {
return lhs.compareTo(rhs);
}
};
Map<String, Object> treeMap = new TreeMap<>(comparator);
for (Map.Entry<String, Object> entry : map.entrySet()) {
treeMap.put(entry.getKey(), entry.getValue());
}
return treeMap;
}

/** 构造源串 */
public String getSignParamsString(Map<String, Object> map) {
//map.put("nonce", getRndStr(6 + RANDOM.nextInt(8)));
//map.put("timestamp", "" + (System.currentTimeMillis() / 1000L));
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, Object> entry : getSortedMapByKey(map).entrySet()) {
sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
}
return sb.toString();
}

/** 构造源串 */
public String getSign(Map<String, Object> map) {
String sign = getSignParamsString(map) + "appId=" + AppConfig.AppId;
return sign;
}

说明:如果要求服务器只允许一定时间范围内的请求,可以在getSignParamsString方法中添加时间戳作为接口签名的一部分,防止重放攻击。

(4)将签名sign和签名的字符串signString进行AES加密,将AES加密密钥用服务器公钥加密,后传给服务器.

1
2
3
RSAUtils.encryptByServerPublicKey(App.getApp().getAESKey());
AESUtils.encryptData(App.getApp().getAESKey(), signString);
AESUtils.encryptData(App.getApp().getAESKey(), sign);

signString为什么在前端生成呢?
为了在服务器重新生成签名字符串时,防止由于前后端开发语言的不同而产生不一致。

(5)服务器解密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function valideReqSign(req) {

var sourceSign = req.body.sign;
var signString = req.body.signString;
var key = req.body.aesKey;


if(paramUtility.isEnpty(key)
|| paramUtility.isEnpty(sourceSign)
|| paramUtility.isEnpty(signString)) {
return false;
}

//a、步骤
key = serverPrivateKey.decrypt(key, 'utf-8');
//b、步骤
signString = aesUtils.AESDec(key, signString);
//c、步骤
signString = signString + "appId=" + decAndEncConfig.getAppId();
var localSign = aesUtils.AESEnc(key, signString);
//d、步骤
if(sourceSign !== localSign) {
var resJson = {
"data": {},
"msg": "签名不正确",
"status": 205
};
if(!paramUtility.isNULL(res)) {
res.end(jsonUtil.josnObj2JsonString(resJson));
}
return false;
}
return true;
}

a、取出参数,用服务器RSA私钥解密AES密钥
b、用AES密钥解密签名和签名字符串
c、签名字符串追加分发给前端的AppScrect后,用a、得到的AES加密重新生产签名。
d、对比前端传来的签名和重新生成的签名是否一致。

(5)根据AppId找到对应的AppScrect,用AppScrect对服务器返回的结果进行AES加密。

注意:确保前后端在不同开发语言情况下,AES算法的结果是一样的。

后面会给出我用到的java和nodejs版本的RSA和AES加解密算法源码。

(6)前端从JNI中取出AppScrect对响应结果进行解密即可。

前后端加解密算法源码

java的RSA加解密算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.RSAPrivateKeySpec;
import java.security.spec.RSAPublicKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

import javax.crypto.Cipher;

public class RSAUtils {

public static final String PRIVATE_KEY = "填写自己的private ky";
private static final String PUBLIC_KEY = AppConfig.RSA_SERVER_PUBLIC_KEY_STR;

/** RSA最大加密明文大小 */
private static final int MAX_ENCRYPT_BLOCK = 117;

/** RSA最大解密密文大小 */
private static final int MAX_DECRYPT_BLOCK = 128;

/** 加密算法RSA */
private static final String KEY_ALGORITHM = "RSA";

/**
* 生成公钥和私钥
*
* @throws Exception
*
*/
public static void getKeys() throws Exception {
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
keyPairGen.initialize(1024);
KeyPair keyPair = keyPairGen.generateKeyPair();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();

String publicKeyStr = getPublicKeyStr(publicKey);
String privateKeyStr = getPrivateKeyStr(privateKey);

System.out.println("公钥\r\n" + publicKeyStr);
System.out.println("私钥\r\n" + privateKeyStr);
}

/**
* 使用模和指数生成RSA公钥
* 注意:【此代码用了默认补位方式,为RSA/None/PKCS1Padding,不同JDK默认的补位方式可能不同,如Android默认是RSA
* /None/NoPadding】
*
* @param modulus
* 模
* @param exponent
* 公钥指数
* @return
*/
public static RSAPublicKey getPublicKey(String modulus, String exponent) {
try {
BigInteger b1 = new BigInteger(modulus);
BigInteger b2 = new BigInteger(exponent);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
RSAPublicKeySpec keySpec = new RSAPublicKeySpec(b1, b2);
return (RSAPublicKey) keyFactory.generatePublic(keySpec);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

/**
* 使用模和指数生成RSA私钥
* 注意:【此代码用了默认补位方式,为RSA/None/PKCS1Padding,不同JDK默认的补位方式可能不同,如Android默认是RSA
* /None/NoPadding】
*
* @param modulus
* 模
* @param exponent
* 指数
* @return
*/
public static RSAPrivateKey getPrivateKey(String modulus, String exponent) {
try {
BigInteger b1 = new BigInteger(modulus);
BigInteger b2 = new BigInteger(exponent);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
RSAPrivateKeySpec keySpec = new RSAPrivateKeySpec(b1, b2);
return (RSAPrivateKey) keyFactory.generatePrivate(keySpec);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

public static String encryptByServerPublicKey(String data) {
try {
return RSAUtils.encryptByPublicKey(data);
} catch (Exception e) {
e.printStackTrace();
return "";
}
}

public static String decryptByClentPrivateKey(String data) {
try {
return RSAUtils.decryptByPrivateKey(data);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
//
private String schel = "RSA/ECB/OAEPWithSHA1AndMGF1Padding";
/**
* 公钥加密
*"RSA/ECB/PKCS1Padding"
* @param data
* @return
* @throws Exception
*/
private static String encryptByPublicKey(String data) throws Exception {
byte[] dataByte = data.getBytes();
byte[] keyBytes = Base64Utils.decode(PUBLIC_KEY);
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
Key publicK = keyFactory.generatePublic(x509KeySpec);
// 对数据加密
// Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.ENCRYPT_MODE, publicK);
int inputLen = dataByte.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
byte[] cache;
int i = 0;
// 对数据分段加密
while (inputLen - offSet > 0) {
if (inputLen - offSet > MAX_ENCRYPT_BLOCK) {
cache = cipher.doFinal(dataByte, offSet, MAX_ENCRYPT_BLOCK);
} else {
cache = cipher.doFinal(dataByte, offSet, inputLen - offSet);
}
out.write(cache, 0, cache.length);
i++;
offSet = i * MAX_ENCRYPT_BLOCK;
}
byte[] encryptedData = out.toByteArray();
out.close();
return Base64Utils.encode(encryptedData);
}

/**
* 私钥解密
*
* @param data
* @return*
* @throws Exception
*/
private static String decryptByPrivateKey(String data) throws Exception {
byte[] encryptedData = Base64Utils.decode(data);
byte[] keyBytes = Base64Utils.decode(PRIVATE_KEY);
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM, "BC");
Key privateK = keyFactory.generatePrivate(pkcs8KeySpec);
// Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");

cipher.init(Cipher.DECRYPT_MODE, privateK);
int inputLen = encryptedData.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
byte[] cache;
int i = 0;
// 对数据分段解密
while (inputLen - offSet > 0) {
if (inputLen - offSet > MAX_DECRYPT_BLOCK) {
cache = cipher
.doFinal(encryptedData, offSet, MAX_DECRYPT_BLOCK);
} else {
cache = cipher
.doFinal(encryptedData, offSet, inputLen - offSet);
}
out.write(cache, 0, cache.length);
i++;
offSet = i * MAX_DECRYPT_BLOCK;
}
byte[] decryptedData = out.toByteArray();
out.close();
return new String(decryptedData);
}

/**
* 获取模数和密钥
*
* @return
*/
public static Map<String, String> getModulusAndKeys() {

Map<String, String> map = new HashMap<String, String>();

try {
InputStream in = RSAUtils.class
.getResourceAsStream("/rsa.properties");
Properties prop = new Properties();
prop.load(in);

String modulus = prop.getProperty("modulus");
String publicKey = prop.getProperty("publicKey");
String privateKey = prop.getProperty("privateKey");

in.close();

map.put("modulus", modulus);
map.put("publicKey", publicKey);
map.put("privateKey", privateKey);

} catch (IOException e) {
e.printStackTrace();
}

return map;
}

/**
* 从字符串中加载公钥
*
* @param publicKeyStr
* 公钥数据字符串
* @throws Exception
* 加载公钥时产生的异常
*/
public static PublicKey loadPublicKey(String publicKeyStr) throws Exception {
try {
byte[] buffer = Base64Utils.decode(publicKeyStr);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(buffer);
return (RSAPublicKey) keyFactory.generatePublic(keySpec);
} catch (NoSuchAlgorithmException e) {
throw new Exception("无此算法");
} catch (InvalidKeySpecException e) {
throw new Exception("公钥非法");
} catch (NullPointerException e) {
throw new Exception("公钥数据为空");
}
}

/**
* 从字符串中加载私钥<br>
* 加载时使用的是PKCS8EncodedKeySpec(PKCS#8编码的Key指令)。
*
* @param privateKeyStr
* @return
* @throws Exception
*/
public static PrivateKey loadPrivateKey(String privateKeyStr)
throws Exception {
try {
byte[] buffer = Base64Utils.decode(privateKeyStr);
// X509EncodedKeySpec keySpec = new X509EncodedKeySpec(buffer);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(buffer);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return (RSAPrivateKey) keyFactory.generatePrivate(keySpec);
} catch (NoSuchAlgorithmException e) {
throw new Exception("无此算法");
} catch (InvalidKeySpecException e) {
throw new Exception("私钥非法");
} catch (NullPointerException e) {
throw new Exception("私钥数据为空");
}
}

public static String getPrivateKeyStr(PrivateKey privateKey)
throws Exception {
return new String(Base64Utils.encode(privateKey.getEncoded()));
}

public static String getPublicKeyStr(PublicKey publicKey) throws Exception {
return new String(Base64Utils.encode(publicKey.getEncoded()));
}

public static void main(String[] args) throws Exception {
getKeys();
}
}

java的AES加解密算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
import java.util.UUID;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

/**
* AES工具类,密钥必须是16位字符串
*/
public class AESUtils {

/**偏移量,必须是16位字符串*/
private static final String IV_STRING = "16-Bytes--String";

/**
* 默认的密钥
*/
public static final String DEFAULT_KEY = "1bd83b249a414036";

/**
* 产生随机密钥(这里产生密钥必须是16位)
*/
public static String generateKey() {
String key = UUID.randomUUID().toString();
key = key.replace("-", "").substring(0, 16);// 替换掉-号
return key;
}

/**
* 加密
* @param key
* @param content
* @return
*/
public static String encryptData(String key, String content) {
byte[] encryptedBytes = new byte[0];
try {
byte[] byteContent = content.getBytes("UTF-8");
// 注意,为了能与 iOS 统一
// 这里的 key 不可以使用 KeyGenerator、SecureRandom、SecretKey 生成
byte[] enCodeFormat = key.getBytes();
SecretKeySpec secretKeySpec = new SecretKeySpec(enCodeFormat, "AES");
byte[] initParam = IV_STRING.getBytes();
IvParameterSpec ivParameterSpec = new IvParameterSpec(initParam);
// 指定加密的算法、工作模式和填充方式
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
encryptedBytes = cipher.doFinal(byteContent);
// 同样对加密后数据进行 base64 编码
return Base64Utils.encode(encryptedBytes);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

/**
* 解密
* @param key
* @param content
* @return
*/
public static String decryptData(String key, String content) {
try {
// base64 解码
byte[] encryptedBytes = Base64Utils.decode(content);
byte[] enCodeFormat = key.getBytes();
SecretKeySpec secretKey = new SecretKeySpec(enCodeFormat, "AES");
byte[] initParam = IV_STRING.getBytes();
IvParameterSpec ivParameterSpec = new IvParameterSpec(initParam);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec);
byte[] result = cipher.doFinal(encryptedBytes);
return new String(result, "UTF-8");
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

public static void main(String[] args) {
String plainText = AESUtils.decryptData("F431E6FF9051DA07", "q8jHYk6LSbwC2K4zmr/wRZo8mlH0VdMzPEcAzQadTCpSrPQ/ZnTmuIvQxiLOnUXu");
System.out.println("aes加密后: " + plainText);
}

}

node.js的RSA加解密算法

使用”node-rsa”: “^0.4.2”,模块

node.js的AES加解密算法

AES算法:aes.js

进一步改进,关注:
JIN的签名验证

JNI_OnLoad() 方法对 APK 签名进行验证