背景
为什么想写这一篇文章呢?做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 签名进行验证