证书双向认证

狂风中的少年 提交于 2019-11-29 07:25:54

服务端

Tomcat配置

<Connector port="9443" protocol="org.apache.coyote.http11.Http11NioProtocol" SSLEnabled ="true" sslProtocol ="TLS" maxThreads="150"
        acceptCount="100" maxHttpHeaderSize="8192" maxKeepAliveRequests="100"
        scheme="https" secure="true"
        keystoreFile="conf/server.p12"
        keystorePass="222222"
        keystoreType="PKCS12"
        clientAuth="true"
        truststoreFile="conf/root.p12"
        truststorePass="333333"
        truststoreType="PKCS12"
 ciphers="SSL_ECDHE_RSA_WITH_AES_256_GCM_SHA384,SSL_ECDHE_RSA_WITH_AES_128_CBC_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,SSL_ECDHE_RSA_WITH_AES_128_GCM_SHA256,  TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"
        maxPostSize="10240" connectionTimeout="20000"
        allowTrace="false" xpoweredBy="false"
        server="WebServer"
        URIEncoding="UTF-8" sslEnabledProtocols="TLSv1.2,TLSv1.1"/>

SpringBoot配置

server:
  ssl:
    enabled: true
    key-store: /cert/server.p12
    key-store-password: 222222
    key-store-type: PKCS12
    trust-store: /cert/root.p12
    trust-store-password: 333333
    trust-store-type: PKCS12
    client-auth: need

客户端

客户端RestTemplate

maven依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-commons</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.sun.jersey.contribs</groupId>
            <artifactId>jersey-apache-client4</artifactId>
            <version>1.19.1</version>
        </dependency>
        <dependency>
            <groupId>com.netflix.eureka</groupId>
            <artifactId>eureka-client</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>

HttpClientProperties类

@Component
@ConfigurationProperties(prefix = "dc.security.https.httpclient")
public class HttpClientProperties {
    /**
     * 是否开启服务端证书校验
     */
    private boolean enabled = true;
    /**
     * 是否开启客户端证书发送
     */
    private boolean clientCert = false;
    /**
     * 是否支持客户端负载均衡
     */
    private boolean loadBalanced = true;
    /**
     * 是否支持eureka注册发现
     */
    private boolean eureka = true;
    /**
     * CA根证书密钥库文件
     */
    private String caRootCertKeyStore;
    /**
     * CA根证书密钥库密码
     */
    private String caRootCertPassword;
    /**
     * 客户端证书库文件
     */
    private String clientCertKeyStore;
    /**
     * 客户端证书库密码
     */
    private String clientCertPassword;
    /**
     * 建立连接的超时时间
     */
    private int connectTimeout = 20000;
    /**
     * 连接不够用的等待时间
     */
    private int requestTimeout = 20000;
    /**
     * 每次请求等待返回的超时时间
     */
    private int socketTimeout = 30000;
    /**
     * 每个主机最大连接数
     */
    private int defaultMaxPerRoute = 100;
    /**
     * 最大连接数
     */
    private int maxTotalConnections = 300;
    /**
     * 连接保持活跃的时间(Keep-Alive)
     */
    private int defaultKeepAliveTimeMillis = 20000;
    /**
     * 空闲连接的生存时间
     */
    private int closeIdleConnectionWaitTimeSecs = 30;
}

X509Util工具类

public class X509Util {

    public static X509TrustManager getX509TrustManager(HttpClientProperties properties) {
        try (FileInputStream rootKeyStore = new FileInputStream(properties.getCaRootCertKeyStore())){
            // 加载服务端信任根证书库
            KeyStore trustKeyStore = KeyStore.getInstance("PKCS12");
            trustKeyStore.load(rootKeyStore, properties.getCaRootCertPassword().toCharArray());
            // 初始化服务端信任证书管理器
            TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
            tmf.init(trustKeyStore);
            TrustManager[] trustManagers = tmf.getTrustManagers();
            return (X509TrustManager) trustManagers[0];
        } catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException e) {
            throw new RuntimeException("init trustManager error", e);
        }
    }

    public static KeyManager[] getX509KeyManagers(HttpClientProperties properties) {
        try (FileInputStream clientKeystore = new FileInputStream(properties.getClientCertKeyStore())) {
            // 加载客户端证书库
            KeyStore clientKeyStore = KeyStore.getInstance("PKCS12");
            clientKeyStore.load(clientKeystore, properties.getClientCertPassword().toCharArray());
            KeyManagerFactory keyManagerFactory = KeyManagerFactory
                .getInstance(KeyManagerFactory.getDefaultAlgorithm());
            keyManagerFactory.init(clientKeyStore, properties.getClientCertPassword().toCharArray());
            return keyManagerFactory.getKeyManagers();
        } catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException | UnrecoverableKeyException e) {
            throw new RuntimeException("init keyManagers error", e);
        }
    }
}

ICrlService服务类

public interface ICrlService {

    /**
     * 实现此方法查询证书吊销列表
     *
     * @return 证书吊销序列号集合
     */
    Set<String> getCrlList();
}

AbstractUserInfoInterceptor

public abstract class AbstractUserInfoInterceptor implements HttpRequestInterceptor {

    private static final Logger _logger = LoggerFactory.getLogger(AbstractUserInfoInterceptor.class);

    /**
     * 内部微服务之间调用增加的用户信息头
     */
    private static final String HEADER_ACCESS_USER = "access-user";

    public void process(HttpRequest request, HttpContext context) {
        _logger.debug("ClientIPInterceptor start to handle...");
        request.addHeader(HEADER_ACCESS_USER, loadUserInfo());
    }

    /**
     * 加载用户信息
     *
     * @return 用户信息
     */
    protected abstract String loadUserInfo();
}

AbstractClientIPInterceptor

public abstract class AbstractClientIPInterceptor implements HttpRequestInterceptor {

    private static final Logger _logger = LoggerFactory.getLogger(AbstractClientIPInterceptor.class);

    /**
     * 内部微服务之间调用增加的IP地址头
     */
    private static final String HEADER_X_REMOTE_USER_IP = "X-Remote-User-IP";

    public void process(HttpRequest request, HttpContext context) {
        _logger.debug("ClientIPInterceptor start to handle...");
        request.addHeader(HEADER_X_REMOTE_USER_IP, loadRemoteClientIp());
    }

    /**
     * 加载用户真实IP地址
     *
     * @return 用户真实IP地址
     */
    protected abstract String loadRemoteClientIp();
}

SecurityEurekaClientConfig

@Configuration
@EnableConfigurationProperties({HttpClientProperties.class})
@ConditionalOnProperty(value = "dc.security.https.httpclient.eureka", havingValue = "true", matchIfMissing = true)
public class SecurityEurekaClientConfig {

    private static final Logger logger = LoggerFactory.getLogger(SecurityEurekaClientConfig.class);

    @Autowired
    private HttpClientProperties properties;

    @Bean
    public DiscoveryClient.DiscoveryClientOptionalArgs discoveryClientOptionalArgs() throws Exception {
        logger.info("DiscoveryClient.DiscoveryClientOptionalArgs init ...");
        EurekaJerseyClientImpl.EurekaJerseyClientBuilder builder = new EurekaJerseyClientImpl.EurekaJerseyClientBuilder();
        builder.withClientName("eureka-client");
        builder.withCustomSSL(sslContextEureka());
        builder.withMaxTotalConnections(10);
        builder.withMaxConnectionsPerHost(10);
        DiscoveryClient.DiscoveryClientOptionalArgs args = new DiscoveryClient.DiscoveryClientOptionalArgs();
        args.setEurekaJerseyClient(builder.build());
        return args;
    }

    private SSLContext sslContextEureka() throws Exception {
        // 加载服务端信任Keystore
        X509TrustManager origTrustmanager = getX509TrustManager(properties);
        TrustManager[] wrappedTrustManagers = new TrustManager[]{
            new X509TrustManager() {
                @Override
                public X509Certificate[] getAcceptedIssuers() {
                    logger.info(">>>>>>>>>>>>>> sslContextEureka getAcceptedIssuers 00000000000000000 start ...");
                    return origTrustmanager.getAcceptedIssuers();
                }

                @Override
                public void checkClientTrusted(X509Certificate[] certs, String authType) throws CertificateException {
                    logger.info(">>>>>>>>>>>>>> sslContextEureka checkClientTrusted 111111111111 start ...");
                }

                @Override
                public void checkServerTrusted(X509Certificate[] certs, String authType) throws CertificateException {
                    logger.info(">>>>>>>>>>>>>> sslContextEureka checkServerTrusted 222222222222222 start ...");
                }
            }
        };

        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, wrappedTrustManagers, new java.security.SecureRandom());
        return sslContext;
    }
}

主配置类SecurityHttpClientConfig

@Configuration
@ConditionalOnProperty(value = "dc.security.https.httpclient.enabled", havingValue = "true")
@EnableScheduling
@EnableConfigurationProperties({HttpClientProperties.class})
public class SecurityHttpClientConfig {

    private static final Logger logger = LoggerFactory.getLogger(SecurityHttpClientConfig.class);

    @Autowired
    private HttpClientProperties properties;
    @Autowired
    private ICrlService crlService;
    @Autowired
    protected ApplicationContext context;

    @Bean
    @ConditionalOnMissingBean(RestTemplate.class)
    @ConditionalOnProperty(value = "dc.security.https.httpclient.load-balanced", havingValue = "false")
    public RestTemplate restTemplateSimple(RestTemplateBuilder restTemplateBuilder) {
        logger.info("simple RestTemplate");
        return restTemplateBuilder.build();
    }

    @Bean
    @LoadBalanced
    @ConditionalOnMissingBean(RestTemplate.class)
    @ConditionalOnProperty(value = "dc.security.https.httpclient.load-balanced", havingValue = "true", matchIfMissing = true)
    public RestTemplate restTemplateLoadBalanced(RestTemplateBuilder restTemplateBuilder) {
        logger.info("loadBalanced RestTemplate");
        return restTemplateBuilder.build();
    }

    @Bean
    @DependsOn(value = {"customRestTemplateCustomizer"})
    public RestTemplateBuilder restTemplateBuilder(MappingJackson2HttpMessageConverter jackson2HttpMessageConverter) {
        RestTemplateBuilder builder = new RestTemplateBuilder(customRestTemplateCustomizer());

        List<HttpMessageConverter<?>> messageConverters = new ArrayList<>();
        StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter(StandardCharsets.UTF_8);
        FormHttpMessageConverter formMessageConverter = new FormHttpMessageConverter();
        messageConverters.add(stringHttpMessageConverter);
        messageConverters.add(jackson2HttpMessageConverter);
        messageConverters.add(formMessageConverter);
        builder.messageConverters(messageConverters);

        return builder;
    }

    @Bean
    public RestTemplateCustomizer customRestTemplateCustomizer() {
        return restTemplate -> {
            HttpComponentsClientHttpRequestFactory rf = new HttpComponentsClientHttpRequestFactory();
            rf.setHttpClient(httpClient(sslContext()));
            restTemplate.setRequestFactory(rf);
        };
    }

    private SSLContext sslContext() {
        // 加载服务端信任Keystore
        X509TrustManager origTrustmanager = getX509TrustManager(properties);
        TrustManager[] wrappedTrustManagers = new TrustManager[]{
            new X509TrustManager() {
                @Override
                public X509Certificate[] getAcceptedIssuers() {
                    logger.info(">>>>>>>>>>>>>> getAcceptedIssuers 00000000000000000 start ...");
                    return origTrustmanager.getAcceptedIssuers();
                }

                @Override
                public void checkClientTrusted(X509Certificate[] certs, String authType) throws CertificateException {
                    logger.info(">>>>>>>>>>>>>> checkClientTrusted 111111111111 start ...");
                    origTrustmanager.checkClientTrusted(certs, authType);
                }

                @Override
                public void checkServerTrusted(X509Certificate[] certs, String authType) throws CertificateException {
                    logger.info(">>>>>>>>>>>>>> checkServerTrusted 222222222222222 start ...");
                    try {
                        //Original trust checking
                        origTrustmanager.checkServerTrusted(certs, authType);

                        //Check revocation with each cert
                        //from docs: CertificateException - if the certificate chain is not trusted by this TrustManager.
                        for (X509Certificate cert : certs) {
                            String certSerial = cert.getSerialNumber().toString(16).toUpperCase();
                            if (crlService.getCrlList().contains(certSerial)) {
                                logger.error("cert serial={} is in crl list, bad", certSerial);
                                throw new CertificateException();
                            }
                        }
                    } catch (CertificateExpiredException e) {
                        logger.error("cert expired error");
                        throw new CertificateExpiredException();
                    }
                }
            }
        };

        try {
            SSLContext sslContext = SSLContext.getInstance("TLS");
            if (properties.isClientCert()) { // 如果开启客户端证书校验,则需要发送客户端证书
                sslContext.init(getX509KeyManagers(properties), wrappedTrustManagers, new java.security.SecureRandom());
            } else { // 否则不需要发送客户端证书
                sslContext.init(null, wrappedTrustManagers, new java.security.SecureRandom());
            }
            return sslContext;
        } catch (NoSuchAlgorithmException | KeyManagementException e) {
            throw new RuntimeException("init sslContext");
        }
    }

    private PoolingHttpClientConnectionManager poolingConnectionManager(SSLContext sslContext) {
        SSLConnectionSocketFactory sslsf;
        try {
            HostnameVerifier hostnameVerifier = (s, sslSession) -> {
                try {
                    Certificate[] certs = sslSession.getPeerCertificates();
                    X509Certificate x509 = (X509Certificate) certs[0];
                } catch (SSLPeerUnverifiedException e) {
                    logger.error("hostnameVerifier error", e);
                    return false;
                }
                return true;
            };
            sslsf = new SSLConnectionSocketFactory(sslContext, hostnameVerifier);
        } catch (Exception e) {
            logger.error("Pooling Connection Manager Initialisation failure");
            throw new RuntimeException("Pooling Connection Manager Initialisation failure", e);
        }
        Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder
            .<ConnectionSocketFactory>create()
            .register("https", sslsf)
            .register("http", new PlainConnectionSocketFactory())
            .build();

        PoolingHttpClientConnectionManager poolingConnectionManager = new PoolingHttpClientConnectionManager(
            socketFactoryRegistry);
        poolingConnectionManager.setMaxTotal(properties.getMaxTotalConnections());  //最大连接数
        poolingConnectionManager.setDefaultMaxPerRoute(properties.getDefaultMaxPerRoute());  //同路由并发数
        return poolingConnectionManager;
    }


    private ConnectionKeepAliveStrategy connectionKeepAliveStrategy() {
        return (response, httpContext) -> {
            HeaderElementIterator it = new BasicHeaderElementIterator
                (response.headerIterator(HTTP.CONN_KEEP_ALIVE));
            while (it.hasNext()) {
                HeaderElement he = it.nextElement();
                String param = he.getName();
                String value = he.getValue();
                if (value != null && param.equalsIgnoreCase("timeout")) {
                    return Long.parseLong(value) * 1000;
                }
            }
            return properties.getDefaultKeepAliveTimeMillis();
        };
    }

    private CloseableHttpClient httpClient(SSLContext sslContext) {
        RequestConfig requestConfig = RequestConfig.custom()
            .setConnectionRequestTimeout(properties.getRequestTimeout())
            .setConnectTimeout(properties.getConnectTimeout())
            .setSocketTimeout(properties.getSocketTimeout()).build();

        HttpClientBuilder httpClientBuilder = HttpClients.custom()
            .setDefaultRequestConfig(requestConfig)
            .setConnectionManager(poolingConnectionManager(sslContext))
            .setKeepAliveStrategy(connectionKeepAliveStrategy())
            .setRetryHandler(new DefaultHttpRequestRetryHandler(3, true));

        // 增加请求拦截器
        Map<String, HttpRequestInterceptor> interceptorMap = context.getBeansOfType(HttpRequestInterceptor.class);
        if (interceptorMap.size() > 0) {
            for (HttpRequestInterceptor interceptor : interceptorMap.values()) {
                httpClientBuilder.addInterceptorLast(interceptor);
            }
        }
        return httpClientBuilder.build();
    }

    /**
     * You can't set an idle connection timeout in the config for Apache HTTP Client.
     * The reason is that there is a performance overhead in doing so.
     * <properties>
     * The documentation clearly states why, and gives an example of an idle connection monitor implementation you can
     * copy.
     * Essentially this is another thread that you run to periodically call closeIdleConnections on
     * HttpClientConnectionManager
     * <properties>
     * http://hc.apache.org/httpcomponents-client-ga/tutorial/html/connmgmt.html
     *
     * @return 线程
     */
    @Bean
    public Runnable idleConnectionMonitor() {
        return new Runnable() {
            @Override
            @Scheduled(fixedDelay = 10000)
            public void run() {
                try {
                    PoolingHttpClientConnectionManager connectionManager = poolingConnectionManager(sslContext());
                    logger.trace("run IdleConnectionMonitor - Closing expired and idle connections...");
                    connectionManager.closeExpiredConnections();
                    connectionManager
                        .closeIdleConnections(properties.getCloseIdleConnectionWaitTimeSecs(), TimeUnit.SECONDS);
                } catch (Exception e) {
                    logger.error("run IdleConnectionMonitor - Exception occurred. msg={}, e={}", e.getMessage(), e);
                }
            }
        };
    }

    @Bean
    @ConditionalOnMissingBean(TaskScheduler.class)
    public TaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setThreadNamePrefix("poolScheduler");
        scheduler.setPoolSize(10);
        return scheduler;
    }

    @Bean
    @ConditionalOnMissingBean(ICrlService.class)
    public ICrlService crlService() {
        return HashSet::new;
    }

}

客户端配置

dc:
  security:
    https:
      httpclient:
        enabled: true
        ca-root-cert-key-store: /cert/root.p12  #根证书库
        ca-root-cert-password: 333333 #根证书库密码
        client-cert: true #开启客户端证书
        client-cert-key-store: /cert/server.p12 #客户端证书库
        client-cert-password: 222222 #客户端证书库密码
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!