找到你要的答案

Q:How spring security wotk with client certificate authentication with POST method

Q:春季如何安全工作与后方法客户端证书认证

When I use RestTemplate of spring framework to connect to my web service which is secured by HTTPS with client authentication by spring security through Restful API, I found problem to use POST method. It seems that the X509AuthenticationFilter don't get the client certificate when I use POST method. I don't have the same problem when I use GET method. The following is the XML configuration file for spring security in the server side.

<http pattern="/resources/**" security="none" />
<http auto-config="true" use-expressions="true" entry-point-ref="forbiddenAuthEntryPoint">
    <intercept-url pattern="/" access="permitAll" />
    <intercept-url pattern="/service/**" access="hasRole('ROLE_ABC_USER')" />
    <intercept-url pattern="/**" access="permitAll" />

    <!-- <x509 subject-principal-regex="CN=(.*?)," user-service-ref="userDetailsService" 
        /> -->

    <custom-filter position="X509_FILTER" ref="myX509AuthenticationFilter" />
</http>


<bean:bean id="myX509AuthenticationFilter"
    class="com.ray.MyX509AuthenticationFilter">
    <bean:property name="authenticationManager" ref="authenticationManager" />
</bean:bean>

<bean:bean id="preauthAuthenticationProvider"
    class="org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider">
    <bean:property name="preAuthenticatedUserDetailsService" ref="authenticationUserDetailsService" />
</bean:bean>
<bean:bean id="authenticationUserDetailsService"
    class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper">
    <bean:property name="userDetailsService" ref="userDetailsService" />
</bean:bean>

<bean:bean id="forbiddenAuthEntryPoint"
    class="org.springframework.security.web.authentication.Http403ForbiddenEntryPoint" />

<authentication-manager alias="authenticationManager">
    <authentication-provider ref="preauthAuthenticationProvider" />
    <authentication-provider>
        <user-service id="userDetailsService">
            <user name="www.ray.insight" password="dummy" authorities="ROLE_ABC_USER" />
        </user-service>
    </authentication-provider>
</authentication-manager>

Originally, I use the standard x509 element in the security namespace as you can see from the comment out line. During my testing, I use the following MyX509AuthenticationFilter for debugging purpose.

public class MyX509AuthenticationFilter extends X509AuthenticationFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(MyX509AuthenticationFilter.class);

protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {

    X509Certificate[] certs = (X509Certificate[]) request.getAttribute("javax.servlet.request.X509Certificate");

    if (certs != null && certs.length > 0) {
        LOGGER.debug("X.509 client authentication certificate:" + certs[0]);
    } else {
        LOGGER.debug("No client certificate found in request.");
    }

    return super.getPreAuthenticatedPrincipal(request);
}

}

In order to further debug the request. I have added the following servlet filter in web.xml before spring security to print out the client certificate.

public class MyRequestFilter implements Filter {
Logger LOGGER = LoggerFactory.getLogger(MyRequestFilter.class);

@Override
public void doFilter(ServletRequest _servletRequest, ServletResponse _servletResponse, FilterChain _filterChain)
        throws IOException, ServletException {
    LOGGER.debug("MyRequestFilter doFilter(): Entering");

    if (_servletRequest instanceof HttpServletRequest) {
        HttpServletRequest httpServletRequest = (HttpServletRequest) _servletRequest;
        String requestURI = httpServletRequest.getRequestURI();
        String queryString = httpServletRequest.getQueryString();
        StringBuffer requestURL = httpServletRequest.getRequestURL();

        LOGGER.debug("requestURI ->" + requestURI + "<-");
        LOGGER.debug("queryString ->" + queryString + "<-");
        LOGGER.debug("requestURL ->" + requestURL.toString() + "<-");

        X509Certificate[] certs = (X509Certificate[]) httpServletRequest
                .getAttribute("javax.servlet.request.X509Certificate");

        if (certs != null && certs.length > 0) {
            LOGGER.debug("X.509 client authentication certificate:" + certs[0]);
        } else {
            LOGGER.debug("No client certificate found in request.");
        }

    } else {
        LOGGER.debug("get non HttpServletRequest!!" + _servletRequest);

    }
    _filterChain.doFilter(_servletRequest, _servletResponse);
}
}

In order to use client authentication, I have also setup the tomcat to use client authentication and the following line is added to the server.xml.

    <Connector SSLEnabled="true" acceptCount="100" clientAuth="true"
        disableUploadTimeout="true" enableLookups="false" keyAlias="abcServer"
        keypass="password" keystoreFile="tomcat8Cert2.jks" keystorePass="password"
        maxHttpHeaderSize="8192" maxSpareThreads="75" maxThreads="150"
        minSpareThreads="25" port="443" scheme="https" secure="true"
        sslProtocol="TLS" truststoreFile="trustStoreCert2.jks"
        truststorePass="password" />

By using the above program and configuration, if I involve web service through GET method, everything work fine. However, if I involve web service through POST method, the MyRequestFilter can print out the certificate with the correct CN, but the MyX509AuthenticationFilter has not printed out anything and spring security just return "org.springframework.web.client.HttpClientErrorException: 403 Forbidden". This exception should come from Http403ForbiddenEntryPoint of spring security.

I have further test the same web service after comment out spring security filter in my web.xml of the server side and involve it through the same client code. It work. Hence, I suspect something wrong with my setting that make the spring security cannot work property with the POST method to do the client certificate authentication. Any body has idea what I have missed in spring security?

=====================

In case you may interesting to know my client code. The following is the code snippet in the client side.

I use the following to initialize the RestTemplate.

private void initialise() {
    restTemplate = new RestTemplate();

    HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient());
    factory.setReadTimeout(30000);
    factory.setConnectTimeout(30000);
    restTemplate.setRequestFactory(factory);
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    restTemplate.getMessageConverters().add(new StringHttpMessageConverter());
}

The following code is used to return the httpclient with all the required keystore.

        KeyStore trustStore = KeyStore.getInstance(this.getKeyStoreType());
        FileInputStream instream = new FileInputStream(new File(this.getKeyStorePath()));
        try {
            trustStore.load(instream, this.getKeyStorePassword().toCharArray());
        } finally {
            instream.close();
        }

        // TODO: Should trust only authorized client 
        TrustStrategy allTrust = new TrustStrategy() {
            @Override
            public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                return true;
            }
        };
        SSLContext sslcontext = SSLContexts.custom().loadTrustMaterial(
                trustStore, allTrust)
                .loadKeyMaterial(trustStore, this.getKeyStorePassword()
                        .toCharArray()) 
                .build();

        SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslcontext,
                SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
        CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(sslsf).build();

Then the following code is used to involve the web service through POST.

        response = restTemplate.postForEntity(this.getBasePath() + path, httpEntity, String.class);

And the following code is used to involve the web service through GET.

        response = restTemplate.exchange(this.getBasePath() + path, HttpMethod.GET, httpEntity,
                String.class);

When I use RestTemplate of spring framework to connect to my web service which is secured by HTTPS with client authentication by spring security through Restful API, I found problem to use POST method. It seems that the X509AuthenticationFilter don't get the client certificate when I use POST method. I don't have the same problem when I use GET method. The following is the XML configuration file for spring security in the server side.

<http pattern="/resources/**" security="none" />
<http auto-config="true" use-expressions="true" entry-point-ref="forbiddenAuthEntryPoint">
    <intercept-url pattern="/" access="permitAll" />
    <intercept-url pattern="/service/**" access="hasRole('ROLE_ABC_USER')" />
    <intercept-url pattern="/**" access="permitAll" />

    <!-- <x509 subject-principal-regex="CN=(.*?)," user-service-ref="userDetailsService" 
        /> -->

    <custom-filter position="X509_FILTER" ref="myX509AuthenticationFilter" />
</http>


<bean:bean id="myX509AuthenticationFilter"
    class="com.ray.MyX509AuthenticationFilter">
    <bean:property name="authenticationManager" ref="authenticationManager" />
</bean:bean>

<bean:bean id="preauthAuthenticationProvider"
    class="org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider">
    <bean:property name="preAuthenticatedUserDetailsService" ref="authenticationUserDetailsService" />
</bean:bean>
<bean:bean id="authenticationUserDetailsService"
    class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper">
    <bean:property name="userDetailsService" ref="userDetailsService" />
</bean:bean>

<bean:bean id="forbiddenAuthEntryPoint"
    class="org.springframework.security.web.authentication.Http403ForbiddenEntryPoint" />

<authentication-manager alias="authenticationManager">
    <authentication-provider ref="preauthAuthenticationProvider" />
    <authentication-provider>
        <user-service id="userDetailsService">
            <user name="www.ray.insight" password="dummy" authorities="ROLE_ABC_USER" />
        </user-service>
    </authentication-provider>
</authentication-manager>

原来,我在安全空间标准X509元你可以看到从注释行。在我的测试中,我使用下面的用于调试目的myx509authenticationfilter。

public class MyX509AuthenticationFilter extends X509AuthenticationFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(MyX509AuthenticationFilter.class);

protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {

    X509Certificate[] certs = (X509Certificate[]) request.getAttribute("javax.servlet.request.X509Certificate");

    if (certs != null && certs.length > 0) {
        LOGGER.debug("X.509 client authentication certificate:" + certs[0]);
    } else {
        LOGGER.debug("No client certificate found in request.");
    }

    return super.getPreAuthenticatedPrincipal(request);
}

}

为了进一步调试请求。我添加了以下春天之前安全打印客户端证书在web.xml servlet过滤器。

public class MyRequestFilter implements Filter {
Logger LOGGER = LoggerFactory.getLogger(MyRequestFilter.class);

@Override
public void doFilter(ServletRequest _servletRequest, ServletResponse _servletResponse, FilterChain _filterChain)
        throws IOException, ServletException {
    LOGGER.debug("MyRequestFilter doFilter(): Entering");

    if (_servletRequest instanceof HttpServletRequest) {
        HttpServletRequest httpServletRequest = (HttpServletRequest) _servletRequest;
        String requestURI = httpServletRequest.getRequestURI();
        String queryString = httpServletRequest.getQueryString();
        StringBuffer requestURL = httpServletRequest.getRequestURL();

        LOGGER.debug("requestURI ->" + requestURI + "<-");
        LOGGER.debug("queryString ->" + queryString + "<-");
        LOGGER.debug("requestURL ->" + requestURL.toString() + "<-");

        X509Certificate[] certs = (X509Certificate[]) httpServletRequest
                .getAttribute("javax.servlet.request.X509Certificate");

        if (certs != null && certs.length > 0) {
            LOGGER.debug("X.509 client authentication certificate:" + certs[0]);
        } else {
            LOGGER.debug("No client certificate found in request.");
        }

    } else {
        LOGGER.debug("get non HttpServletRequest!!" + _servletRequest);

    }
    _filterChain.doFilter(_servletRequest, _servletResponse);
}
}

为了使用客户端身份验证,我还安装了Tomcat使用客户端验证和下面一行添加到server.xml。

    <Connector SSLEnabled="true" acceptCount="100" clientAuth="true"
        disableUploadTimeout="true" enableLookups="false" keyAlias="abcServer"
        keypass="password" keystoreFile="tomcat8Cert2.jks" keystorePass="password"
        maxHttpHeaderSize="8192" maxSpareThreads="75" maxThreads="150"
        minSpareThreads="25" port="443" scheme="https" secure="true"
        sslProtocol="TLS" truststoreFile="trustStoreCert2.jks"
        truststorePass="password" />

通过使用上述程序和配置,如果我通过GET方法涉及web服务,一切都会好起来的。然而,如果我涉及到Web服务方法的myrequestfilter通过后,可以打印出证书与正确的CN,但myx509authenticationfilter没有打印出任何弹簧安全回“org.springframework.web.client.httpclienterrorexception:没有权限访问此网站”。这个例外应该来自弹簧安全http403forbiddenentrypoint。

我有进一步的测试相同的Web服务的评论在我的服务器端的web.xml弹簧安全过滤后,涉及通过相同的客户端代码。它的工作。因此,我怀疑我的设置有问题,使Spring安全无法工作的属性与POST方法做客户端证书认证。任何人都知道我错过了春天的安全吗?

=====================

万一你可能知道我的客户代码。以下是在客户端代码段。

我使用下面的初始化RestTemplate。

private void initialise() {
    restTemplate = new RestTemplate();

    HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient());
    factory.setReadTimeout(30000);
    factory.setConnectTimeout(30000);
    restTemplate.setRequestFactory(factory);
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    restTemplate.getMessageConverters().add(new StringHttpMessageConverter());
}

下面的代码是用来返回所有需要的密钥库的时候。

        KeyStore trustStore = KeyStore.getInstance(this.getKeyStoreType());
        FileInputStream instream = new FileInputStream(new File(this.getKeyStorePath()));
        try {
            trustStore.load(instream, this.getKeyStorePassword().toCharArray());
        } finally {
            instream.close();
        }

        // TODO: Should trust only authorized client 
        TrustStrategy allTrust = new TrustStrategy() {
            @Override
            public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                return true;
            }
        };
        SSLContext sslcontext = SSLContexts.custom().loadTrustMaterial(
                trustStore, allTrust)
                .loadKeyMaterial(trustStore, this.getKeyStorePassword()
                        .toCharArray()) 
                .build();

        SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslcontext,
                SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
        CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(sslsf).build();

然后,下面的代码用于将Web服务通过POST。

        response = restTemplate.postForEntity(this.getBasePath() + path, httpEntity, String.class);

下面的代码用于通过获取web服务。

        response = restTemplate.exchange(this.getBasePath() + path, HttpMethod.GET, httpEntity,
                String.class);
spring  web-services  security  spring-mvc  spring-security