传统的 HTTP 协议名为“超文本传输协议”,其客户端和服务器的交互方式为:
在数据传输过程中,需要经过无数的终端,因为在传输过程使用的是明问传输,所以在中间会很容易被监听、截获或者修改:
这就是所谓的“中间人攻击”,在安全大于天的今天,这种情况显然是不被允许的,如何防范这种情况的发生呢?
既然 HTTP 在网络传输中是明文传输的,谁拿到都能看得懂,那我们将传输数据加密传输不就行了么。数据在传输过程中都是加密过后的内容,而客户端和服务器根据手中的密钥来对加密信息进行解密之后来读取信息,即使中间有人截获了,也不会知道我们传输的是什么,我们的安全目的也就达到了,这样 HTTPS 应运而生。
上面提到了客户端和服务器需要对传输内容进行加密解密,自然就需要双方都需要有一个相同的密钥,只要这个密钥不泄露,密文就不会被破解,那么问题又随之而来,客户端是遍布全世界的,如何保证随机的客户端和服务器都拥有相同的密钥呢?非对称加密
可以完美解决这个问题:
至此,客户端和服务器都拥有的相同的密钥,就可以对传输内容进行加密解密了。
这个时候又一个问题随之二来,服务器的公钥是对外公布的,那么如何保证服务器的公钥不被篡改呢?这时候该 CA机构
来出场摆平了。
CA 机构规定,每个“好网站”都需要有一个“良民证”,只要是这个良民证的网站,就可以随便出入大门,如果没有良民证或者良民证被发现是假的,就会被特务跟踪。
每个网站需要像 CA 机构去申请一个“良民证”—— CA 证书,申请时需要携带自己的身份证明:网站公钥。CA 机构根据你的申请信息会将你的公钥打包成一个“良民证”—— CA 证书,这样你将良民证(CA 证书)携带在身上(部署在服务器),就可以证明自己的清白了。
客户端在访问我们的网站时,服务器首先会将这个证书返回给浏览器,操作系统会集成所有 CA 机构的公钥,然后浏览器使用 CA 机构的公钥对证书进行解密,从而得到我们网站的公钥和一些其他身份信息,如何证书以及其他信息是正确的,那么会提示用户这是一个合法网站,接下来就是双方的加密通信了;如果内置的所有 CA 公钥都无法解开这个证书,或者解开之后发现证书信息是错误的或者是过期的,则会做出响应的提示,提示用户这个网站是危险的。
除了根据 CA 机构申请证书之外,我们也可以自己创建证书,因为我们自己创建了证书,系统没有提前内置公钥,所以浏览器也会提醒这个网站是有问题的,但这并不妨碍我们进行加密传输,接下来我们实践一下。
上面讲的 HTTPS 实际上是一个单向的认证,也就是说,客户端只对服务器的合法性进行验证,而服务器并不验证客户端,这满足了日常的访问需求。但在一些特殊领域,例如金融、军事等等保密要求更加严格的领域,服务器并不完全对所有人开放,所以也就需要对客户端的合法性进行验证,这就是双向认证。
keytool -genkey -alias server -keyalg RSA -keystore server.jks -validity 3600 -storepass 111111
上面命令执行之后,需要输入一系列证书信息
会生成我们网站的私钥文件:server.jks
。
需要注意的是:
您的名字与姓氏是什么?
需要输入服务器的 IP 或者域名,否则会出错keytool -export -alias server -file server.cer -keystore server.jks -storepass 111111
这个命令是从私钥中导出证书:server.cer
,这个证书中包含了我们网站的一些信息(上一条命令中我们输入的那些)以及网站的公钥
将上面生成的私钥文件 server.jks
上传到服务器,修改 apache-tomcat-8.5.47/conf/server.xml
文件,添加以下标签:
<Connector
SSLEnabled="true"
acceptCount="100"
clientAuth="false"
disableUploadTimeout="true"
enableLookups="true"
keystoreFile="/***********/server.jks" //这个就是我们的私钥文件
keystorePass="111111" //私钥文件密码
maxSpareThreads="75"
maxThreads="200"
minSpareThreads="5"
port="8443"
protocol="org.apache.coyote.http11.Http11NioProtocol"
scheme="https"
secure="true"
sslProtocol="TLS"
/>
重启 Tomcat
将上面生成的 server.cer
放到项目的 assets 目录中待用:
try {
val inputStream: InputStream = assets.open("server.cer")
val trustManager: X509TrustManager = trustManagerForCertificates(inputStream)
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(null, arrayOf<TrustManager>(trustManager), null)
val sslSocketFactory = sslContext.socketFactory
val request: Request = Request.Builder()
.url("https://www.lifekeeper.top:8443")
.build()
val client: OkHttpClient = OkHttpClient.Builder()
.sslSocketFactory(sslSocketFactory, trustManager)
.hostnameVerifier(HostnameVerifier { hostname, _ ->
"www.lifekeeper.top" == hostname
})
.build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
e.printStackTrace()
}
override fun onResponse(call: Call, response: Response) {
Log.d(TAG, response.body!!.string())
}
})
} catch (e: NoSuchAlgorithmException) {
e.printStackTrace()
} catch (e: IOException) {
e.printStackTrace()
} catch (e: GeneralSecurityException) {
e.printStackTrace()
}
需要注意的是,创建 OkHttpClient 时,一定要添加hostnameVerifier
方法,可以对域名做一下验证,或者干脆直接返回 true,如果没有添加这个方法,则会抛出javax.net.ssl.SSLPeerUnverifiedException: Hostname xxx not verifie
@Throws(GeneralSecurityException::class)
private fun trustManagerForCertificates(`in`: InputStream): X509TrustManager {
val certificateFactory: CertificateFactory = CertificateFactory.getInstance("X.509")
val certificates: Collection<Certificate?> = certificateFactory.generateCertificates(`in`)
require(!certificates.isEmpty()) { "expected non-empty set of trusted certificates" }
// Put the certificates a key store.
val password = "password".toCharArray() // Any password will work.
val keyStore: KeyStore = newEmptyKeyStore(password)
for ((index, certificate) in certificates.withIndex()) {
val certificateAlias = (index).toString()
keyStore.setCertificateEntry(certificateAlias, certificate)
}
val keyManagerFactory: KeyManagerFactory = KeyManagerFactory.getInstance(
KeyManagerFactory.getDefaultAlgorithm()
)
keyManagerFactory.init(keyStore, password)
val trustManagerFactory: TrustManagerFactory = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm()
)
trustManagerFactory.init(keyStore)
val trustManagers: Array<TrustManager> = trustManagerFactory.trustManagers
check(!(trustManagers.size != 1 || trustManagers[0] !is X509TrustManager)) {
("Unexpected default trust managers:"
+ trustManagers.contentToString())
}
return trustManagers[0] as X509TrustManager
}
@Throws(GeneralSecurityException::class)
private fun newEmptyKeyStore(password: CharArray): KeyStore {
return try {
val keyStore: KeyStore =
KeyStore.getInstance(KeyStore.getDefaultType()) // 这里添加自定义的密码,默认
val `in`: InputStream? = null // By convention, 'null' creates an empty key store.
keyStore.load(`in`, password)
keyStore
} catch (e: IOException) {
throw AssertionError(e)
}
}
单向访问就是这么简单。
对于一些特殊的需求,客户端不光需要认证服务端,服务端也确保客户端也是合法的,这就需要双向认证了。
和单向认证不同的是,因为需要双向的认证合法性,所以需要生成服务端和客户端两套证书,并相互信任:
生成服务器证书
keytool -genkeypair -v -alias server -keyalg RSA -validity 3650 -keystore server.keystore -storepass 111111 -keypass 111111 -dname "CN=www.lifekeeper.top,OU=lifekeeper,O=lifekeeper,L=beijing,ST=beijing,C=cn"
导出服务器端证书
keytool -exportcert -alias server -keystore server.keystore -file server.cer -storepass 111111
将服务器端证书导入信任证书
keytool -importcert -alias serverca -keystore server_trust.keystore -file server.cer -storepass 111111
生成客户端证书
keytool -genkeypair -v -alias client -dname "CN=lifekeeper client" -keyalg RSA -validity 3650 -keypass 111111 -keystore client.p12 -storepass 111111 -storetype PKCS12
导出客户端证书
keytool -exportcert -alias client -file client.cer -keystore client.p12 -storepass 111111 -storetype PKCS12
将客户端证书导入到服务器信任证书库
keytool -importcert -alias clientca -keystore server_trust.keystore -file client.cer -storepass 111111
将 server.keystore
和 server_trust.keystore
都上传到服务器,修改 apache-tomcat-8.5.47/conf/server.xml
文件,添加以下标签:
<Connector
SSLEnabled="true"
acceptCount="100"
clientAuth="true"
truststoreFile="/home/apache-tomcat-8.5.47/conf/server_trust.keystore"
disableUploadTimeout="true"
enableLookups="true"
keystoreFile="/home/apache-tomcat-8.5.47/conf/server.keystore"
keystorePass="111111"
maxSpareThreads="75"
maxThreads="200"
minSpareThreads="5"
port="8443"
protocol="org.apache.coyote.http11.Http11NioProtocol"
scheme="https"
secure="true"
sslProtocol="TLS"
/>
重启 Tomcat
Java平台可以识别 keystore 的证书文件,而 Android 平台只识别 bks
格式的证书文件,所以我们需要将 client.p12
文件转换成 bks 格式的,下载转换工具:
解压之后,使用 java -jar portecle.jar
命令打开软件,然后按照下面的步骤执行转换:
转换过程中有可能会出错,提示:
java.security.KeyStoreException:java.io.IOException:Error initialising store of key store:
java.security.invalidKeyException:lllegal key size
这是因为
Illegal key size or default parameters 是指密钥长度受限制,
java运行时环境读到的是受限的policy文件。
policy文件位于${java_home}/jre/lib/security 目录下。
这种限制是因为美国对软件出口的控制。
解决办法如下:
jdk\jre\lib\security
目录下的相同文件然后将转换后的 client.bks
和 server.cer
文件放到 Android 项目的 assets 目录待用。
Android 代码:
工具类:
HttpsSSLParams.kt
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.X509TrustManager
class HttpsSSLParams {
var sSLSocketFactory: SSLSocketFactory? = null
var trustManager: X509TrustManager? = null
}
HttpsTrustManager.kt
import java.security.KeyStore
import java.security.cert.CertificateException
import java.security.cert.X509Certificate
import javax.net.ssl.TrustManager
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
class HttpsTrustManager(localTrustManager: X509TrustManager) : X509TrustManager {
private var defaultTrustManager: X509TrustManager? = null
private var localTrustManager: X509TrustManager? = localTrustManager
init {
val var4 = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
var4.init(null as KeyStore?)
defaultTrustManager = chooseTrustManager(var4.trustManagers)
}
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {
}
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {
try {
defaultTrustManager!!.checkServerTrusted(chain, authType)
} catch (ce: CertificateException) {
localTrustManager!!.checkServerTrusted(chain, authType)
}
}
override fun getAcceptedIssuers(): Array<X509Certificate?> {
return arrayOfNulls(0)
}
private fun chooseTrustManager(trustManagers: Array<TrustManager>): X509TrustManager? {
for (trustManager in trustManagers) {
if (trustManager is X509TrustManager) {
return trustManager
}
}
return null
}
}
HttpsUnSafeTrustManager.kt
import java.security.cert.X509Certificate
import javax.net.ssl.X509TrustManager
class HttpsUnSafeTrustManager : X509TrustManager {
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {
}
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {
}
override fun getAcceptedIssuers(): Array<X509Certificate> {
return arrayOf()
}
}
HttpsUtil.kt
import java.io.IOException
import java.io.InputStream
import java.security.*
import java.security.cert.CertificateException
import java.security.cert.CertificateFactory
import javax.net.ssl.*
class HttpsUtil {
companion object {
fun getSslSocketFactory(
certificates: Array<InputStream>,
bksFile: InputStream?,
password: String?
): HttpsSSLParams {
val sslParams = HttpsSSLParams()
return try {
val trustManagers = prepareTrustManager(*certificates)
val keyManagers = prepareKeyManager(bksFile, password)
val sslContext = SSLContext.getInstance("TLS")
val trustManager: X509TrustManager
trustManager = if (trustManagers != null) {
HttpsTrustManager(chooseTrustManager(trustManagers)!!)
} else {
HttpsUnSafeTrustManager()
}
sslContext.init(keyManagers, arrayOf<TrustManager>(trustManager), null)
sslParams.sSLSocketFactory = sslContext.socketFactory
sslParams.trustManager = trustManager
sslParams
} catch (e: NoSuchAlgorithmException) {
e.printStackTrace()
throw AssertionError(e)
} catch (e: KeyManagementException) {
e.printStackTrace()
throw AssertionError(e)
} catch (e: KeyStoreException) {
e.printStackTrace()
throw AssertionError(e)
}
}
private fun prepareTrustManager(vararg certificates: InputStream): Array<TrustManager>? {
if (certificates.isEmpty()) return null
try {
val certificateFactory = CertificateFactory.getInstance("X.509")
val keyStore = KeyStore.getInstance(KeyStore.getDefaultType())
keyStore.load(null)
for ((index, certificate) in certificates.withIndex()) {
val certificateAlias = (index).toString()
keyStore.setCertificateEntry(
certificateAlias,
certificateFactory.generateCertificate(certificate)
)
try {
certificate.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
val trustManagerFactory: TrustManagerFactory =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
trustManagerFactory.init(keyStore)
return trustManagerFactory.trustManagers
} catch (e: NoSuchAlgorithmException) {
e.printStackTrace()
} catch (e: CertificateException) {
e.printStackTrace()
} catch (e: KeyStoreException) {
e.printStackTrace()
} catch (e: IOException) {
e.printStackTrace()
}
return null
}
private fun prepareKeyManager(
bksFile: InputStream?,
password: String?
): Array<KeyManager?>? {
try {
if (bksFile == null || password == null) return null
val clientKeyStore = KeyStore.getInstance("BKS")
clientKeyStore.load(bksFile, password.toCharArray())
val keyManagerFactory: KeyManagerFactory =
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
keyManagerFactory.init(clientKeyStore, password.toCharArray())
return keyManagerFactory.getKeyManagers()
} catch (e: NoSuchAlgorithmException) {
e.printStackTrace()
} catch (e: UnrecoverableKeyException) {
e.printStackTrace()
} catch (e: CertificateException) {
e.printStackTrace()
} catch (e: KeyStoreException) {
e.printStackTrace()
} catch (e: IOException) {
e.printStackTrace()
}
return null
}
private fun chooseTrustManager(trustManagers: Array<TrustManager>): X509TrustManager? {
for (trustManager in trustManagers) {
if (trustManager is X509TrustManager) {
return trustManager
}
}
return null
}
}
}
OKHttp 使用如下:
val request: Request = Request.Builder()
.url("https://www.lifekeeper.top:8443")
.build()
val factory = HttpsUtil.getSslSocketFactory(
arrayOf(assets.open("server.cer")),
assets.open("client.bks"),
"111111"
)
val client: OkHttpClient = OkHttpClient.Builder()
.connectTimeout(60 * 1000.toLong(), TimeUnit.MILLISECONDS)
.readTimeout(5 * 60 * 1000.toLong(), TimeUnit.MILLISECONDS)
.writeTimeout(5 * 60 * 1000.toLong(), TimeUnit.MILLISECONDS)
.sslSocketFactory(
factory.sSLSocketFactory!!, factory.trustManager!!
)
.hostnameVerifier(HostnameVerifier { hostname, _ ->
"www.lifekeeper.top" == hostname
})
.build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
e.printStackTrace()
}
override fun onResponse(call: Call, response: Response) {
Log.d(TAG, response.body!!.string())
}
})
至此,HTTPS 双向认证完成~