在OSI參考模型中Http與Https均屬於應用層協議。Http即Hypertext Transfer Protocol,超文本傳輸協議;而Https為Secure Hypertext Transfer Protocol安全超文本傳輸協議,它是一個安全通信通道,基於HTTP開發,用於在客戶端與服務器之間交換信息,它使用安全套接字層SSL進行信息交換,簡單來說它就是HTTP的安全版。
Http默認使用80端口,Https使用443端口。
Http的數據在網絡上是明文傳輸,而Https則是通過加密後的傳輸,因此它相比http會更加安全,但是由於需要額外加解密操作,因為Https的效率沒有那麼高。在登錄Https站點和Http站點時,可以明顯感覺到性能差異。
當前許多的web站點登錄時都是采用普通的http進行傳輸,這種方式有著極大的安全隱患。當前web開發登錄系統常用的有以下四種方式:
1) 賬號和密碼完全沒有加密,明文傳送。這種方式的安全級別是最低的,它無疑是將自己的賬號和密碼直接暴露給別人,通過抓包工具(例:WireShark)可以很容易的截獲到賬號和密碼。
2) 密碼采用MD5或其它加密方式進行加密,聲稱不可破解。其實,完全沒有必要破解,只要截獲加密後的密碼串,就可以以你的身份訪問服務器,這樣也是可以通過認證授權的。這種方式在加密程度上有了一定程度的提高,但仍是不安全的。
3) 客戶端在登錄前去服務端拿一次密鑰,通過該密鑰進行加密,而服務器端的密鑰是隨機生成的,每次訪問均會用不同的密鑰。這種方式的安全性比較高。
4) 采用“安全性最高”的HTTPS方式傳輸,客戶端與服務端會經過認證,且中間的傳輸數據全部進行加密。之所以在安全性最高上加引號,是因為它也不是絕對安全的,比如前段時間Openssl曝出安全漏洞,大名鼎鼎的“心髒出血”,黑客利用它的一個memcpy的bug,可以從溢出的內存中拿到64K的用戶數據,導致用戶信息洩露。但是這個安全性級別相對前面三個是最高的,當前服務端的證書一年收費大約3-5千,用這點錢換來相對安全,是很劃算的事情了。
無論是Http還是Https都是基於TCP進行傳輸的,因此使用SOCKET模擬HTTP訪問web站點的方式,很簡單,就是將頭部數據拼接成數據包,發送給服務端,然後接收返回再解析就可以了。
其基本流程和編寫普通SOCKET通信是一樣的。Windows下的流程為:
a. WSAStartup對Winsock服務進行初始化
b. 建立socket套接字
c. connect連接服務端
d. send發送數據
e. recv接收數據
下面,以某站點的登錄為例,利用Fiddler抓到的POST的頭部信息如下:
這樣,我們就可以構建這樣的數據包發送出去,然後接收響應了,C++實現核心代碼請見下部分。
BOOL SocketClient::ConnectToServer(const CString strServerUrl, const int nPort) { cstrServerUrl = strServerUrl; nServerPort = nPort; BOOL bRet = FALSE; do { if (!InitializeContext()) { break; } if(!Connect()) { break; } bRet = TRUE; } while (FALSE); return bRet; } BOOL SocketClient::LoginToServer(const CString strUsername, const CString strPasswd) { cstrUserName = strUsername; cstrPassWord = strPasswd; BOOL bRet = FALSE; do { if (!SendPostData()) { break; } bRet = TRUE; } while (FALSE); return bRet; } BOOL SocketClient::LogoutOfServer() { return FALSE; } BOOL SocketClient::InitializeContext() { BOOL bRet = FALSE; wsaData = new WSADATA; WORD wVersion = MAKEWORD(2, 2); do { if(0 != WSAStartup(wVersion, wsaData)) { break; } if(LOBYTE( wsaData->wVersion ) != 2 || HIBYTE( wsaData->wVersion ) != 2 ) { WSACleanup(); break; } LPHOSTENT lpHostTent; lpHostTent = gethostbyname(cstrServerUrl); if (NULL == lpHostTent) { break; } socketClient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (socketClient == INVALID_SOCKET) { WSACleanup(); break; } socketAddrClient = new SOCKADDR_IN; socketAddrClient->sin_family = AF_INET; socketAddrClient->sin_port = htons(nServerPort); socketAddrClient->sin_addr = *((LPIN_ADDR)*lpHostTent->h_addr_list); memset(socketAddrClient->sin_zero, 0, sizeof(socketAddrClient->sin_zero)); bRet = TRUE; } while (FALSE); return bRet; } BOOL SocketClient::Connect() { BOOL bRet = FALSE; do { if (SOCKET_ERROR == connect(socketClient, (LPSOCKADDR)socketAddrClient, sizeof(SOCKADDR_IN))) { int nErrorCode = WSAGetLastError(); closesocket(socketClient); break; } bRet = TRUE; } while (FALSE); return bRet; } BOOL SocketClient::SendPostData() { CString cstrSendData; CString cstrSendParam = redirect=&username=+cstrUserName+&password=+cstrPassWord+&auto_login=checked&submit=%E7%99%BB%E5%BD%95; BOOL bRet = FALSE; CString cstrSendParamLen; cstrSendParamLen.Format(%d, cstrSendParam.GetLength()); cstrSendData = POST http://account.vsochina.com/user/login HTTP/1.1 ; cstrSendData += Host: account.vsochina.com ; cstrSendData += User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:35.0) Gecko/20100101 Firefox/35.0 ; cstrSendData += Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 ; cstrSendData += Accept-Language: zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3 ; cstrSendData += Accept-Encoding: gzip, deflate ; cstrSendData += DNT: 1 ; cstrSendData += Referer: http://account.vsochina.com/user/login ; cstrSendData += Connection: keep-alive ; cstrSendData += Content-Type: application/x-www-form-urlencoded ; cstrSendData += Content-Length: + cstrSendParamLen + ; cstrSendData += ; cstrSendData += cstrSendParam; CString cstrRecvData; do { if (-1 == send(socketClient, cstrSendData.GetBuffer(), cstrSendData.GetLength(), 0)) { break; } char recvData[1000] = {0}; int nRecvLen; while((nRecvLen = recv(socketClient, recvData, sizeof(recvData), 0)) > 0) { cstrRecvData += recvData; } if (cstrRecvData.GetLength() == 0) { break; } ParseCookieFromRecvData(cstrRecvData); //!判斷返回的COOKIE信息中,UID是否存在 if (cstrCookieUid.IsEmpty()) { break; } bRet = TRUE; } while (FALSE); return bRet; } void SocketClient::ParseCookieFromRecvData(const CString cstrRecvData) { listlstCookiesLine; //!存放Set-Cookie的一行,例:Set-Cookie: vso_uname=houqd_1111; CString cstrFind = Set-Cookie:; //!查找標記 CString cstrSeperator = ; //!以 分割號來分割字符串 int nPos = 0; int nStart = cstrRecvData.Find(cstrSeperator); while(nStart != -1) { CString cstrSessionLine = cstrRecvData.Mid(nPos, nStart - nPos + 1); if (cstrSessionLine.Find(cstrFind) != -1) { CString cstrRealRecord = cstrSessionLine.Right(cstrSessionLine.GetLength() - cstrFind.GetLength() - 3); list ::iterator it = find(lstCookiesLine.begin(), lstCookiesLine.end(), cstrRealRecord); if (it == lstCookiesLine.end()) { lstCookiesLine.push_back(cstrRealRecord); } } nPos = nStart; nStart = cstrRecvData.Find(cstrSeperator, nPos + 2); } //!根據每行獲取的cookie值,解析為key-value的形式 vector vecCookieSet; for (list ::iterator it = lstCookiesLine.begin(); it != lstCookiesLine.end(); it++) { CString cstrCookies = *it; CString cstrSeperator = ;; StaticUtility::StringSplit(cstrCookies, cstrSeperator, vecCookieSet); } vector vecTemp; for (vector ::iterator it = vecCookieSet.begin(); it != vecCookieSet.end(); it++) { vecTemp.clear(); CString cstrOneCookies = *it; CString cstrSeperator = =; StaticUtility::StringSplit(cstrOneCookies, cstrSeperator, vecTemp); CString cstrKey = vecTemp[0]; CString cstrVal = vecTemp[1]; if(cstrKey.Compare(vso_uid) == 0) { cstrCookieUid = cstrVal; break; } } }
通過接收來的頭部信息中,將cookie信息解析出來,就可以判斷是否登錄成功了。然後,如果有或許的操作,在請求中掛上這些cookie信息,就可以獲取想要的數據,完成想要的操作了。
HTTPS=HTTP + SSL,因此利用OpenSSL發送請求給HTTPS站點和第二章的SOCKET發送HTTP是非常相似的,只不過要在原生的套接字上套上SSL層,基本流程如下:
a. WSAStartup對Winsock服務進行初始化
b. 建立socket套接字
c. connect連接服務端
d. 建立SSL上下文
e. 建立SSL
f. 將SSL與前面建立的socket套接字綁定
g. SSL_write()發送數據
h. SSL_read()接收數據
下面以小米官網站點的登錄為例,來展示利用OpenSSL如何訪問HTTPS站點,模擬登陸,核心代碼,見下一章節。
#pragma comment( lib, libeay32.lib ) #pragma comment( lib, ssleay32.lib ) HttpsClient::HttpsClient(void): wsaData(NULL), socketAddrClient(NULL), ssl(NULL), sslCtx(NULL), sslMethod(NULL), serverCertification(NULL) { SSL_load_error_strings(); SSLeay_add_ssl_algorithms(); } HttpsClient::~HttpsClient(void) { //!清理打開的句柄 if (NULL != ssl) { SSL_shutdown(ssl); closesocket(socketClient); SSL_free(ssl); ssl = NULL; } if (NULL != sslCtx) { SSL_CTX_free(sslCtx); } WSACleanup(); } BOOL HttpsClient::ConnectToServer(const CString strServerUrl, const int nPort) { cstrServerUrl = strServerUrl; nServerPort = nPort; BOOL bRet = FALSE; do { if (!InitializeSocketContext()) { break; } if (!SocketConnect()) { break; } if (!InitializeSslContext()) { break; } if (!SslConnect()) { break; } bRet = TRUE; } while (FALSE); return bRet; } BOOL HttpsClient::LoginToServer(const CString strUsername, const CString strPasswd) { cstrUserName = strUsername; cstrPassWord = strPasswd; BOOL bRet = FALSE; do { if (!SendLoginPostData()) { break; } CString cstrRecvData; RecvLoginPostData(cstrRecvData); if (cstrRecvData.GetLength() == 0) { break; } ParseCookieFromRecvData(cstrRecvData); if (cstrCookieUid.IsEmpty() || cstrCookieUid.Compare(EXPIRED) == 0) { break; } bRet = TRUE; } while (FALSE); return bRet; } BOOL HttpsClient::LogoutOfServer() { return FALSE; } BOOL HttpsClient::InitializeSocketContext() { //!初始化winSocket環境 BOOL bRet = FALSE; wsaData = new WSADATA; WORD wVersion = MAKEWORD(2, 2); do { if(0 != WSAStartup(wVersion, wsaData)) { break; } if(LOBYTE( wsaData->wVersion ) != 2 || HIBYTE( wsaData->wVersion ) != 2 ) { WSACleanup(); break; } LPHOSTENT lpHostTent; lpHostTent = gethostbyname(cstrServerUrl); if (NULL == lpHostTent) { break; } socketClient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (socketClient == INVALID_SOCKET) { WSACleanup(); break; } socketAddrClient = new SOCKADDR_IN; socketAddrClient->sin_family = AF_INET; socketAddrClient->sin_port = htons(nServerPort); socketAddrClient->sin_addr = *((LPIN_ADDR)*lpHostTent->h_addr_list); memset(socketAddrClient->sin_zero, 0, sizeof(socketAddrClient->sin_zero)); bRet = TRUE; } while (FALSE); return bRet; } BOOL HttpsClient::SocketConnect() { //!原生socket連接 BOOL bRet = FALSE; do { if (SOCKET_ERROR == connect(socketClient, (LPSOCKADDR)socketAddrClient, sizeof(SOCKADDR_IN))) { int nErrorCode = WSAGetLastError(); closesocket(socketClient); break; } bRet = TRUE; } while (FALSE); return bRet; } BOOL HttpsClient::InitializeSslContext() { //!SSL通信初始化 BOOL bRet = FALSE; do { sslMethod = SSLv23_client_method(); if(NULL == sslMethod) { break; } sslCtx = SSL_CTX_new(sslMethod); if (NULL == sslCtx) { break; } ssl = SSL_new(sslCtx); if (NULL == ssl) { break; } bRet = TRUE; } while (FALSE); return bRet; } BOOL HttpsClient::SslConnect() { //!SSL綁定原生socket,並連接服務器 BOOL bRet = FALSE; do { SSL_set_fd(ssl, socketClient); int nRet = SSL_connect(ssl); if (-1 == nRet) { break; } bRet = TRUE; } while (FALSE); return bRet; } BOOL HttpsClient::SslGetCipherAndCertification() { BOOL bRet = FALSE; do { cstrSslCipher = SSL_get_cipher(ssl); serverCertification = SSL_get_certificate(ssl); if (NULL == serverCertification) { break; } cstrSslSubject = X509_NAME_oneline(X509_get_subject_name(serverCertification), 0, 0); cstrSslIssuer = X509_NAME_oneline(X509_get_issuer_name(serverCertification), 0, 0); X509_free(serverCertification); bRet = TRUE; } while (FALSE); return bRet; } BOOL HttpsClient::SendLoginPostData() { CString cstrSendData; //CString cstrSendParam = redirect=&username=+cstrUserName+&password=+cstrPassWord+&auto_login=checked&submit=%E7%99%BB%E5%BD%95; CString cstrSendParam = user=+cstrUserName+&_json=true&pwd=+cstrPassWord+&callback=http%3A%2F%2Forder.mi.com%2Flogin%2Fcallback%3Ffollowup%3Dhttp%253A%252F%252Fwww.mi.com%252F%26sign%3DNWU4MzRmNjBhZmU4MDRmNmZkYzVjMTZhMGVlMGFmMTllMGY0ZTNhZQ%2C%2C&sid=mi_eshop&qs=%253Fcallback%253Dhttp%25253A%25252F%25252Forder.mi.com%25252Flogin%25252Fcallback%25253Ffollowup%25253Dhttp%2525253A%2525252F%2525252Fwww.mi.com%2525252F%252526sign%25253DNWU4MzRmNjBhZmU4MDRmNmZkYzVjMTZhMGVlMGFmMTllMGY0ZTNhZQ%25252C%25252C%2526sid%253Dmi_eshop&hidden=&_sign=%2Bw73Dr7cAfRlMfOR6fW%2BF0QG4jE%3D&serviceParam=%7B%22checkSafePhone%22%3Afalse%7D&captCode=; BOOL bRet = FALSE; CString cstrSendParamLen; cstrSendParamLen.Format(%d, cstrSendParam.GetLength()); cstrSendData = POST https://account.xiaomi.com/pass/serviceLoginAuth2 HTTP/1.1 ; cstrSendData += Host: account.xiaomi.com ; cstrSendData += User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:35.0) Gecko/20100101 Firefox/35.0 ; cstrSendData += Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 ; cstrSendData += Accept-Language: zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3 ; cstrSendData += Accept-Encoding: gzip, deflate ; cstrSendData += DNT: 1 ; cstrSendData += Content-Type: application/x-www-form-urlencoded; charset=UTF-8 ; cstrSendData += Referer: https://account.xiaomi.com/pass/serviceLogin?callback=http%3A%2F%2Forder.mi.com%2Flogin%2Fcallback%3Ffollowup%3Dhttp%253A%252F%252Fwww.mi.com%252Findex.html%26sign%3DNDRhYjQwYmNlZTg2ZGJhZjI0MTJjY2ZiMTNiZWExODMwYjkwNzg2ZQ%2C%2C&sid=mi_eshop ; cstrSendData += Content-Length: + cstrSendParamLen + ; cstrSendData += Connection: keep-alive ; cstrSendData += ; cstrSendData += cstrSendParam; CString cstrRecvData; do { int nRet = SSL_write(ssl, cstrSendData, cstrSendData.GetLength()); if(-1 == nRet) { break; } bRet = TRUE; } while (FALSE); return bRet; } void HttpsClient::RecvLoginPostData(CString &cstrRecvData) { BOOL bRet = FALSE; do { TIMEVAL tval; tval.tv_sec = 20; tval.tv_usec = 0; while(TRUE) { FD_SET fds; FD_ZERO(&fds); FD_SET(socketClient, &fds); char recvData[1000] = {0}; int nRecvLen; //int nSelect = select(FD_SETSIZE, &fds, NULL, NULL, &tval); //if (1 != nSelect) //{ // break; //} int nErr = SSL_read(ssl, recvData, sizeof(recvData)); if (nErr <= 0) { break; } cstrRecvData += recvData; } if (cstrRecvData.GetLength() == 0) { break; } bRet = TRUE; } while (FALSE); } void HttpsClient::ParseCookieFromRecvData(const CString cstrRecvData) { listlstCookiesLine; //!存放Set-Cookie的一行,例:Set-Cookie: vso_uname=houqd_1111; CString cstrFind = Set-Cookie:; //!查找標記 CString cstrSeperator = ; //!以 分割號來分割字符串 int nPos = 0; int nStart = cstrRecvData.Find(cstrSeperator); while(nStart != -1) { CString cstrSessionLine = cstrRecvData.Mid(nPos, nStart - nPos + 1); if (cstrSessionLine.Find(cstrFind) != -1) { CString cstrRealRecord = cstrSessionLine.Right(cstrSessionLine.GetLength() - cstrFind.GetLength() - 3); list ::iterator it = find(lstCookiesLine.begin(), lstCookiesLine.end(), cstrRealRecord); if (it == lstCookiesLine.end()) { lstCookiesLine.push_back(cstrRealRecord); } } nPos = nStart; nStart = cstrRecvData.Find(cstrSeperator, nPos + 2); } //!根據每行獲取的cookie值,解析為key-value的形式 vector vecCookieSet; for (list ::iterator it = lstCookiesLine.begin(); it != lstCookiesLine.end(); it++) { CString cstrCookies = *it; CString cstrSeperator = ;; StaticUtility::StringSplit(cstrCookies, cstrSeperator, vecCookieSet); } vector vecTemp; for (vector ::iterator it = vecCookieSet.begin(); it != vecCookieSet.end(); it++) { vecTemp.clear(); CString cstrOneCookies = *it; CString cstrSeperator = =; StaticUtility::StringSplit(cstrOneCookies, cstrSeperator, vecTemp); CString cstrKey; CString cstrVal; if (vecTemp.size() == 2) { cstrKey = vecTemp[0]; cstrVal = vecTemp[1]; } if(cstrKey.Compare(userId) == 0) { cstrCookieUid = cstrVal; break; } } }
同理,判斷登錄也是在返回的信息中拿cookie信息,再進行下一步操作。