MINA、Netty、Twisted一起學(十二):HTTPS
由於HTTPS協議是由HTTP協議加上SSL/TLS協議組合而成,在閱讀本文前可以先閱讀一下HTTP伺服器和SSL/TLS兩篇博文,本文中的程式碼也是由這兩篇博文中的程式碼組合而成。
HTTPS介紹
上一篇博文中介紹了SSL/TLS協議,我們平時接觸最多的SSL/TLS協議的應用就是HTTPS協議了,現在可以看到越來越多的網站已經是https開頭了,百度搜索也由曾經的http改為https。有關百度為什麼升級https推薦閱讀:http://zhanzhang.baidu.com/wiki/383
HTTPS即HTTP over SSL,實際上就是在原來HTTP協議的底層加入了SSL/TLS協議層,使得客戶端(例如瀏覽器)與伺服器之間的通訊加密傳輸,攻擊者無法竊聽和篡改。相對而言HTTP協議則是明文傳輸,安全性並不高。
HTTPS主要可以避免以下幾個安全問題:
1. 竊聽隱私:使用明文傳輸的HTTP協議,傳輸過程中的資訊都可能會被攻擊者竊取到,例如你登入網站的使用者名稱和密碼、在電商的購買記錄、搜尋記錄等,這就會造成例如賬號被盜、各種隱私洩漏的風險。而使用HTTPS對通訊內容加密過後,即使被攻擊者竊取到也無法破解其中的內容。
2. 篡改內容:HTTP使用明文傳輸,不但訊息會被竊取,還可能被篡改,例如常見的運營HTTP商劫持。你是否曾經瀏覽http協議的百度時,時不時會在頁面下方彈出小廣告,這些小廣告並不是百度放上去的,而是電信網通等運營商乾的,運營商通過篡改伺服器返回的頁面內容,加入一段HTML程式碼就可以輕鬆實現小廣告。而使用HTTPS的百度,就不再會出現這樣的小廣告,因為攻擊者無法對傳輸內容解密和加密,就無法篡改。
3. 冒充:例如DNS劫持,當你輸入一個http網址在瀏覽器開啟時,有可能開啟的是一個假的網站,連的並不是真網站的伺服器,假的網站可能給你彈出廣告,還可能讓你輸入使用者名稱密碼來盜取賬戶。使用HTTPS的話,伺服器都會有數字證書和私鑰,數字證書公開的,私鑰是網站伺服器私密的,假網站如果使用假的證書,瀏覽器會攔截並提示,如果使用真的證書,由於沒有私鑰也無法建立連線。
生成私鑰和證書
瀏覽器信任的證書一般是CA機構(證書授權中心)頒發的,證書有收費的也有免費的,本文使用免費證書用於測試。可以在騰訊雲https://www.qcloud.com/product/ssl申請一個免費證書,申請證書前需要提供一個域名,即該證書作用的域名。
我在本文中使用的是我自己的域名gw2.vsgames.cn在騰訊雲申請的免費證書,如果沒有自己的域名無法申請免費證書,可以在本文的末尾下載原始碼,其中有我生成好的證書用於測試。
證書生成好下載後包含一個私鑰檔案(.key)和一個證書檔案(.crt),騰訊雲生成的證書可以在Nginx目錄下找到這兩個檔案。
這兩個檔案在Twisted中可以直接使用,但是Java只能使用PKCS#8私鑰檔案,需要對上面的.key檔案用openssl進行轉換(如果你是在我提供的原始碼中獲取證書和私鑰檔案,我已經提供了轉換好的私鑰,可以跳過這一步)。
轉換成DER二進位制格式私鑰檔案,供MINA使用:
openssl pkcs8 -topk8 -inform PEM -in 2_gw2.vsgames.cn.key -outform DER -nocrypt -out private.der
轉換成PEM文字格式私鑰檔案,供Netty使用:
openssl pkcs8 -topk8 -inform PEM -in 2_gw2.vsgames.cn.key -outform PEM -nocrypt -out private.pem
除了在CA機構申請證書,還可以通過自簽名的方式生成私鑰和證書,上一篇博文中採用的就是這種方式。不過由於自簽名的證書不是CA機構頒發,不受瀏覽器信任,在瀏覽器開啟HTTPS地址時會有安全提示,測試時可以忽略提示。
HTTPS伺服器實現
MINA
public class MinaServer {
public static void main(String[] args) throws Exception {
String certPath = "/Users/wucao/Desktop/https/1_gw2.vsgames.cn_bundle.crt"; // 證書
String privateKeyPath = "/Users/wucao/Desktop/https/private.der"; // 私鑰
// 證書
// https://docs.oracle.com/javase/7/docs/api/java/security/cert/X509Certificate.html
InputStream inStream = null;
Certificate certificate = null;
try {
inStream = new FileInputStream(certPath);
CertificateFactory cf = CertificateFactory.getInstance("X.509");
certificate = cf.generateCertificate(inStream);
} finally {
if (inStream != null) {
inStream.close();
}
}
// 私鑰
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Files.readAllBytes(new File(privateKeyPath).toPath()));
PrivateKey privateKey = KeyFactory.getInstance("RSA").generatePrivate(keySpec);
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(null, null);
Certificate[] certificates = {certificate};
ks.setKeyEntry("key", privateKey, "".toCharArray(), certificates);
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(ks, "".toCharArray());
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), null, null);
IoAcceptor acceptor = new NioSocketAcceptor();
DefaultIoFilterChainBuilder chain = acceptor.getFilterChain();
chain.addLast("ssl", new SslFilter(sslContext)); // SslFilter + HttpServerCodec實現HTTPS
chain.addLast("codec", new HttpServerCodec());
acceptor.setHandler(new HttpServerHandle());
acceptor.bind(new InetSocketAddress(8080));
}
}
class HttpServerHandle extends IoHandlerAdapter {
@Override
public void exceptionCaught(IoSession session, Throwable cause)
throws Exception {
cause.printStackTrace();
}
@Override
public void messageReceived(IoSession session, Object message)
throws Exception {
if (message instanceof HttpRequest) {
// 請求,解碼器將請求轉換成HttpRequest物件
HttpRequest request = (HttpRequest) message;
// 獲取請求引數
String name = request.getParameter("name");
if(name == null) {
name = "World";
}
name = URLDecoder.decode(name, "UTF-8");
// 響應HTML
String responseHtml = "<html><body>Hello, " + name + "</body></html>";
byte[] responseBytes = responseHtml.getBytes("UTF-8");
int contentLength = responseBytes.length;
// 構造HttpResponse物件,HttpResponse只包含響應的status line和header部分
Map<String, String> headers = new HashMap<String, String>();
headers.put("Content-Type", "text/html; charset=utf-8");
headers.put("Content-Length", Integer.toString(contentLength));
HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SUCCESS_OK, headers);
// 響應BODY
IoBuffer responseIoBuffer = IoBuffer.allocate(contentLength);
responseIoBuffer.put(responseBytes);
responseIoBuffer.flip();
session.write(response); // 響應的status line和header部分
session.write(responseIoBuffer); // 響應body部分
}
}
}
Netty
public class NettyServer {
public static void main(String[] args) throws InterruptedException, SSLException {
File certificate = new File("/Users/wucao/Desktop/https/1_gw2.vsgames.cn_bundle.crt"); // 證書
File privateKey = new File("/Users/wucao/Desktop/https/private.pem"); // 私鑰
final SslContext sslContext = SslContextBuilder.forServer(certificate, privateKey).build();
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
// 加入SslHandler實現HTTPS
SslHandler sslHandler = sslContext.newHandler(ch.alloc());
pipeline.addLast(sslHandler);
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new HttpServerHandler());
}
});
ChannelFuture f = b.bind(8080).sync();
f.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
}
class HttpServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws UnsupportedEncodingException {
if (msg instanceof HttpRequest) {
// 請求,解碼器將請求轉換成HttpRequest物件
HttpRequest request = (HttpRequest) msg;
// 獲取請求引數
QueryStringDecoder queryStringDecoder = new QueryStringDecoder(request.uri());
String name = "World";
if(queryStringDecoder.parameters().get("name") != null) {
name = queryStringDecoder.parameters().get("name").get(0);
}
// 響應HTML
String responseHtml = "<html><body>Hello, " + name + "</body></html>";
byte[] responseBytes = responseHtml.getBytes("UTF-8");
int contentLength = responseBytes.length;
// 構造FullHttpResponse物件,FullHttpResponse包含message body
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.wrappedBuffer(responseBytes));
response.headers().set("Content-Type", "text/html; charset=utf-8");
response.headers().set("Content-Length", Integer.toString(contentLength));
ctx.writeAndFlush(response);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
Twisted
# -*- coding:utf-8 –*-
from twisted.internet import reactor, ssl
from twisted.web import server, resource
sslContext = ssl.DefaultOpenSSLContextFactory(
'/Users/wucao/Desktop/https/2_gw2.vsgames.cn.key', # 私鑰
'/Users/wucao/Desktop/https/1_gw2.vsgames.cn_bundle.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 "<html><body>Hello, " + name + "</body></html>"
site = server.Site(MainResource())
reactor.listenSSL(8080, site, sslContext)
reactor.run()
客戶端測試
由於瀏覽器就是最天然的HTTPS客戶端,這裡可以使用瀏覽器來測試。
首先,由於我的證書對應的域名是gw2.vsgames.cn,而伺服器程式碼執行在本機上,所以先需要配置hosts將域名解析到localhost上:
127.0.0.1 gw2.vsgames.cn
在瀏覽器開啟https://gw2.vsgames.cn:8080/?name=叉叉哥可以看到測試結果:
證書和私鑰正確的HTTPS伺服器,在Chrome瀏覽器左上角會有“安全”提示,其他瀏覽器也會有相應的提示。