证书双向认证

原创
2019/09/05 10:52
阅读数 2K

服务端

Nginx配置

server {
    listen       8800 ssl;
    server_name  127.0.0.1;
    ssl_certificate /usr/local/NSP/nginx/certificate/nginx.crt;
    ssl_certificate_key /usr/local/NSP/nginx/certificate/nginx.key;
    ssl_password_file /usr/local/NSP/nginx/certificate/fifo;
    ssl_protocols       TLSv1.2;
    ssl_ciphers         "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES256-SHA:HIGH:!MEDIUM:!LOW:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4:@STRENGTH";
    ssl_prefer_server_ciphers on;
    ssl_dhparam ./ssl/dh2048.pem;

    root /usr/local/NSP/nginx/html/deployui;

    location /GatewayService/ {
        proxy_pass https://10.31.31.112:18006/GatewayService/;

        #如果证书里面配置了common name,且和proxy_pass中的ip不匹配,设置此参数为common name
        proxy_ssl_name server.xncoding.com;
        proxy_ssl_protocols TLSv1.1  TLSv1.2;
        proxy_ssl_certificate /usr/local/NSP/nginx/certificate/nginx.crt;
        proxy_ssl_certificate_key /usr/local/NSP/nginx/certificate/nginx.key;
        proxy_ssl_password_file /usr/local/NSP/nginx/certificate/fifo;
        proxy_ssl_trusted_certificate /usr/local/NSP/nginx/certificate/root.crt;
        proxy_ssl_verify on;
        proxy_ssl_verify_depth 2;

        proxy_connect_timeout 600s;
        proxy_read_timeout 600s;
        proxy_send_timeout 600s;
        default_type application/json;
        proxy_intercept_errors off;
        break;
    }
}

Tomcat配置

注意下面的truststoreAlgorithm="SunX509" 是在tomcat8.5版本之后必须要添加的配置

<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"
        truststoreAlgorithm="SunX509"
    ciphers="TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_128_CBC_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 {

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

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

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

                @Override
                public void checkServerTrusted(X509Certificate[] certs, String authType) throws CertificateException {
                    logger.debug(">>>>>>>>>>>>>> checkServerTrusted 222222222222222 start ...");
                    // Original trust checking
                    origTrustmanager.checkServerTrusted(certs, authType);
                    if (certs.length > 1) {
                        for (int i = 1; i < certs.length; i++) {
                            X509Certificate ca = certs[i];
                            // 根证书的扩展属性中“基本限制”属性是否包括“CA”
                            if (ca.getBasicConstraints() == -1) {
                                logger.error("check CA certificate error");
                                throw new CertificateException("not a CA certificate");
                            }
                            // 根证书的扩展属性中“密钥用法”中是否包括“证书签名”属性
                            /*
                             * KeyUsage ::= BIT STRING {
                             *   digitalSignature        (0),
                             *   nonRepudiation          (1),
                             *   keyEncipherment         (2),
                             *   dataEncipherment        (3),
                             *   keyAgreement            (4),
                             *   keyCertSign             (5),
                             *   cRLSign                 (6),
                             *   encipherOnly            (7),
                             *   decipherOnly            (8) }
                             */
                            boolean[] keyUsages = ca.getKeyUsage();
                            if (!keyUsages[5]) {
                                logger.error("check keyusage keyCertSign error");
                                throw new CertificateException("keyusage has not value keyCertSign");
                            }
                        }
                    }
                }
            }
        };

        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 error");
        }
    }

    public static X509TrustManager getX509TrustManager(HttpClientProperties properties) {
        String caRootCertKeyStore = properties.getCaRootCertKeyStore();
        try {
            PathUtils.checkRegularAndSecure(caRootCertKeyStore);
        } catch (SecurityException e) {
            throw new RuntimeException("init trustManager, check regular and secure error", e);
        }
        try (FileInputStream rootKeyStore = new FileInputStream(caRootCertKeyStore)) {
            // 加载服务端信任根证书库
            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);
        }
    }

    private static KeyManager[] getX509KeyManagers(HttpClientProperties properties) {
        String clientCertKeyStore = properties.getClientCertKeyStore();
        try {
            PathUtils.checkRegularAndSecure(clientCertKeyStore);
        } catch (SecurityException e) {
            throw new RuntimeException("init KeyManager, check regular and secure error", e);
        }
        try (FileInputStream clientKeystore = new FileInputStream(clientCertKeyStore)) {
            // 加载客户端证书库
            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() {
        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() {
        logger.info("SecurityEurekaClientConfig init sslContext...");
        return X509Util.initSslContext(properties);
    }
}

主配置类SecurityHttpClientConfig

/**
 * 支持HTTPS协议访问和证书双向认证的HttpClient配置类
 *
 * @author x00482439
 * @version v1.0, 2019-05-14
 * @since Security SDK
 */
@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
    protected ApplicationContext context;

    @Bean(name = "simpleRestTemplate")
    @ConditionalOnMissingBean(name = "simpleRestTemplate")
    @ConditionalOnProperty(value = "dc.security.https.httpclient.simple", havingValue = "true")
    public RestTemplate simpleRestTemplate(RestTemplateBuilder restTemplateBuilder) {
        logger.info("simple RestTemplate initialize");
        return restTemplateBuilder.build();
    }

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

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

        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(SSLContext sslContext) {
        return restTemplate -> {
            HttpComponentsClientHttpRequestFactory rf = new HttpComponentsClientHttpRequestFactory();
            rf.setHttpClient(httpClient(sslContext));
            restTemplate.setRequestFactory(rf);
        };
    }

    @Bean
    public SSLContext sslContext() {
        logger.info("SecurityHttpClientConfig init sslContext...");
        return X509Util.initSslContext(properties);
    }

    @Bean
    public 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;
    }

    @Bean
    public 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))
            .setConnectionManagerShared(true)
            .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.", 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 #客户端证书库密码
展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部