客户端和服务端基于RSA非对称加密问题——关键接口防刷
业务需求:为了防止业务中关键接口被刷(对于网赚类业务,提现和收益类接口属于关键接口),客户端和服务端采用非对称加密进行安全校验。
思路:1、为不影响产品使用体验,仅对关键接口(关键接口一般非频繁操作)进行加密
2、将时间戳作为加密项之一,防止同一个签名可以使用多次
3、服务端对定义的关键接口请求做校验,限制一个签名只能使用一次,并做签名合法性校验
流程:1、服务端生成秘钥对,将公钥给客户端,本地保存私钥
2、客户端使用公钥对数据进行加密(**+timestamp*),前端请求的话通过事件向客户端拿签名
3、服务端对前端/客户端请求中的签名进行校验
客户端代码:Android端kotlin
1 object RSA { 2 val charset: Charset = Charset.forName("UTF-8") 3 const val KEY_SIZE = 1024 4 private const val KEY_ALGORITHM = "RSA/ECB/PKCS1Padding" 5 const val SIGNATURE_ALGORITHM = "SHA256withRSA" 6 private const val PUBLIC_KEY = "******************************************" 7 private const val PRIVATE_KEY = "123123" 8 9 @Throws(java.lang.Exception::class) 10 fun generateKeyPair(): KeyPair? { 11 val generator = KeyPairGenerator.getInstance("RSA") 12 generator.initialize(2048, SecureRandom()) 13 return generator.generateKeyPair() 14 } 15 16 @Throws(java.lang.Exception::class) 17 fun string2PrivateKey(priStr: String): PrivateKey? { 22 val keyBytes: ByteArray = Base64.decode(priStr, Base64.NO_WRAP) 23 val pkcs8EncodedKeySpec = PKCS8EncodedKeySpec(keyBytes) 24 val kf = KeyFactory.getInstance("RSA") 25 return kf.generatePrivate(pkcs8EncodedKeySpec) 26 } 27 43 @Throws(java.lang.Exception::class) 44 fun string2PublicKey(pubStr: String): PublicKey? { 45 val keyBytes: ByteArray = Base64.decode(pubStr, Base64.NO_WRAP) 46 val keySpec = X509EncodedKeySpec(keyBytes) 47 val keyFactory = KeyFactory.getInstance("RSA") 48 return keyFactory.generatePublic(keySpec) 49 } 50 51 /** 52 * 加密 53 */ 54 @Throws(java.lang.Exception::class) 55 fun encrypt(data: String): String? { 56 return encrypt(data, string2PublicKey(PUBLIC_KEY)) 57 } 58 59 @Throws(java.lang.Exception::class) 60 fun encrypt(plainText: String, publicKey: PublicKey?): String? { 61 val encryptCipher = Cipher.getInstance(KEY_ALGORITHM) 62 encryptCipher.init(Cipher.ENCRYPT_MODE, publicKey) 63 val cipherText = encryptCipher.doFinal(plainText.toByteArray(StandardCharsets.UTF_8)) 64 return Base64.encodeToString(cipherText, Base64.NO_WRAP) 65 } 66 67 @Throws(java.lang.Exception::class) 68 fun decrypt(cipherText: String?): String? { 69 return decrypt(cipherText, string2PrivateKey(PRIVATE_KEY)) 70 } 71 72 @Throws(java.lang.Exception::class) 73 fun decrypt(cipherText: String?, privateKey: PrivateKey?): String? { 74 val bytes = Base64.decode(cipherText?.toByteArray(), Base64.NO_WRAP) 75 val decriptCipher = Cipher.getInstance(KEY_ALGORITHM) 76 decriptCipher.init(Cipher.DECRYPT_MODE, privateKey) 77 Log.debug(TAG, decriptCipher.provider.name) 78 return String(decriptCipher.doFinal(bytes), StandardCharsets.UTF_8) 79 } 80 81 @Throws(java.lang.Exception::class) 82 fun sign(plainText: String, privateKey: PrivateKey?): String? { 83 val privateSignature = Signature.getInstance(SIGNATURE_ALGORITHM) 84 privateSignature.initSign(privateKey) 85 privateSignature.update(plainText.toByteArray(StandardCharsets.UTF_8)) 86 val signature = privateSignature.sign() 87 return Base64.encodeToString(signature, Base64.NO_WRAP) 88 } 89 90 @Throws(java.lang.Exception::class) 91 fun verify(plainText: String, signature: String?, publicKey: PublicKey?): Boolean { 92 val publicSignature = Signature.getInstance(SIGNATURE_ALGORITHM) 93 publicSignature.initVerify(publicKey) 94 publicSignature.update(plainText.toByteArray(StandardCharsets.UTF_8)) 95 val signatureBytes = Base64.decode(signature, Base64.NO_WRAP) 96 return publicSignature.verify(signatureBytes) 97 } 98 }
服务端:java 1 public class RSAUtil { 2
3 private static String privateKey = "***************"; 4 5 @Value("${check.sign.uri:#}") 6 private String checkSignUri; 7 8 @Autowired 9 @Qualifier("shua-kxctJedisClusterClient") 10 private JedisClusterClient jedisClusterClient; 11 12 //生成秘钥对 13 // public static KeyPair getKeyPair() throws Exception { 14 // KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); 15 // keyPairGenerator.initialize(512); 16 // KeyPair keyPair = keyPairGenerator.generateKeyPair(); 17 // return keyPair; 18 // } 19 20 // //获取公钥(Base64编码) 21 // public static String getPublicKey(KeyPair keyPair){ 22 // PublicKey publicKey = keyPair.getPublic(); 23 // byte[] bytes = publicKey.getEncoded(); 24 // return byte2Base64(bytes); 25 // } 26 // 27 // //获取私钥(Base64编码) 28 // public static String getPrivateKey(KeyPair keyPair){ 29 // PrivateKey privateKey = keyPair.getPrivate(); 30 // byte[] bytes = privateKey.getEncoded(); 31 // return byte2Base64(bytes); 32 // } 33 34 //将Base64编码后的私钥转换成PrivateKey对象 35 private PrivateKey string2PrivateKey(String priStr) throws Exception{ 36 byte[] keyBytes = base642Byte(priStr); 37 PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); 38 KeyFactory keyFactory = KeyFactory.getInstance("RSA"); 39 PrivateKey privateKey = keyFactory.generatePrivate(keySpec); 40 return privateKey; 41 } 42 43 public boolean checkSign(long userId,String sign,String uri){ 44 /**核心校验算法**/
70 } 71 72 private String decrypt(String cipherText, PrivateKey privateKey) throws Exception { 73 byte[] bytes = Base64.getDecoder().decode(cipherText); 74 75 Cipher decriptCipher = Cipher.getInstance("RSA"); 76 decriptCipher.init(Cipher.DECRYPT_MODE, privateKey); 77 78 return new String(decriptCipher.doFinal(bytes), "UTF-8"); 79 } 80 81 //Base64编码转字节数组 82 private byte[] base642Byte(String base64Key) throws IOException { 83 BASE64Decoder decoder = new BASE64Decoder(); 84 return decoder.decodeBuffer(base64Key); 85 } 86 }
核心校验算法逻辑:
1、记录最新一次请求成功的签名和时间戳,校验本次请求中的签名是否是上一次使用过的签名
2、时间校验,为了防止恶意用户使用之前的签名交替请求,添加时间校验,也就是只有顺序的时间戳能通过校验
3、特殊情况:在弱网环境下请求延迟以及异步接口(即前端不等待服务端结果返回)情况下,顺序的时间序列会出现掉队情况(可以想象成每个请求拿着时间令牌排队,上述情况会造成掉队),这是掉队的请求就无法通过校验,所以增加允许的掉队时间跨度。(时间跨度需要根据情况设定)
注意点:在联调过程中出现服务端解密客户端加密签名时报填充方式不一致的错误,客户端在获取Cipher时一定要使用“RSA/ECB/PKCS1Padding”
上线效果:新产品上线三天后dau达到8万,被拦截的签名错误总计757条,主要集中在4个账号身上,说明对普通用户无影响,且能够拦截恶意刷接口行为。如下是拦截日志分布,呈现时间段分布,也说明是被个别账号在某时间点集中刷接口。
总结:没有最好的校验算法,适合自己才最重要,多根据具体业务去思考才能更好的提高。