Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
見的https網站做的是服務端認證(server authentication),瀏覽器通過證書判斷你所訪問的https://baidu.com是否真的是百度,而不是其他人偽造的網站。同時還對流量加密,防止別人竊聽你的流量。
tls還可以做客戶端認證(client authentication),即服務端判斷客戶端是否為其所信任的客戶端。由此可見,客戶端認證用于那些需要受控訪問服務端。
在數據中心中,有些服務是非常敏感的,那么我們要做到:
所以很明顯,前兩個問題可以通過服務端認證解決,最后一個問題可以通過客戶端認證解決。順便一提,如果要使用客戶端認證就必須使用服務端認證。
先來講講概念然后舉個tomcat的例子講講怎么做。
概念
服務端認證
不論是做Server authentication還是Client authentication都需要證書。證書的來源有兩種:
在一切可能的情況下都應該使用權威CA簽發的證書,為什么這么建議?因為這里牽涉到一個信任問題,瀏覽器、編程語言SDK和某些工具都維護了一個信任CA證書清單,只要是由這些CA簽發的證書那就信任,否則就不信任。而這個鏈條是可以多級的,這里就不展開了。你只需要知道由信任CA簽發的所有證書都是可信的。比如JDK自帶的信任CA證書可以通過下面命令看到:
keytool -list -keystore $JAVA_HOME/jre/lib/security/cacerts verisignclass2g2ca [jdk], 2016-8-25, trustedCertEntry, 證書指紋 (SHA1): B3:EA:C4:47:76:C9:C8:1C:EA:F2:9D:95:B6:CC:A0:08:1B:67:EC:9D digicertassuredidg3 [jdk], 2016-8-25, trustedCertEntry, 證書指紋 (SHA1): F5:17:A2:4F:9A:48:C6:C9:F8:A2:00:26:9F:DC:0F:48:2C:AB:30:89 verisignuniversalrootca [jdk], 2016-8-25, trustedCertEntry, ...
讓你輸密碼的時候輸入changeit。
如果這個證書不是由信任CA簽發的(比如自己簽發)會發生什么?瀏覽器、編程語言SDK、你所使用的工具會報告以下錯誤:
curl:
curl: (60) SSL certificate problem: self signed certificate in certificate chain
Java:
Exception in thread "main" javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target at sun.security.ssl.Alerts.getSSLException(Alerts.java:192) at sun.security.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1964) at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:328) at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:322) at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1614) ...
瀏覽器:
這個錯誤實際上就是在告訴你這個證書不可信任,可能是一個偽造站點,讓你小心點兒。如果這個證書由權威CA簽發,那么就沒有這個問題了。但是權威CA簽發的證書要求申請人擁有域名,如果你這個服務是內部使用的沒有域名,那就只能自己簽發了。那么如何解決上面的問題呢?你得把自己簽發的證書加入到信任CA證書清單里。
下圖是權威CA簽發證書的示例:
可以看到客戶端有一個truststore,這個就是存放信任CA證書的地方,服務端有一個keystore,存放的自己的證書及對應的私鑰。
下圖是自簽發證書的示例:
在上面可以看到我們自己成為了一個Root CA,把它放到客戶端的truststore里。
客戶端認證
前面講過客戶端認證是服務端來驗證客戶端是否可信的機制,其實做法和服務端認證類似只不過方向相反。客戶端認證大多數情況下只能是自簽發的(因為沒有域名),雖然不是不可以從權威CA簽發但是存在一些問題。下面解釋為什么,假設權威CA是let's encrypt,然后服務端信任它簽發的所有證書。但是let's encrypt是阿貓阿狗都可以申請的,現在有一個黑客申請了這個證書,然后請求你的服務端,服務端就認可了。
上面這個問題可以用這個方法解決:比如你用let's encrypt申請了A證書,黑客用let's encrypt申請了B證書,你的服務端的truststore只信任A證書,那么黑客用B證書訪問你的時候就會被拒絕。但是這就帶來另一個問題,比如你在開發的時候客戶端證書有這么幾套:生產用、調試用、開發用,那么每次客戶端簽發一個證書都要更新到你的服務器的truststore里,這也太麻煩了。
所以結合安全性和便利性,我們把自己變成Root CA,然后服務端信任它,這樣一來服務端就可以在開發的時候把Client Root CA內置進去,大大減輕了維護truststore的工作量,看下圖:
用Tomcat舉個例子
下面舉一個Tomcat做客戶端認證的例子,因為是測試用,所以服務端認證也是用的自簽發證書。
我們用了cfssl這個工具來生成證書。
服務端
先弄一套目錄:
# 放自簽發的服務端CA根證書 server-secrets/ca # 放自簽發的服務端的證書 server-secrets/cert # 放服務端的keystore和truststore server-secrets/jks
生成自簽名CA證書
新建文件:server-secrets/ca/server-root-ca-csr.json
內容如下:
{ "key": { "algo": "rsa", "size": 2048 }, "names": [ { "O": "Company", "OU": "Datacenter", "L": "Shanghai", "ST": "Shanghai", "C": "CN" } ], "CN": "server-root-ca" }
運行下面命令生成Server ROOT CA證書:
cfssl gencert --initca=true ./server-root-ca-csr.json | cfssljson --bare server-root-ca
會得到下面幾個文件:
server-secrets/ca/ ├── server-root-ca-key.pem ├── server-root-ca.csr └── server-root-ca.pem
用下面命令驗證證書:
openssl x509 -in ./server-root-ca.pem -text -noout Certificate: Data: Version: 3 (0x2) Serial Number: 0c:8a:1a:ca:da:fa:4c:17:6c:1f:42:40:4c:f1:90:f4:fd:1d:fe:58 Signature Algorithm: sha256WithRSAEncryption Issuer: C=CN, ST=Shanghai, L=Shanghai, O=Company, OU=Datacenter, CN=server-root-ca Validity Not Before: Mar 27 05:14:00 2019 GMT Not After : Mar 25 05:14:00 2024 GMT Subject: C=CN, ST=Shanghai, L=Shanghai, O=Company, OU=Datacenter, CN=server-root-ca
可以看到簽發人和被簽發人是同一個。
生成自簽發證書
新建文件 server-secrets/cert/server-gencert.json,內容如下:
{ "signing": { "default": { "usages": [ "signing", "key encipherment", "server auth" ], "expiry": "87600h" } } }
可以看到我們會生成用來做server auth的證書。
新建文件 server-secrets/cert/demo-csr.json,內容如下:
{ "key": { "algo": "rsa", "size": 2048 }, "names": [ { "O": "Company", "OU": "Datacenter", "L": "Shanghai", "ST": "Shanghai", "C": "CN" } ], "CN": "server-demo", "hosts": [ "127.0.0.1", "localhost" ] }
看上面的hosts,你可以根據自己的需要填寫域名或IP,這里因為是本地演示所以是127.0.0.1和localhost。
運行下面命令生成證書
cfssl gencert \ --ca ../ca/server-root-ca.pem \ --ca-key ../ca/server-root-ca-key.pem \ --config ./server-gencert.json \ ./demo-csr.json | cfssljson --bare ./demo
得到文件:
server-secrets/cert/ ├── demo-key.pem ├── demo.csr └── demo.pem
驗證結果:
openssl x509 -in ./demo.pem -text -noout Certificate: Data: Version: 3 (0x2) Serial Number: 1d:d0:51:97:6c:ce:ea:29:2a:f4:3b:3c:48:a3:69:b0:ef:f3:26:7b Signature Algorithm: sha256WithRSAEncryption Issuer: C=CN, ST=Shanghai, L=Shanghai, O=Company, OU=Datacenter, CN=server-root-ca Validity Not Before: Mar 27 05:17:00 2019 GMT Not After : Mar 24 05:17:00 2029 GMT Subject: C=CN, ST=Shanghai, L=Shanghai, O=Company, OU=Datacenter, CN=server-demo
可以看到簽發者是server-root-ca,Subject是server-demo。
將證書導入keystore
到 server-secrets/jks,執行下面命令生成pkcs12格式的keystore(JDK識別這個格式)
openssl pkcs12 -export \ -in ../cert/demo.pem \ -inkey ../cert/demo-key.pem \ -out server-demo.keystore \ -name server-demo \ -CAfile ../ca/server-root-ca.pem \ -caname root -chain
過程中會讓你輸入密碼,你就輸入:server-demo-ks。
得到文件:
server-secrets/jks/ └── server-demo.keystore
用JDK提供的keytool看看里面的內容:
keytool -list -keystore server-demo.keystore server-demo, 2019-3-27, PrivateKeyEntry, 證書指紋 (SHA1): B2:E5:46:63:BB:00:E7:82:48:A4:2F:EC:01:41:CE:B4:4B:CE:68:7A
讓你輸入密碼的時候就輸入:server-demo-ks。
客戶端
先弄一套目錄:
# 放自簽發的客戶端CA根證書 client-secrets/ca # 放自簽發的客戶端的證書 client-secrets/cert # 放客戶端的keystore和truststore client-secrets/jks
生成自簽名CA證書
新建文件 client-secrets/ca/client-root-ca-csr.json:
{ "key": { "algo": "rsa", "size": 2048 }, "names": [ { "O": "Company", "OU": "Datacenter", "L": "Shanghai", "ST": "Shanghai", "C": "CN" } ], "CN": "client-root-ca" }
運行下面命令生成Client ROOT CA證書:
cfssl gencert --initca=true ./client-root-ca-csr.json | cfssljson --bare client-root-ca
會得到下面幾個文件:
client-secrets/ca/ ├── client-root-ca-key.pem ├── client-root-ca.csr └── client-root-ca.pem
用下面命令驗證證書:
openssl x509 -in ./client-root-ca.pem -text -noout Certificate: Data: Version: 3 (0x2) Serial Number: 7e:fc:f3:53:07:1a:17:ae:24:34:d5:1d:00:02:d6:e4:24:09:92:12 Signature Algorithm: sha256WithRSAEncryption Issuer: C=CN, ST=Shanghai, L=Shanghai, O=Company, OU=Datacenter, CN=client-root-ca Validity Not Before: Mar 27 05:20:00 2019 GMT Not After : Mar 25 05:20:00 2024 GMT Subject: C=CN, ST=Shanghai, L=Shanghai, O=Company, OU=Datacenter, CN=client-root-ca
可以看到簽發人和被簽發人是同一個。
生成自簽發證書
新建文件 client-secrets/cert/client-gencert.json,內容如下:
{ "signing": { "default": { "usages": [ "signing", "key encipherment", "client auth" ], "expiry": "87600h" } } }
可以看到我們會生成用來做client auth的證書。
新建文件 client-secrets/cert/demo-csr.json,內容如下:
{ "key": { "algo": "rsa", "size": 2048 }, "names": [ { "O": "Company", "OU": "Datacenter", "L": "Shanghai", "ST": "Shanghai", "C": "CN" } ], "CN": "client-demo" }
這里沒有hosts,這是因為我們不需要用這個證書來做服務端認證。
運行下面命令生成證書
cfssl gencert \ --ca ../ca/client-root-ca.pem \ --ca-key ../ca/client-root-ca-key.pem \ --config ./client-gencert.json \ ./demo-csr.json | cfssljson --bare ./demo
得到文件:
client-secrets/cert/ ├── demo-key.pem ├── demo.csr └── demo.pem
驗證結果:
openssl x509 -in ./demo.pem -text -noout Certificate: Data: Version: 3 (0x2) Serial Number: 6e:50:e2:2c:02:bb:ef:fd:03:d9:2c:0a:8f:ba:90:65:fb:c4:b5:75 Signature Algorithm: sha256WithRSAEncryption Issuer: C=CN, ST=Shanghai, L=Shanghai, O=Company, OU=Datacenter, CN=client-root-ca Validity Not Before: Mar 27 05:21:00 2019 GMT Not After : Mar 24 05:21:00 2029 GMT Subject: C=CN, ST=Shanghai, L=Shanghai, O=Company, OU=Datacenter, CN=client-demo
可以看到簽發者是client-root-ca,Subject是client-demo。
將證書導入keystore
到 client-secrets/jks,執行下面命令生成pkcs12格式的keystore(JDK識別這個格式)
openssl pkcs12 -export \ -in ../cert/demo.pem \ -inkey ../cert/demo-key.pem \ -out client-demo.keystore \ -name client-demo \ -CAfile ../ca/client-root-ca.pem \ -caname root -chain
過程中會讓你輸入密碼,你就輸入:client-demo-ks。
得到文件:
client-secrets/jks/ └── client-demo.keystore
用JDK提供的keytool看看里面的內容:
keytool -list -keystore client-demo.keystore client-demo, 2019-3-27, PrivateKeyEntry, 證書指紋 (SHA1): 83:AE:0E:5E:0C:CE:86:C9:D1:84:D7:6F:87:F3:76:1F:B4:3E:46:31
讓你輸入密碼的時候就輸入:client-demo-ks。
兩端互信
好了,到此為止server和client的證書都已經生成了,接下來只需要將各自的root-ca添加到彼此都truststore中。
把server-root-ca導入到client的truststore中
cd client-secrets/jks keytool -importcert \ -alias server-root-ca \ -storetype pkcs12 \ -keystore client.truststore \ -storepass client-ts \ -file ../../server-secrets/ca/server-root-ca.pem -noprompt
注意上面的-storepass參數,這個是trustore的密碼:client-ts。
得到文件:
client-secrets/jks/ └── client.truststore
用JDK提供的keytool看看里面的內容:
keytool -list -keystore client.truststore server-root-ca, 2019-3-27, trustedCertEntry, 證書指紋 (SHA1): 75:E3:78:97:85:B2:29:38:25:3C:FD:EC:68:97:9B:78:A0:5F:BB:9D
讓你輸入密碼的時候就輸入:client-ts。
把client-root-ca導入到server的truststore中
cd server-secrets/jks keytool -importcert \ -alias client-root-ca \ -storetype pkcs12 \ -keystore server.truststore \ -storepass server-ts \ -file ../../client-secrets/ca/client-root-ca.pem -noprompt
注意上面的-storepass參數,這個是trustore的密碼:server-ts。
得到文件:
server-secrets/jks/ └── server.truststore
用JDK提供的keytool看看里面的內容:
keytool -list -keystore server.truststore client-root-ca, 2019-3-27, trustedCertEntry, 證書指紋 (SHA1): 1E:95:2C:12:AA:7E:6D:E7:74:F1:83:C2:B8:73:6F:EE:57:FB:CA:46
讓你輸入密碼的時候就輸入:server-ts。
配置Tomcat
好了,我們現在client和server都有了自己證書放在了自己的keystore中,而且把彼此的root-ca證書放到了自己的truststore里。現在我們弄一個tomcat作為server,然后為他配置SSL。
修改tomcat/conf/server.xml,添加如下Connector:
<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol" maxThreads="150" SSLEnabled="true"> <SSLHostConfig certificateVerification="required" truststoreFile="/path/to/server-secrets/jks/server.truststore" truststorePassword="server-ts" truststoreType="PKCS12"> <Certificate certificateKeyAlias="server-demo" certificateKeystoreFile="/path/to/server-secrets/demo-jks/server-demo.keystore" certificateKeystoreType="PKCS12" certificateKeystorePassword="server-demo-ks" type="RSA" /> </SSLHostConfig> </Connector>
可以看到我們開啟了客戶端認證certificateVerification="required",也開啟了服務端認證<Certificate>。記得修改上面的keystore和truststore的路徑。
修改tomcat/conf/web.xml,添加如下元素:
<security-constraint> <web-resource-collection> <web-resource-name>Automatic Forward to HTTPS/SSL</web-resource-name> <url-pattern>/*</url-pattern> </web-resource-collection> <user-data-constraint> <transport-guarantee>CONFIDENTIAL</transport-guarantee> </user-data-constraint> </security-constraint>
這個作用是當訪問8080端口時,都跳轉到8443端口,強制走HTTPS。
啟動tomcat:
tomcat/bin/catalina.sh run
用curl測試
好了,我們現在用curl來測試訪問一下:
curl https://localhost:8443/ curl: (60) SSL certificate problem: self signed certificate in certificate chain ...
看到curl說服務端用的是一個自簽發的證書,不可信,也就是說服務端認證失敗。添加--insecure試試:
curl --insecure https://localhost:8443/ curl: (35) error:1401E412:SSL routines:CONNECT_CR_FINISHED:sslv3 alert bad certificate
這里就說明客戶端認證失敗。
所以如果要正確訪問得像下面這樣,指定server-root-ca證書,以及客戶端自己簽發的證書及private key:
curl --cacert server-secrets/ca/server-root-ca.pem \ --key client-secrets/cert/demo-key.pem \ --cert client-secrets/cert/demo.pem \ https://localhost:8443/ <!DOCTYPE html> <html lang="en"> ...
Httpclient測試
我們現在用Httpclient來訪問看看。pom.xml中添加依賴:
<dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.7</version> </dependency>
Java代碼,記得把文件路徑改掉:
import org.apache.http.HttpEntity; import org.apache.http.HttpException; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.ssl.SSLContexts; import org.apache.http.util.EntityUtils; import javax.net.ssl.SSLContext; import java.io.File; import java.io.IOException; public class Client { public static void main(String[] args) throws Exception { SSLContext sslcontext=SSLContexts.custom() .loadTrustMaterial( new File("/path/to/client-secrets/demo-jks/client.truststore"), "client-ts".toCharArray() ) .loadKeyMaterial( new File("/path/to/client-secrets/demo-jks/client-demo.keystore"), "client-demo-ks".toCharArray(), "client-demo-ks".toCharArray()) .build(); SSLConnectionSocketFactory sslsf=new SSLConnectionSocketFactory( sslcontext, SSLConnectionSocketFactory.getDefaultHostnameVerifier()); CloseableHttpClient httpclient=HttpClients.custom() .setSSLSocketFactory(sslsf) .build(); HttpGet httpGet=new HttpGet("https://localhost:8443"); CloseableHttpResponse response=httpclient.execute(httpGet); try { System.out.println(response.getStatusLine()); HttpEntity entity=response.getEntity(); System.out.println(EntityUtils.toString(entity)); } finally { response.close(); } } }
安全性考慮
關于反向代理
因為服務端認證所需要的證書直接配置在Tomcat上的,因此在做反向代理的時候不能使用SSL Termination模式,而是得使用SSL Passthrough模式。
其他語言、SDK、工具
上面講的方法不是只適用于Tomcat和Httpclient的,TLS的服務端認證與客戶端認證應該在絕大部分的語言、SDK、類庫都有支持,請自行參閱文檔實踐。文中的keystore和truststore是Java特有的,不過不必迷惑,因為它們僅僅起到一個存放證書和private key的保險箱,有些語言或工具則是直接使用證書和private key,比如前面提到的curl。
歡迎工作一到五年的Java工程師朋友們加入Java程序員開發: 721575865
群內提供免費的Java架構學習資料(里面有高可用、高并發、高性能及分布式、Jvm性能調優、Spring源碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)合理利用自己每一分每一秒的時間來學習提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!
在統計用戶在人數的時候,我們用到了監聽器,監聽器大致分為以下三種:
這種統計辦法是錯誤的認為每次刷新頁面后進行進行一次的count++運算
import javax.servlet.*;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSessionAttributeListener;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import javax.servlet.http.HttpSessionBindingEvent;
@WebListener()
public class MyRequestListener implements ServletRequestListener{
private ServletContext sc;
private Integer count;
@Override
//請求被初始化 Request
public void requestInitialized(ServletRequestEvent sre) {
//獲取全局域
sc=sre.getServletContext();
//將count從全局域中獲取出來
count=(Integer) sc.getAttribute("count");
System.out.println(count);
count++;
System.out.println(count);
sc.setAttribute("count",count);
}
}
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSessionAttributeListener;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import javax.servlet.http.HttpSessionBindingEvent;
@WebListener()
public class MyServletContextListener implements ServletContextListener{
private ServletContext sc;
@Override
//Application被初始化的時候創建
public void contextInitialized(ServletContextEvent sce) {
Integer count=0;
//獲取全局域
sc=sce.getServletContext();
//將count放入到全局域中
sc.setAttribute("count",count);
}
}
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>$Title$</title>
</head>
<body>
<center><h1>You are the ${applicationScope.count} customer to visit. </h1></center>
</body>
</html>
這種錯誤地做法導致的是每刷新一次頁面 就會導致count進行累加操作,最終產生錯誤的在線人數,所以此時想到不應該監聽Request域,而應該監聽Session域。
在第二次監聽Session域之后,發現每次刷新頁面后不改變count但是在啟動不同的瀏覽器后count++會實現,但是,這樣做并不是我們要統計的在線人數,所以此種做法錯誤。由于代碼只是將原來寫在Request監聽器中的代碼轉移到Session監聽器中,所以其他沒變的代碼將不重復。
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSessionAttributeListener;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import javax.servlet.http.HttpSessionBindingEvent;
@WebListener()
public class MySessionListener implements HttpSessionListener{
private ServletContext sc;
private Integer count;
@Override
//當對話產生時激活此方法
public void sessionCreated(HttpSessionEvent se) {
sc=se.getSession().getServletContext();
count=(Integer) sc.getAttribute("count");
count++;
sc.setAttribute("count",count);
}
}
這時我們發現對于在線人數的統計,不是網頁訪問的次數,也不是瀏覽器打開的個數,對需求的理解的錯誤理解。所以正確的做法是統計其IP的數量,這樣的話,不管你在一臺電腦上開啟多少客戶端,都會只有一個。
統計其IP的數量,將IP的數量作為當前的在線人數,那么如何統計IP的數量呢?這樣將會導出以下問題:
現在來解決這些問題:
到了這里又冒出來一個問題集合(List)放到哪個域里呢?
ServletContext域
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSessionAttributeListener;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import javax.servlet.http.HttpSessionBindingEvent;
import java.util.ArrayList;
import java.util.List;
@WebListener()
public class MyServletContextListener implements ServletContextListener{
private ServletContext sc;
@Override
//Application被初始化的時候創建
public void contextInitialized(ServletContextEvent sce) {
//創建一個鏈表來存儲IP
List<String> ips=new ArrayList<>();
sc=sce.getServletContext();
//將創建好的鏈表對象,放到Application域中
sc.setAttribute("ips",ips);
}
}
由于IP只能在Request域中獲取,所以遍歷判斷在Request域中進行。
import javax.servlet.*;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.*;
import java.util.List;
@WebListener()
public class MyRequestListener implements ServletRequestListener{
private HttpServletRequest sr;
private String clientIp;
private ServletContext sc;
private List<String> ips;
private HttpSession session;
@Override
//請求被初始化 Request
public void requestInitialized(ServletRequestEvent sre) {
//從請求域中獲取IP
sr=(HttpServletRequest) sre.getServletRequest();
clientIp=sr.getRemoteAddr();
session=sr.getSession();
session.setAttribute("clientIp",clientIp);
//測試
// System.out.println("clientIp="+ clientIp);
//獲取Application域中的List
sc=sre.getServletContext();
ips=(List<String>) sc.getAttribute("ips");
//遍歷ips
for (String ip :
ips) {
if (clientIp.equals(ip))
return;
}
ips.add(clientIp);
sc.setAttribute("ips",ips);
}
}
因為要統計在線人數,所以要設置退出按鈕,點擊退出按鈕之后,因為要從List域中移除,所以使用Session域監聽器來判斷session回話的關閉
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.*;
import java.util.List;
@WebListener()
public class MySessionListener implements HttpSessionListener{
private ServletContext sc;
private List<String> ips;
private HttpSession session;
private Object clientIp;
@Override
public void sessionDestroyed(HttpSessionEvent se) {
sc=se.getSession().getServletContext();
ips=(List<String>) sc.getAttribute("ips");
session=se.getSession();
clientIp=session.getAttribute("clientIp");
//刪除ip,如何獲取IP,但是不可以從session獲取到IP
//因為Session獲取不到Request
//一個Session包含多個Request
//一個Request只對應一個Session 所以獲取不到,這時只能先從Request域中獲取到的ips,放置到Session域
//然后從Session 域中讀取
ips.remove(clientIp);
// session一失效就馬上將此IP從鏈表中移除是錯誤的
//應該看此IP是否有另外的回話存在,如果有的話不能刪除
}
}
此處代碼是頁面點擊關閉后,激活的退出方法
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
@WebServlet(name="LogoutServlet",urlPatterns="/logoutServlet")
public class LogoutServlet extends HttpServlet {
private HttpSession session;
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//從域中獲取一個session,設置為false 如果域中存在一個session,則直接獲取,如果不存在,則返回一個空的session
session=request.getSession(false);
if (session !=null){
//使session失效
session.invalidate();
//失效后,需要進行的操作,List鏈表中需要減去,用到了Session域監聽器
}
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doPost(request,response);
}
}
在jsp頁面進行讀取的時候,因為ips是以List鏈表的形式存在的,所以要想判斷當前在線人數,所以必須要判斷鏈表的長度,所以是applicationScope.ips.size()
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>$Title$</title>
</head>
<body>
<center><h1>You are the ${applicationScope.ips.size()} customer to visit. </h1><br>
<h3><a href="${pageContext.request.contextPath}/logoutServlet">
安全退出
</a></h3>
</center>
</body>
</html>
好了?,這時候,程序寫完了,如何判斷呢?
此時,我們的程序是部署在本地的Tomcat上的,對于本臺電腦,只有一個IP,如何實現多個IP呢?其實啊我們的電腦可以有三個IP,在訪問服務器的時候,服務器的IP多寫幾個,相當于本機的IP多出來幾個。是哪三個IP呢?
1、默認clientIp : 0:0:0:0:0:0:0:1
2、127.0.0.1
這時大家可能會問127.0.0.1和localhost有什么區別呢,其實在這里要區分三個概念:
localhost、127.0.0.1 和 本機IP之間的區別:
3、IPv4地址:192.168.1.110
這樣就很完美的實現了本地三個IP的測試。
寫到這里,似乎已經可以簡單的測試當前在線人數,也許仔細的人會發現在Session域被銷毀的方法中的注釋中發現一些貓膩。大家可以仔細想想,如果客戶端用不同的瀏覽器,相同的IP去訪問呢?點擊退出后,會不會出現錯誤情況呢?答案是會的。演示結果如下圖
所以在點擊退出登錄的按鈕之后,不可以直接將IP移除,要判斷有沒有另外的回話存在,如果有另外的回話存在,此IP是不可以刪掉的,問題由此變的復雜了,因為還要統計此IP所發出的會話有多少。
整體思路:
在全局域中,將不是直接將iP存放在List的鏈表中,而是以一個Map的形式存在,Map的鍵為String類型,Key為List類型,List中存放的是當前IP所激發的會話對象,這樣就可以統計,一個IP觸發的sessions有多少個。
通過調用Map的get方法,將當前IP最為參數,將可以獲取到他所激發的會話集合。但是,此集合可能為空,因為有可能當前IP一次也沒有訪問此頁面,所以在List為空的時候好要創建一個ArrayList來存放sessions,然后將變化后的List重新寫回到Map,再將變化后的Map寫回到全局域中 。這樣創建過程基本完成。
然后考慮銷毀過程,IP還需方法放到Session域中,當session被銷毀的時候,應該把當前Session從List 中刪除,但是Map中此sessions對應的IP可是不能直接刪,要判斷List中的sessions的個數(Entry對象),個數為1的時候才可以刪除,不然就不可以刪除。
所以,要將當前IP通過Request域存放到當前Session域中,
然后,要考慮的問題是,每次刷新頁面后sessions的個數會增加,這是錯誤的,原因是什么?
答案是,因為在存放sessions的時候,創建數組直接進行的添加,這樣的話,每次一刷新頁面,就會導致sessions的添加,所以在此之前應該判斷,sessions中是否有此session,有的話直接跳出。
這樣添加就沒問題了
在Map中,需要使用鍵值對的方式,Key為IP,Value為List,那么List中存放什么呢?存放的是此IP發出的所有回話的HttpSession的對象,所以List的泛型是HttpSession。
請求,在請求中,因為將當前Session 對象存放到List中, List在Map中,Map在全局域中,所以首先得從全局域獲取到Map,然后,從Map中獲取由當前IP所發出的所有Session的組成的List,判斷當前的List是否為NULL,若為NULL,則創建List,否則,將當前SessioncurrentSession放入List中。
import javax.servlet.*;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@WebListener()
public class MyRequestListener implements ServletRequestListener{
private HttpServletRequest sr;
private String clientIp;
private ServletContext sc;
private List<String> ips;
private HttpSession currentSession;
private Map<String,List<HttpSession>> map;
private List<HttpSession> sessions;
@Override
//請求被初始化 Request
public void requestInitialized(ServletRequestEvent sre) {
//從請求域中獲取IP
sr=(HttpServletRequest) sre.getServletRequest();
clientIp=sr.getRemoteAddr();
currentSession=sr.getSession();
//將當前Session 對象存放到List中, List在Map中,Map在全局域中,
sc=sre.getServletContext();
map=(Map<String, List<HttpSession>>) sc.getAttribute("map");
//從Map中獲取由當前IP所發出的所有Session的組成的List
sessions=map.get(clientIp);
//判斷當前的List是否為NULL,若為NULL,則創建List,否則,將當前Session放入List
if (sessions==null){
sessions=new ArrayList<>();
}
// 遍歷List的session 對象,若有則不添加,若沒有則添加
for (HttpSession session :
sessions) {
if (session==currentSession)
return;
}
sessions.add(currentSession);
//將變化過的List重新寫回到Map
map.put(clientIp,sessions);
//再將變化的Map寫回到全局域中
sc.setAttribute("map",map);
//將當前IP放入到當前Session
currentSession.setAttribute("clientIp",clientIp);
}
}
這里將不使用ips了,所以將其刪除
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@WebListener()
public class MyServletContextListener implements ServletContextListener{
private ServletContext sc;
@Override
//Application被初始化的時候創建
public void contextInitialized(ServletContextEvent sce) {
//創建一個Map,key為IP,value為該IP上所發出的會話的對象
Map<String,List<HttpSession>> map=new HashMap<>();
sc=sce.getServletContext();
//將map放到全局域中
sc.setAttribute("map",map);
}
}
接下來剖析Session的刪除工作,獲取當前Session對象,這里有之前傳遞過來的IP,在進行刪除操作的時候,要注意此處,刪除的是List中的sessions,刪除之后,還要判斷其IP的是否要刪除,如果List中沒有該元素,則說明當前IP所發出的會話全部關閉,就可以從map中將當前IP對應的Entry對象刪除,否則,當前IP所發出的會話任存在,那么使用put方法將變化過的List寫回到map。
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.*;
import java.util.List;
import java.util.Map;
@WebListener()
public class MySessionListener implements HttpSessionListener{
private ServletContext sc;
private List<String> ips;
private HttpSession currentSession;
private String clientIp;
private Map<String,List<HttpSession>> map;
private List<HttpSession> sessions;
@Override
public void sessionDestroyed(HttpSessionEvent se) {
sc=se.getSession().getServletContext();
currentSession=se.getSession();
clientIp=(String) currentSession.getAttribute("clientIp");
map=(Map<String, List<HttpSession>>) sc.getAttribute("map");
//從Map中獲取List
sessions=map.get(clientIp);
//從List中刪除當前Session對象
sessions.remove(currentSession);
//如果List中沒有該元素,則說明當前IP所發出的會話全部關閉,就可以從map中
//將當前IP對應的Entry對象刪除
//若List中仍有元素,當前IP所發出的會話任存在,那么將變化過的List寫回到map
if (sessions.size()==0){
map.remove(clientIp);
}else {
map.put(clientIp,sessions);
}
sc.setAttribute("map",map);
}
}
因為處理的退出的頁面/logoutServlet不需要做任何不同的處理,所以這里將不再重復。
因為在jsp用到了JSP標準庫,所以到導兩個包。
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
<title>$Title$</title>
</head>
<body>
<center><h1>You are the ${applicationScope.map.size()} customer to visit. </h1><br>
<h3><a href="${pageContext.request.contextPath}/logoutServlet">
安全退出
</a><br></h3>
<h2>
<c:forEach items="${map}" var="entry">
${entry.key }=${entry.value.size()}<br>
</c:forEach>
</h2>
</center>
</body>
</html>
最后 測試成功,這就是一個完美的統計當前用戶的在線人數。
來源:https://mp.weixin.qq.com/s/gM-lyh_9FeFKRRAJlPY83g
佬勿噴,我就是一個腳本小子
通過谷歌爬取要進行sql注入的網站
inurl:/search_results.php search=inurl:’Product.asp?BigClassName
inurl:Article_Print.asp?
inurl:NewsInfo.asp?id=inurl:EnCompHonorBig.asp?id=inurl:NewsInfo.asp?id=inurl:ManageLogin.asp
inurl:Offer.php?idf=inurl:Opinions.php?id=inurl:Page.php?id=inurl:Pop.php?id=inurl:Post.php?id=inurl:Prod_info.php?id=inurl:Product-item.php?id=inurl:Product.php?id=inurl:Product_ranges_view.php?ID=inurl:Productdetail.php?id=inurl:Productinfo.php?id=inurl:Produit.php?id=inurl:Profile_view.php?id=inurl:Publications.php?id=inurl:Stray-Questions-View.php?num=inurl:aboutbook.php?id=inurl:ages.php?id=inurl:announce.php?id=inurl:art.php?idm=inurl:article.php?ID=inurl:asp?id=inurl:avd_start.php?avd=inurl:band_info.php?id=inurl:buy.php?category=inurl:category.php?id=inurl:channel_id=inurl:chappies.php?id=inurl:clanek.php4?id=inurl:clubpage.php?id=inurl:collectionitem.php?id=inurl:communique_detail.php?id=inurl:curriculum.php?id=inurl:declaration_more.php?decl_id=inurl:detail.php?ID=inurl:download.php?id=inurl:downloads_info.php?id=inurl:event.php?id=inurl:faq2.php?id=inurl:fellows.php?id=inurl:fiche_spectacle.php?id=inurl:forum_bds.php?num=inurl:galeri_info.php?l=inurl:gallery.php?id=inurl:game.php?id=inurl:games.php?id=inurl:historialeer.php?num=inurl:hosting_info.php?id=inurl:humor.php?id=
import requests
from lxml import etree
import time
def create_requests(page,data):
url="https://www.google.com/search?q="+data+"&lr=lang_zh-CN&start={}".format(page)+"&ie=utf-8"
header={
"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36"
}
response=requests.get(url=url,headers=header)#proxies=proxy
response.encoding="utf-8"
context=response.text
return context
def parse_data(context):
parse=etree.HTML(context)
data=parse.xpath('//*/<span>@href')#CTL{n} </span> f=open(r"url.txt","a")
for url in data:
if "/search?" in url:
continue
if "google.com" in url:
continue
if ".jpg" in url:
continue
if "jpeg" in url:
continue
if "png" in url:
continue
if "#" in url:
continue
if "%" in url:
continue
if "url=https://" in url:
continue
if "url=http://" in url:
continue
if ".pdf" in url:
continue
if ".htm" in url:
continue
if ".htmls" in url:
continue
if ".html" in url:
continue
if ".gov.cn" in url:
continue
break
f.write(url+"\n")
print(url+"成功寫入")
f.close()
if __name__=='__main__':
data=input("請輸入Google語法:")
print("----------------------開始抓取----------------------")
for page in range(0,int(input("請輸入結束頁面的倍數:")),10):
context=create_requests(page,data)
parse_data(context)
print("----------------------抓取完畢----------------------")
需要使用魔法,如果沒有魔法還有一種方法,使用谷歌鏡像站,通過fofa搜索
title="Google" && region="HK"
title=="Google" && server=="cloudflare"
進入谷歌鏡像站,輸入谷歌hacking語法搜索
上面的腳本把谷歌網址換成鏡像站網址一樣使用,然后保存以下腳本
import os
import shutil
def get_exists(path):
sum=0
for root, dirs, files in os.walk(path, topdown=False): # 使用topdown=False以便于刪除目錄
log_path=os.path.join(root, "log")
if os.path.isfile(log_path) and os.path.getsize(log_path)==0:
shutil.rmtree(root)
dirs[:]=[] # 清空dirs列表,因為已經刪除了當前目錄
else:
sum +=1
print(root + "\t注入成功")
return sum
if __name__=='__main__':
path=r"result"
print("--------------------開啟執行--------------------")
sum=get_exists(path)
print(f"--------------------{sum}注入點--------------------")
調用上面的腳本去看一下sqlmap的日志文件,如果sqlmap成功發現注入點會有日志文件,日志文件不是空的,沒有注入點,日志文件就為空
下面這個腳本就是全自動的,自動運行sqlmap和查看注入點,腳本運行完sqlmap就會自動調用下一個命令,最好和sqlmap在同一個文件夾
*請認真填寫需求信息,我們會在24小時內與您取得聯系。