在python上使用SSL有许多场景,我主要关注的是使用python访问HTTPS资源,以及使用python提供HTTPS服务。(HTTPS是SSL在WEB上的应用之一)
一、使用python访问HTTPS网站
这应该算是最简单也是最常见的场景了。我们使用python做为客户端去访问公网上的网站,而这个网站为了传输安全(避免被劫持或者窃听)使用了HTTPS服务,传输过程内容都经过了SSL加密。下面来看下具体的python代码,这里使用的是python2.7.11,用的是python自带的urllib2。当然你也可以使用requests或者pycurl等第三方库,都可以,原理都是一样的,只是实现的手段有些差异而已。
import urllib2
import ssl
if __name__ == ‘__main__‘:
myurl="https://www.baidu.com"
req = urllib2.Request(myurl)
try:
response = urllib2.urlopen(req)
print "HTTP return code:%d" % response.getcode()
strResult= response.read()
print strResult
except Exception ,ex:
print "Found Error :%s" % str(ex)
运行它,我们就可以得到这个网站上的内容了。并且传输过程都经过了加密。是不是很简单。整个的传输过程大概是这样的,客户端请求SSL连接握手(其中会跟服务器有些SSL协议之间的交互,这个我们不用详细去研究)服务器把自己的证书传给客户端,客户端对这个证书进行认证(每台客户端电脑上都会默认安装一些权威CA(证书签发机构)的证书,这个认证就是使用这些证书进行的,python的ssl模块会自动加载这些权威CA证书,当然你也可以自己指定,这个后面会谈到),确保这个证书是由可信的CA颁布的(客户端对服务端证书进行认证这个操作,是从python2.7.9开始才有的,以前版本的是不进行认证的),之后就是利用证书交换密钥后,使用密钥对传输内容进行加密传输。
互联网上大部分的正规网站都是会有自己的证书的,这些证书都是经过权威CA认证的,可以被认为是可信的(其实这个也不一定,前段时间就有过谷歌封杀赛门铁克CA签发的证书的事件,有兴趣的可以去网上搜索看看)。但是同样也有不少网站,由于各种各样的原因没有这些经过权威CA认证的证书,毕竟申请这些证书流程比较麻烦,并且要支付一定的费用。这些网站也可以提供安全的HTTPS服务,但由于他们的证书是自签名的或者是由非权威的CA颁发的(这两种证书后面会谈到),所以会被认为是不可信的。当使用浏览器打开这些网站时,浏览器会提醒你这是不安全的连接,当然这个不安全是指网站未经权威认证,所以网站本身可能不安全,但在网络传输层面上来看,传输是安全的。那我们访问这些网站时,使用上面的代码会有错误提示,下面代码就是访问一个这是我自己建立的一个HTTPS网站https://127.0.0.1:8443,该网站使用的是自签名的证书。如果你使用谷歌浏览器访问会提示你:您的连接不是私密连接,如果你强制连接则会提示不安全
import urllib2
import ssl
if __name__ == ‘__main__‘:
myurl="https://127.0.0.1:8443"
req = urllib2.Request(myurl)
try:
response = urllib2.urlopen(req)
print "HTTP return code:%d" % response.getcode()
strResult= response.read()
print strResult
except Exception ,ex:
print "Found Error :%s" % str(ex)
运行这个就会出现错误:
提示证书校验错误。这时如果我们想要访问这样的网站就要把客户端的证书校验关闭。在前面加上一句:ssl._create_default_https_context = ssl._create_unverified_context即可
import urllib2
import ssl
ssl._create_default_https_context = ssl._create_unverified_context
if __name__ == ‘__main__‘:
myurl="https://127.0.0.1:8443"
req = urllib2.Request(myurl)
try:
response = urllib2.urlopen(req)
print "HTTP return code:%d" % response.getcode()
strResult= response.read()
print strResult
except Exception ,ex:
print "Found Error :%s" % str(ex)
当然也可以自己创建一个不校验的SSL上下文,然后引用这个上下文来打开url
ctx = ssl._create_unverified_context()
然后
response = urllib2.urlopen(req,context=ctx)
二、使用OPENSSL生成证书
1、生成自签名的证书
在使用python提供HTTPS服务之前,我们需要先生成证书,这里请参考https://www.cnblogs.com/wucao/archive/2017/02/27/6474711.html 这篇文章讲解了如何使用openssl生成证书。在WINDOWS平台记得要设置环境变量,参考我的一篇博文http://blog.sina.com.cn/s/blog_5d18f85f0102xdm7.html。
使用命令:openssl req -x509 -newkey rsa:2048 -nodes -days 365 -keyout private.pem -out cert.crt
之后会要求我们输入一些组织信息,你可以根据你的实际情况填写。之后就生成了自签名的证书cert.crt,私钥是private.pem
2、自己建立一个CA(证书签发机构),然后用自己的CA来颁发证书。这个我们后面讨论双向认证的时候会用到。详细内容请参考http://blog.csdn.net/gx_1983/article/details/47866537
这里我只是简单的写一下我自己的步骤
1) 保证openssl的bin目录在path环境变量里面。创建一个工作目录,这里我使用ca这个目录。然后在ca下面再创建目录demoCA。之后在demoCA下创建空白文本文件index.txt和serial,并且打开serial写入字符01
2) 先造成CA的KEY和证书
openssl req -new -x509 -days 36500 -key ca.key -out ca.crt
执行后会出现提示,主要是要求输入一些组织方面的信息,请按要求填写即可
执行成功后会造成ca.key和ca.crt 两个文件,ca.key为私钥需要妥善保管,不要轻易给别人。ca.crt为证书可以随意传播。
3) 生成服务器的私钥和证书,其中证书使用了CA的私钥进行签名:
l 生成server私钥
openssl genrsa -out server.key 2048
l 使用server私钥生成server端证书请求文件
openssl req -new -key server.key -out server.csr
一样需要回答一些组织方面的问题
l 使用server证书请求文件通过CA生成由CA签名的证书
openssl ca -in server.csr -out server.crt -cert ca.crt -keyfile ca.key
l 验证server证书
openssl verify -CAfile ca.crt server.crt
这样就得到两个文件:一个是私钥server.key;一个是证书server.crt
4) 用跟3)同样的方法生成客户端的client.key和client.crt这两个文件在后面的双向认证里面会用到
三、使用python提供HTTPS服务
我们有了证书和私钥了,下面就可以正式使用python建立一个HTTPS网站了。这里我使用框架实现,用的是twisted。使用的证书是之前用OPENSSL生成的自签名证书。
#-* -coding: utf-8 -* -
from twisted.web import server, resource
from twisted.internet import reactor,ssl
class MainResource(resource.Resource):
isLeaf = True
# 用于处理GET类型请求
def render_GET(self, request):
# name参数
name = ‘World‘
if request.args.has_key(‘name‘):
name = request.args[‘name‘][0]
# 设置响应编码
request.responseHeaders.addRawHeader("Content-Type", "text/html; charset=utf-8")
# 响应的内容直接返回
return "
Hello, " + name + ""
if __name__ == ‘__main__‘:
sslContext = ssl.DefaultOpenSSLContextFactory(
‘C:/ca/private.pem, # 私钥
‘C:/ca/cert.crt‘ # 证书
)
site = server.Site(MainResource())
reactor.listenSSL(8080, site, sslContext)
print "监听端口:8080"
reactor.run()
使用之前的客户端访问,记得关闭证书校验。这时可以看到可以顺利访问。如果使用浏览器访问,也可以正常访问,但此时会出现安全提示,忽略这个安全提示后也可以正常访问。
四、SSL双向校验
上面的示例都是客户端对服务器端证书的校验。这也是最常见的单向校验。这里面由客户端来校验服务器端的证书(当然也可以选择不校验)。通常的互联网服务都是这种方式。在这种方式下,客户端不需要有证书。服务器端也不会校验客户端的证书。此外还有一种双向校验模式。在这种模式下,客户端也要有证书,并且在协商过程中提供给服务器端。服务器也会对客户端的证书进行校验。这种模式一般应用于对安全要求比较高的环境,服务器和用户端都需要证书,并且双方都要能被权威CA验证通过。看看下面的服务器端代码:
#-* -coding: utf-8 -* -
from twisted.web import server, resource
from twisted.internet import reactor,ssl
from OpenSSL import SSL
def verifyCallback(connection, x509, errnum, errdepth, ok):
if not ok:
print ‘invalid cert from subject:‘, x509.get_subject()
return False
else:
print "Certs are fine", x509.get_subject()
return True
class MainResource(resource.Resource):
isLeaf = True
# 用于处理GET类型请求
def render_GET(self, request):
# name参数
name = ‘World‘
if request.args.has_key(‘name‘):
name = request.args[‘name‘][0]
# 设置响应编码
request.responseHeaders.addRawHeader("Content-Type", "text/html; charset=utf-8")
# 响应的内容直接返回
return "
Hello, " + name + ""
if __name__ == ‘__main__‘:
sslContext = ssl.DefaultOpenSSLContextFactory(
‘C:/ca/private.pem, # 私钥
‘C:/ca/cert.crt‘ # 证书
)
ctx = sslContext.getContext()
#这里改为了双向校验模式
ctx.set_verify(
SSL.VERIFY_PEER | SSL.VERIFY_FAIL_IF_NO_PEER_CERT,
verifyCallback
)
site = server.Site(MainResource())
reactor.listenSSL(8080, site, sslContext)
print "监听端口:8080"
reactor.run()
然后再用客户端来访问一下。这时会发现有错误出现,错误提示为:
提示sslv3协议握手失败。原因就是服务器端要求双向校验,但客户端没有提供自己的证书给服务器,所以握手协商失败。
好的,我们再改一下客户端代码,适配这种双向校验模式。这里客户端使用使用的证书是,是之前使用OPENSSL生成的客户端证书,并且使用了我们自己建立的CA进行签名。
import urllib2
import ssl
KEY_FILE="C:/ca/client.key"
CERT_FILE="C:/ca/client.crt"
context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
context.check_hostname = False
context.load_cert_chain(certfile=CERT_FILE,keyfile=KEY_FILE)
context.verify_mode=ssl.CERT_REQUIRED
if __name__ == ‘__main__‘:
myurl="https://127.0.0.1:8080/?name=qh"
req = urllib2.Request(myurl)
try:
response = urllib2.urlopen(req,context=context)
#response = urllib2.urlopen(req)
print "HTTP return code:%d" % response.getcode()
strResult= response.read()
print strResult
except Exception ,ex:
print "Found Error in auth phase:%s" % str(ex)
这里出现错误提示
证书校验失败。这是因为服务器提供的证书没有通过客户端的校验。
这是因为服务器端的证书是使用自签名的证书,当然通不过客户端校验了。这里我们要把服务器端的证书改成我们使用CA签名的证书。修改一下服务器端代码:
sslContext = ssl.DefaultOpenSSLContextFactory(
‘C:/ca/server.key‘, # 私钥
‘C:/ca/server.crt‘ # 证书
)
改过之后再试,可以还出现同样的错误,这是怎么回事呢?原来服务器端的证书是使用我们自建的CA进行签名的,不是权威CA,所以校验失败了。这怎么办呢,我们可以指定CA的证书,把我们的CA证书设置为可信。修改一下客户端,加上一句:
CA_FILE="c:/ca/ca.crt"
context.load_verify_locations(CA_FILE)
注意ca.crt是之前我们使用OPENSSL建立的CA证书(请参考前面第二部分的内容)。修改之后重新运行,发现还是出现错误:
这个错误提示变了,提示CA不知名。同时在服务器端的回调函数也有错误输出:invalid cert from subject:
这说明服务器端校验客户端证书失败,原因当然是服务器端我们没有把我们自己的CA设置为可信。所以同样的在服务器端也要修改一下,加上以下两句:
cafile="c:/ca/ca.crt"
ctx.load_verify_locations( cafile)
重新运行,这下成功了
下面把完整的代码发一下:
服务器端:
#-* -coding: utf-8 -* -
‘‘‘
Created on 2018-1-16
@author: qh
‘‘‘
from twisted.internet import iocpreactor as iocpreactor
try:
iocpreactor.install()
except Exception, e:
print "iocp install failed:%s" % str(e)
from twisted.web import server, resource
from twisted.internet import reactor,ssl
from OpenSSL import SSL
cafile="c:/ca/ca.crt"
class MainResource(resource.Resource):
isLeaf = True
# 用于处理GET类型请求
def render_GET(self, request):
# name参数
name = ‘World‘
if request.args.has_key(‘name‘):
name = request.args[‘name‘][0]
# 设置响应编码
request.responseHeaders.addRawHeader("Content-Type", "text/html; charset=utf-8")
# 响应的内容直接返回
return "
Hello, " + name + ""
if __name__ == ‘__main__‘:
sslContext = ssl.DefaultOpenSSLContextFactory(
‘C:/ca/server.key‘, # 私钥
‘C:/ca/server.crt‘ # 证书
)
ctx = sslContext.getContext()
ctx.set_verify(
SSL.VERIFY_PEER | SSL.VERIFY_FAIL_IF_NO_PEER_CERT,
verifyCallback
)
ctx.load_verify_locations( cafile)
site = server.Site(MainResource())
reactor.listenSSL(8080, site, sslContext)
print "监听端口:8080"
reactor.run()
客户端:
‘‘‘
Created on 2018-3-2
@author: qh
‘‘‘
import urllib2
import ssl
KEY_FILE="C:/ca/client.key"
CERT_FILE="C:/ca/client.crt"
CA_FILE="c:/ca/ca.crt"
context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
context.check_hostname = False
context.load_cert_chain(certfile=CERT_FILE,keyfile=KEY_FILE)
context.load_verify_locations(CA_FILE)
context.verify_mode=ssl.CERT_REQUIRED
if __name__ == ‘__main__‘:
req = urllib2.Request(myurl)
try:
response = urllib2.urlopen(req,context=context)
#response = urllib2.urlopen(req)
print "HTTP return code:%d" % response.getcode()
strResult= response.read()
print strResult
except Exception ,ex:
print "Found Error in auth phase:%s" % str(ex)
五、其它
1、context.load_cert_chain(certfile=CERT_FILE,keyfile=KEY_FILE)
这个函数可以只提供一个certfile这一个文件,这时要求CERT_FILE里面包括私钥。其实证书文件和私钥文件都是简单的文本文件,你可以把私钥文件的内容复制到证书文件后面,这样你就可以在这个函数里面只提供一个文件了。
2、如何在request对象里面读出客户端信息呢?
所有的客户端信息都可以request对象里面读出,包括客户端的证书信息,看看下面,你所需要的基本都有了
def render_GET(self, request):
print request.content.read()
print request.getAllHeaders()
print request.getClientIP()
print request.getHost()
print request.transport.getPeer()
cl=request.transport.getPeerCertificate()
certificate = ssl.Certificate(cl)
print certificate.getIssuer()
print certificate.getSubject()
3、使用POST方法发送JSON数据
def urllib2_open(myurl):
headers = {‘Content-Type‘: ‘application/json‘}
data={‘test‘:‘hello‘}
req = urllib2.Request(myurl,headers=headers, data=json.dumps(data))
try:
response = urllib2.urlopen(req,context=context)
print "HTTP return code:%d" % response.getcode()
strResult= response.read()
print strResult
except Exception ,ex:
print "Found Error in auth phase:%s" % str(ex)
4、有一个地方我研究的不是特别清楚:默认情况下python是到哪里找权威CA证书的,是在某个文件夹还在通过注册表,还是直接通过操作系统提供的API。
写这么多总算把该写的写完了。之前我只是想用python访问一个HTTPS站点的。但当时很不顺利,不管怎么搞都出现SSLV3_ALERT_HANDSHAKE_FAILURE,当时在网上找了很多类似的解决办法,什么启用SSLV3啊、把加密方式改为ALL啊,另外也用了requests和pycurl等等第三方组件。但所有尝试无一成功。花了我好几天时间。最后终于发现原来这个站点需要双向认证。从这里面感觉自己对SSL了解太少了,走了太多弯路。于是就对SSL好好研究了一下,这才有了上面的内容。这些内容还是比较肤浅,都只限于应用方面,但对我来说足够了。分享给大家,让大家少走我走过的弯路。
原文地址:https://www.cnblogs.com/jiangzhaowei/p/9123467.html