RestTemplate调用https接口报错:Caused by: javax.net.ssl.SSLHandshakeException

问题:

项目中要实现绑定手机号的功能,通过发送验证码短信来验证手机号。这是绑定手机号中的一个常见的操作。

实现这项功能需要用https请求公司的API短信服务接口,但是在调用这个接口的时候请求头要带着token。

调用Keycloak token的生成token接口来获取access_token。

我使用RestTemplate,来完成对接口的post请求,运行程序,报错信息如下。

2020-12-30 14:46:39.683 ERROR [mini-server-api,49f8f771e82eee46,49f8f771e82eee46,false] 22380 --- [nio-8080-exec-1] c.f.m.api.config.GlobalExceptionHandler  : I/O error on POST request for "https://auth.faw.cn/auth/realms/openapi/protocol/openid-connect/token": sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target; nested exception is 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

Caused by: 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

问题定位:

从报错信息可以看出,获取token的post请求出现错误。获取token的方法 getToken 如下:

public String getToken(TokenVO tokenVO) {
        RestTemplate restTemplate = new RestTemplate(new HttpsClientRequestFactory());

        MultiValueMap<String, String> map = new LinkedMultiValueMap<String, String>();
        map.add("grant_type", "client_credentials");
        map.add("client_id", tokenVO.getClientId());
        map.add("client_secret", tokenVO.getSecret());
        HttpEntity httpEntity = RestTemplateUtil.getHttpEntity(map, MediaType.APPLICATION_FORM_URLENCODED);
        String token = restTemplate.postForObject(url, httpEntity, String.class);
        String accessToken = (String) JSON.parseObject(token, Map.class).get("access_token");
        return accessToken;
    }

 在debug模式下运行程序,打断点,一步步查看数据获取情况。请求头和请求体内的内容都已经放置好,错误出现在getToken方法的这行,RestTemplate对象调用postForObject方法时报异常。

String token = restTemplate.postForObject(url, httpEntity, String.class);

根据报错信息查了很多资料,尝试设置超时时间等方法,最后将错误定位在SSLHandshakeException异常,查找资料尝试以下方法解决了问题!

错误原因:

证书验证、协议版本问题。HTTPS请求 = 超文本传输协议HTTP + 安全套接字层SSL。

jdk1.7默认支持的TLS协议是v1 ,jdk1.8默认支持的是v1.2。

我的项目用的是 jdk 1.8

解决方案:

创建RestTemplate时,修改协议版本。

用一个SimpleClientHttpRequestFactory的实现类来修改协议版本:

public class HttpsClientRequestFactory extends SimpleClientHttpRequestFactory {

    @Override
    protected void prepareConnection(HttpURLConnection connection, String httpMethod) {
        try {
            if (!(connection instanceof HttpsURLConnection)) {// http协议
                //throw new RuntimeException("An instance of HttpsURLConnection is expected");
                super.prepareConnection(connection, httpMethod);
            }
            if (connection instanceof HttpsURLConnection) {// https协议,修改协议版本
                SSLContext ctx = SSLContext.getInstance("TLSv1.2");
                X509TrustManager tm = new X509TrustManager() {
                    @Override
                    public void checkClientTrusted(X509Certificate[] chain,
                                                   String authType) throws CertificateException {
                    }
                    @Override
                    public void checkServerTrusted(X509Certificate[] chain,
                                                   String authType) throws CertificateException {
                    }
                    @Override
                    public X509Certificate[] getAcceptedIssuers() {
                        return null;
                    }
                };
                ctx.init(null, new TrustManager[]{tm}, null);
                org.apache.http.conn.ssl.SSLSocketFactory ssf = new org.apache.http.conn.ssl.SSLSocketFactory(ctx, org.apache.http.conn.ssl.SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
                ((HttpsURLConnection) connection).setSSLSocketFactory(ctx.getSocketFactory());
                HttpsURLConnection httpsConnection = (HttpsURLConnection) connection;

                super.prepareConnection(httpsConnection, httpMethod);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

调用接口部分代码:

public String getToken(TokenVO tokenVO) {
        RestTemplate restTemplate = new RestTemplate(new HttpsClientRequestFactory());
        // 封装参数,千万不要替换为Map与HashMap,否则参数无法传递
        MultiValueMap<String, String> map = new LinkedMultiValueMap<String, String>();
        map.add("grant_type", "client_credentials");
        map.add("client_id", tokenVO.getClientId());
        map.add("client_secret", tokenVO.getSecret());
        HttpEntity httpEntity = RestTemplateUtil.getHttpEntity(map, MediaType.APPLICATION_FORM_URLENCODED);
        String token = restTemplate.postForObject(url, httpEntity, String.class);
        String accessToken = (String) JSON.parseObject(token, Map.class).get("access_token");
        return accessToken;
//        return (String) JSON.parseObject(token, Map.class).get("access_token");
    }

注意: post请求的请求参数,封装参数的map如下:

MultiValueMap<String, String> map = new LinkedMultiValueMap<String, String>();

千万不要替换为Map与HashMap,否则参数无法传递!

RestTemplateUtil类中的 getHttpEntity 方法:

public static HttpEntity getHttpEntity(Object t, MediaType mediaType) {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(mediaType);
       return new HttpEntity(t, headers);

    }

经验总结:

  1. 好好利用IDEA编译器的debug运行模式,程序里打断点,仔细追踪每个参数、数据的情况。
  2. 仔细查看报错信息,定位出错类,结合断点情况,定位出错的代码行。
  3. 报错信息中的" cause by "很重要,往往会指出真正错误之处。查资料根据cause by后面的信息查询最高效。如果有多个cause by,第一个cause by最有可能是直接错误原因。
  4. 最重要的没有之一,认真思考,动脑子分析问题才是王道!
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值