移动端与服务端交互安全方案

系统流程图

验签

解决问题:

1、身份验证:是否是我规定的那个人

2、防篡改:是否被第三方劫持并篡改参数

3、防重放:是否重复请求

具体算法:

1、约定appKey,保证该调用请求是平台授权过的调用方发出的,保证请求方唯一性。

2、将appKey加入值请求参数,如:http://****?appKey=1232456&其他参数。

3、对参数进行排序(排序方法约定好即可,如:ASCII码对比),将参数名参数值拼接成字符串。

4、使用md5摘要算法获取摘要,将摘要放入请求头。

package com.zhangteng.rxhttputils.interceptor

import android.text.TextUtils
import com.google.gson.JsonParser
import com.zhangteng.rxhttputils.utils.MD5Util.md5Decode32
import okhttp3.FormBody
import okhttp3.Interceptor
import okhttp3.RequestBody
import okhttp3.Response
import okio.Buffer
import java.io.IOException
import java.nio.charset.Charset
import java.util.*
import kotlin.collections.set

/**
 * 添加签名拦截器
 * Created by Swing on 2019/10/20.
 */
class SignInterceptor(private val appKey: String) : Interceptor {

    @Throws(IOException::class)
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        val requestBuilder = request.newBuilder()
        val urlBuilder = request.url().newBuilder()
        val params: MutableMap<String, Any?> =
            TreeMap()
        if (METHOD_GET == request.method()) {
            val httpUrl = urlBuilder.build()
            val paramKeys = httpUrl.queryParameterNames()
            for (key in paramKeys) {
                val value = httpUrl.queryParameter(key)
                if (!TextUtils.isEmpty(value)) params[key] = value
            }
        } else if (METHOD_POST == request.method()) {
            if (request.body() is FormBody) {
                val formBody = request.body() as FormBody?
                for (i in 0 until formBody!!.size()) {
                    params[formBody.encodedName(i)] = formBody.encodedValue(i)
                }
            } else if (request.body() is RequestBody) {
                val requestBody = request.body()
                val buffer = Buffer()
                requestBody!!.writeTo(buffer)
                var charset = Charset.forName("UTF-8")
                val contentType = requestBody.contentType()
                if (contentType != null) {
                    charset = contentType.charset()
                }
                val paramJson =
                    buffer.readString(charset ?: Charset.defaultCharset())
                val jsonObject = JsonParser().parse(paramJson).asJsonObject
                jsonObject.entrySet().forEach {
                    val jsonElement = it.value
                    if (jsonElement != null && !jsonElement.isJsonArray && !jsonElement.isJsonObject && !jsonElement.isJsonNull) {
                        val value = jsonElement.asString
                        if (!TextUtils.isEmpty(value)) params[it.key] = value
                    }
                }
            }
        }
        val sign = StringBuilder()
        sign.append(appKey)
        for (key in params.keys) {
            sign.append(key).append(params[key])
        }
        val _timestamp = System.currentTimeMillis()
        sign.append("_timestamp").append(_timestamp)
        sign.append(appKey)
        requestBuilder.addHeader("_timestamp", _timestamp.toString())
        requestBuilder.addHeader("_sign", md5Decode32(sign.toString()))
        return chain.proceed(requestBuilder.build())
    }

    companion object {
        private const val METHOD_GET = "GET"
        private const val METHOD_POST = "POST"
    }
}

AES加密

解决问题:

数据加密,防止信息截取,RSA加解密更耗时

具体算法:

AES/CBC/NoPadding

package com.zhangteng.rxhttputils.utils

import java.security.NoSuchAlgorithmException
import java.security.SecureRandom
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec

/**
 * Created by Swing on 2017/12/6.
 */
object AESUtils {
    /**
     * 随机生成秘钥
     */
    val key: String
        get() = try {
            val kg = KeyGenerator.getInstance("AES")
            kg.init(128)
            val sk = kg.generateKey()
            val b = sk.encoded
            byteToHexString(b)
        } catch (e: NoSuchAlgorithmException) {
            e.printStackTrace()
            ""
        }

    /**
     * 使用指定的字符串生成秘钥
     */
    fun getKeyByPass(keyRaw: String): String {
        return try {
            val kg = KeyGenerator.getInstance("AES")
            // kg.init(128);//要生成多少位,只需要修改这里即可128, 192或256
            //SecureRandom是生成安全随机数序列,password.getBytes()是种子,只要种子相同,序列就一样,所以生成的秘钥就一样。
            kg.init(128, SecureRandom(keyRaw.toByteArray()))
            val sk = kg.generateKey()
            val b = sk.encoded
            byteToHexString(b)
        } catch (e: NoSuchAlgorithmException) {
            e.printStackTrace()
            ""
        }
    }

    /**
     * byte数组转化为16进制字符串
     *
     * @param bytes
     * @return
     */
    fun byteToHexString(bytes: ByteArray): String {
        val sb = StringBuffer()
        for (i in bytes.indices) {
            val strHex = Integer.toHexString(bytes[i].toInt())
            if (strHex.length > 3) {
                sb.append(strHex.substring(6))
            } else {
                if (strHex.length < 2) {
                    sb.append("0$strHex")
                } else {
                    sb.append(strHex)
                }
            }
        }
        return sb.toString()
    }

    //加密
    @Throws(Exception::class)
    fun encrypt(data: String, key: String, iv: String): String? {
        val cipher = Cipher.getInstance("AES/CBC/NoPadding")
        val blockSize = cipher.blockSize
        val dataBytes = data.toByteArray()
        var plaintextLength = dataBytes.size
        if (plaintextLength % blockSize != 0) {
            plaintextLength = plaintextLength + (blockSize - plaintextLength % blockSize)
        }
        val plaintext = ByteArray(plaintextLength)
        System.arraycopy(dataBytes, 0, plaintext, 0, dataBytes.size)
        val keyspec =
            SecretKeySpec(key.toByteArray(), "AES")
        val ivspec =
            IvParameterSpec(iv.toByteArray())
        cipher.init(Cipher.ENCRYPT_MODE, keyspec, ivspec)
        val encrypted = cipher.doFinal(plaintext)
        return Base64Utils.encode(encrypted)
    }

    //解密
    @Throws(Exception::class)
    fun decrypt(data: String, key: String, iv: String): String {
        val encrypted1 = Base64Utils.decode(data)
        val cipher = Cipher.getInstance("AES/CBC/NoPadding")
        val keyspec =
            SecretKeySpec(key.toByteArray(), "AES")
        val ivspec =
            IvParameterSpec(iv.toByteArray())
        cipher.init(Cipher.DECRYPT_MODE, keyspec, ivspec)
        val original = cipher.doFinal(encrypted1)
        return String(original)
    }
}

RSA加密

解决问题:

AES秘钥交换困难

具体算法:

RSA/ECB/PKCS1Padding

package com.zhangteng.rxhttputils.utils

import android.util.Base64
import java.security.KeyFactory
import java.security.KeyPair
import java.security.KeyPairGenerator
import java.security.NoSuchAlgorithmException
import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.X509EncodedKeySpec
import java.util.*
import javax.crypto.Cipher

/**
 * 字符串格式的密钥在未在特殊说明情况下都为BASE64编码格式<br></br>
 * 由于非对称加密速度极其缓慢,一般文件不使用它来加密而是使用对称加密,<br></br>
 * 非对称加密算法可以用来对对称加密的密钥加密,这样保证密钥的安全也就保证了数据的安全
 */
object RSAUtils {
    /**
     * 非对称加密密钥算法
     */
    const val RSA = "RSA"

    /**
     * 加密填充方式
     */
    const val ECB_PKCS1_PADDING = "RSA/ECB/PKCS1Padding"

    /**
     * 秘钥默认长度
     */
    const val DEFAULT_KEY_SIZE = 1024

    /**
     * 当要加密的内容超过bufferSize,则采用partSplit进行分块加密
     */
    val DEFAULT_SPLIT = "#PART#".toByteArray()

    /**
     * 当前秘钥支持加密的最大字节数
     */
    const val DEFAULT_BUFFERSIZE = DEFAULT_KEY_SIZE / 8 - 11

    /**
     * 随机生成RSA密钥对
     *
     * @param keyLength 密钥长度,范围:512~2048
     * 一般1024
     * @return
     */
    fun generateRSAKeyPair(keyLength: Int): KeyPair? {
        return try {
            val kpg =
                KeyPairGenerator.getInstance(RSA)
            kpg.initialize(keyLength)
            kpg.genKeyPair()
        } catch (e: NoSuchAlgorithmException) {
            e.printStackTrace()
            null
        }
    }

    /**
     *
     *
     * 公钥加密
     *
     *
     * @param data      源数据
     * @param publicKey 公钥(BASE64编码)
     * @return
     * @throws Exception
     */
    @Throws(Exception::class)
    fun encryptByPublicKey(data: String, publicKey: String?): String {
        return String(
            Base64.encode(
                encryptByPublicKey(
                    data.toByteArray(),
                    Base64.decode(publicKey, 0)
                ), 0
            )
        ).replace("\n", "").replace("\\", "")
    }

    /**
     * 用公钥对字符串进行加密
     *
     * @param data 原文
     */
    @Throws(Exception::class)
    fun encryptByPublicKey(
        data: ByteArray?,
        publicKey: ByteArray?
    ): ByteArray {
        // 得到公钥
        val keySpec =
            X509EncodedKeySpec(publicKey)
        val kf = KeyFactory.getInstance(RSA)
        val keyPublic = kf.generatePublic(keySpec)
        // 加密数据
        val cp = Cipher.getInstance(ECB_PKCS1_PADDING)
        cp.init(Cipher.ENCRYPT_MODE, keyPublic)
        return cp.doFinal(data)
    }

    /**
     *
     *
     * 私钥加密
     *
     *
     * @param data      源数据
     * @param publicKey 公钥(BASE64编码)
     * @return
     * @throws Exception
     */
    @Throws(Exception::class)
    fun encryptByPrivateKey(data: String, publicKey: String?): String {
        return String(
            Base64.encode(
                encryptByPrivateKey(
                    data.toByteArray(),
                    Base64.decode(publicKey, 0)
                ), 0
            )
        ).replace("\n", "").replace("\\", "")
    }

    /**
     * 私钥加密
     *
     * @param data       待加密数据
     * @param privateKey 密钥
     * @return byte[] 加密数据
     */
    @Throws(Exception::class)
    fun encryptByPrivateKey(
        data: ByteArray?,
        privateKey: ByteArray?
    ): ByteArray {
        // 得到私钥
        val keySpec =
            PKCS8EncodedKeySpec(privateKey)
        val kf = KeyFactory.getInstance(RSA)
        val keyPrivate = kf.generatePrivate(keySpec)
        // 数据加密
        val cipher =
            Cipher.getInstance(ECB_PKCS1_PADDING)
        cipher.init(Cipher.ENCRYPT_MODE, keyPrivate)
        return cipher.doFinal(data)
    }

    /**
     *
     *
     * 公钥解密
     *
     *
     * @param encryptedData 已加密数据(BASE64编码)
     * @param publicKey     公钥(BASE64编码)
     * @return
     * @throws Exception
     */
    @Throws(Exception::class)
    fun decryptByPublicKey(
        encryptedData: String?,
        publicKey: String?
    ): String {
        return String(
            decryptByPublicKey(
                Base64.decode(encryptedData, 0),
                Base64.decode(publicKey, 0)
            )
        )
    }

    /**
     * 公钥解密
     *
     * @param data      待解密数据
     * @param publicKey 密钥
     * @return byte[] 解密数据
     */
    @Throws(Exception::class)
    fun decryptByPublicKey(
        data: ByteArray?,
        publicKey: ByteArray?
    ): ByteArray {
        // 得到公钥
        val keySpec =
            X509EncodedKeySpec(publicKey)
        val kf = KeyFactory.getInstance(RSA)
        val keyPublic = kf.generatePublic(keySpec)
        // 数据解密
        val cipher =
            Cipher.getInstance(ECB_PKCS1_PADDING)
        cipher.init(Cipher.DECRYPT_MODE, keyPublic)
        return cipher.doFinal(data)
    }

    /**
     * <P>
     * 私钥解密
    </P> *
     *
     * @param encryptedData 已加密数据(BASE64编码)
     * @param privateKey    私钥(BASE64编码)
     * @return
     * @throws Exception
     */
    @Throws(Exception::class)
    fun decryptByPrivateKey(
        encryptedData: String?,
        privateKey: String?
    ): String {
        return String(
            decryptByPrivateKey(
                Base64.decode(encryptedData, 0),
                Base64.decode(privateKey, 0)
            )
        )
    }

    /**
     * 使用私钥进行解密
     */
    @Throws(Exception::class)
    fun decryptByPrivateKey(
        encrypted: ByteArray?,
        privateKey: ByteArray?
    ): ByteArray {
        // 得到私钥
        val keySpec =
            PKCS8EncodedKeySpec(privateKey)
        val kf = KeyFactory.getInstance(RSA)
        val keyPrivate = kf.generatePrivate(keySpec)

        // 解密数据
        val cp = Cipher.getInstance(ECB_PKCS1_PADDING)
        cp.init(Cipher.DECRYPT_MODE, keyPrivate)
        return cp.doFinal(encrypted)
    }
}

okhttp拦截器实现加解密

package com.zhangteng.rxhttputils.interceptor

import android.text.TextUtils
import com.google.gson.JsonParser
import com.zhangteng.rxhttputils.http.HttpUtils
import com.zhangteng.rxhttputils.http.OkHttpClient
import com.zhangteng.rxhttputils.utils.AESUtils
import com.zhangteng.rxhttputils.utils.DiskLruCacheUtils
import com.zhangteng.rxhttputils.utils.RSAUtils
import com.zhangteng.rxhttputils.utils.SPUtils
import okhttp3.*
import okio.Buffer
import java.io.IOException
import java.nio.charset.Charset

/**
 * 添加加解密拦截器
 * Created by Swing on 2019/10/20.
 */
class EncryptionInterceptor(private val publicKeyUrl: HttpUrl) : Interceptor {
    @Throws(IOException::class)
    override fun intercept(chain: Interceptor.Chain): okhttp3.Response {
        val request = chain.request()
        val headers = request.headers()
        if (headers.names().contains(SECRET) && "true" == headers[SECRET]) {
            var secretRequest: Request? = buildRequest(request) ?: return errorSecretResponse
            var secretResponse = chain.proceed(secretRequest!!)
            val secretResponseBody = secretResponse.body()
            val secretResponseStr =
                if (secretResponseBody != null) secretResponseBody.string() else ""
            val jsonObject = JsonParser().parse(
                secretResponseStr.substring(
                    0,
                    secretResponseStr.lastIndexOf("}") + 1
                )
            ).asJsonObject
            val jsonElement = jsonObject["status"]
            if (jsonElement != null
                && !jsonElement.isJsonArray
                && !jsonElement.isJsonObject
                && !jsonElement.isJsonNull
                && "2100" == jsonElement.asString
            ) {
                SPUtils.put(HttpUtils.getInstance().getContext()!!, SPUtils.FILE_NAME, SECRET, "")
                DiskLruCacheUtils.remove(publicKeyUrl)
                DiskLruCacheUtils.flush()
                secretRequest = buildRequest(request)
                if (secretRequest == null) {
                    return errorSecretResponse
                }
                secretResponse = chain.proceed(secretRequest)
            } else {
                val mediaType =
                    if (secretResponseBody != null) secretResponseBody.contentType() else MediaType.parse(
                        "application/json;charset=UTF-8"
                    )
                val newResonseBody = ResponseBody.create(mediaType, secretResponseStr)
                secretResponse = secretResponse.newBuilder().body(newResonseBody).build()
            }
            return secretResponse
        }
        return chain.proceed(request)
    }

    /**
     * 构建加密请求
     *
     * @param request 原请求
     */
    @Throws(IOException::class)
    private fun buildRequest(request: Request): Request? {
        if (TextUtils.isEmpty(
                SPUtils[HttpUtils.getInstance()
                    .getContext()!!, SPUtils.FILE_NAME, SECRET, ""].toString()
            )
        ) {
            val secretResponse = OkHttpClient.getInstance().client.newCall(
                Request.Builder().url(publicKeyUrl).build()
            ).execute()
            val secretResponseString = secretResponse.body()?.string()
            if (secretResponse.code() == 200) {
                val jsonObject = JsonParser().parse(secretResponseString).asJsonObject
                val jsonElement = jsonObject["result"].asJsonObject["publicKey"]
                SPUtils.put(
                    HttpUtils.getInstance().getContext()!!,
                    SPUtils.FILE_NAME,
                    SECRET,
                    jsonElement.asString
                )
            } else {
                return null
            }
        }
        val aesRequestKey: String = AESUtils.key
        val requestBuilder = request.newBuilder()
        requestBuilder.removeHeader(SECRET)
        try {
            requestBuilder.addHeader(
                SECRET,
                RSAUtils.encryptByPublicKey(
                    aesRequestKey,
                    SPUtils[HttpUtils.getInstance()
                        .getContext()!!, SPUtils.FILE_NAME, SECRET, publicKey] as String
                )
            )
        } catch (e: Exception) {
            return null
        }
        if (METHOD_GET == request.method()) {
            val url = request.url().url().toString()
            val paramsBuilder = url.substring(url.indexOf("?") + 1)
            try {
                val encryptParams =
                    AESUtils.encrypt(paramsBuilder, aesRequestKey, aesRequestKey.substring(0, 16))
                requestBuilder.url(url.substring(0, url.indexOf("?")) + "?" + encryptParams)
            } catch (e: Exception) {
                return null
            }
        } else if (METHOD_POST == request.method()) {
            val requestBody = request.body()
            if (requestBody != null && aesRequestKey.length >= 16) {
                if (requestBody is FormBody) {
                    val formBody = request.body() as FormBody?
                    val bodyBuilder = FormBody.Builder()
                    try {
                        if (formBody != null) {
                            for (i in 0 until formBody.size()) {
                                val value = formBody.encodedValue(i)
                                if (!TextUtils.isEmpty(value)) {
                                    val encryptParams = AESUtils.encrypt(
                                        value,
                                        aesRequestKey,
                                        aesRequestKey.substring(0, 16)
                                    )
                                    bodyBuilder.addEncoded(formBody.encodedName(i), encryptParams)
                                }
                            }
                            requestBuilder.post(bodyBuilder.build())
                        }
                    } catch (e: Exception) {
                        return null
                    }
                } else {
                    val buffer = Buffer()
                    requestBody.writeTo(buffer)
                    var charset =
                        Charset.forName("UTF-8")
                    val contentType = requestBody.contentType()
                    if (contentType != null) {
                        charset = contentType.charset()
                    }
                    val paramsRaw =
                        buffer.readString(charset ?: Charset.defaultCharset())
                    if (!TextUtils.isEmpty(paramsRaw)) {
                        try {
                            val encryptParams = AESUtils.encrypt(
                                paramsRaw,
                                aesRequestKey,
                                aesRequestKey.substring(0, 16)
                            )
                            requestBuilder.post(
                                RequestBody.create(
                                    requestBody.contentType(),
                                    encryptParams
                                )
                            )
                        } catch (e: Exception) {
                            return null
                        }
                    }
                }
            }
        }
        return requestBuilder.build()
    }

    /**
     * 获取加密失败响应
     */
    private val errorSecretResponse: okhttp3.Response
        private get() {
            val failureResponseBuilder = okhttp3.Response.Builder()
            failureResponseBuilder.body(
                ResponseBody.create(
                    MediaType.parse("application/json;charset=UTF-8"),
                    "{\"message\": \"移动端加密失败\",\"status\": ${SECRET_ERROR}}"
                )
            )
            return failureResponseBuilder.build()
        }

    companion object {
        private const val METHOD_GET: String = "GET"
        private const val METHOD_POST: String = "POST"
        const val SECRET: String = "_secret"
        const val SECRET_ERROR: Int = 2100
        const val publicKey: String = ""
    }
}

package com.zhangteng.rxhttputils.interceptor

import android.text.TextUtils
import com.zhangteng.rxhttputils.http.HttpUtils
import com.zhangteng.rxhttputils.interceptor.EncryptionInterceptor.Companion.SECRET
import com.zhangteng.rxhttputils.interceptor.EncryptionInterceptor.Companion.SECRET_ERROR
import com.zhangteng.rxhttputils.utils.AESUtils.decrypt
import com.zhangteng.rxhttputils.utils.RSAUtils
import com.zhangteng.rxhttputils.utils.SPUtils
import okhttp3.Interceptor
import okhttp3.MediaType
import okhttp3.Response
import okhttp3.ResponseBody
import java.io.IOException

/**
 * 添加加解密拦截器
 * Created by Swing on 2019/10/20.
 */
class DecryptionInterceptor : Interceptor {

    @Throws(IOException::class)
    override fun intercept(chain: Interceptor.Chain): Response {
        val response = chain.proceed(chain.request())
        if (!response.isSuccessful || response.code() != 200) {
            return response
        }
        val responseBuilder = response.newBuilder()
        val responseBody = response.body()
        val responseHeaders = response.headers()
        for (name in responseHeaders.names()) {
            if (SECRET.contains(name!!) && !TextUtils.isEmpty(responseHeaders[name])) {
                return try {
                    val encryptKey = responseHeaders[name]
                    val aesResponseKey: String = RSAUtils.decryptByPublicKey(
                        encryptKey,
                        SPUtils[HttpUtils.getInstance()
                            .getContext()!!, SPUtils.FILE_NAME, SECRET, EncryptionInterceptor.publicKey] as String
                    )
                    val mediaType =
                        if (responseBody != null) responseBody.contentType() else MediaType.parse(
                            "application/json;charset=UTF-8"
                        )
                    val responseStr =
                        if (responseBody != null) responseBody.string() else ""
                    val rawResponseStr = decrypt(
                        responseStr,
                        aesResponseKey,
                        aesResponseKey.substring(0, 16)
                    )
                    responseBuilder.body(ResponseBody.create(mediaType, rawResponseStr))
                    responseBuilder.build()
                } catch (e: Exception) {
                    val failureResponse = Response.Builder()
                    failureResponse.body(
                        ResponseBody.create(
                            MediaType.parse("application/json;charset=UTF-8"),
                            "{\"message\": \"移动端解密失败${e.message}\",\"status\": ${SECRET_ERROR}}"
                        )
                    )
                    failureResponse.build()
                }
            }
        }
        return response
    }
}

优化

存在问题:

客户端获取服务端RSA公钥时明文,泄露服务器公钥,造成服务器响应数据泄露

解决方法:

1、客户端获取服务端RSA公钥;

2、客户端随机生成RSA密钥对

3、客户端使用服务器公钥加密客户端公钥后交于服务端;

4、客户端与服务端使用客户端公私钥加解密数据完成安全秘钥交换。

猜你喜欢

转载自blog.csdn.net/duoluo9/article/details/105214983