整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

如何在Tomcat中做TLS客户端认证

如何在Tomcat中做TLS客户端认证

见的https网站做的是服务端认证(server authentication),浏览器通过证书判断你所访问的https://baidu.com是否真的是百度,而不是其他人伪造的网站。同时还对流量加密,防止别人窃听你的流量。

tls还可以做客户端认证(client authentication),即服务端判断客户端是否为其所信任的客户端。由此可见,客户端认证用于那些需要受控访问服务端。

在数据中心中,有些服务是非常敏感的,那么我们要做到:

  1. 客户端和我的流量是加密的,防止别人监听
  2. 客户端能够确认所访问的服务端的确是我们提供的服务端,而不是别人伪造的服务端
  3. 只有我信任的客户端可以访问我,防止恶意请求

所以很明显,前两个问题可以通过服务端认证解决,最后一个问题可以通过客户端认证解决。顺便一提,如果要使用客户端认证就必须使用服务端认证。

先来讲讲概念然后举个tomcat的例子讲讲怎么做。

概念

服务端认证

不论是做Server authentication还是Client authentication都需要证书。证书的来源有两种:

  • 由权威CA签发,一般都是去购买。也可以使用let's encrypt申请免费证书。
  • 自己签发

在一切可能的情况下都应该使用权威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();
 }
 }
}

安全性考虑

  • 所有private key都很重要!如果它被泄漏了,就要回收它所对应都证书。如果CA的private key泄漏了,那么用它签发的所有证书都要被回收。
  • keystore和truststore的密码设置的要复杂一些。

关于反向代理

因为服务端认证所需要的证书直接配置在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等多个知识点的架构资料)合理利用自己每一分每一秒的时间来学习提升自己,不要再用"没有时间“来掩饰自己思想上的懒惰!趁年轻,使劲拼,给未来的自己一个交代!

计用户在线人数

在统计用户在人数的时候,我们用到了监听器,监听器大致分为以下三种:

  1. ServletRequestListener:用于监听请求的监听接口
  2. HttpSessionListener:用于监听会话的监听接口
  3. ServletContextListener:用于监听应用的回话接口

错误的统计办法

监听Request域

这种统计办法是错误的认为每次刷新页面后进行进行一次的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域

在第二次监听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的数量呢?这样将会导出以下问题:

  • 如何获取用户的IP?
  • IP将如何存储?
  • 如何判断IP之前已经存在?

现在来解决这些问题:

  • 只能从请求中获取
  • 通过2、3问题,我们想到了集合(List),因为集合不仅可以存储任何字符串,还可以通过遍历来判断之前是否有重复的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之间的区别:

  • localhost等于127.0.0.1,不过localhost是域名,127.0.0.1是IP地址。
  • localhost和127.0.0.1不需要联网,都是本机访问。
  • 本机IP需要联网,本机IP是本机或外部访问, 本机 IP 就是本机对外放开访问的IP地址,这个网址就 是与物理网卡绑定的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,有的话直接跳出。

这样添加就没问题了

Servlet域中添加Map

在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);
    }

}

ServletContext

这里将不使用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的删除工作,获取当前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注入的网站

Google hack语法

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搜索

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在同一个文件夹