项目中要实现绑定手机号的功能,通过发送验证码短信来验证手机号。这是绑定手机号中的一个常见的操作。
实现这项功能需要用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 map = new LinkedMultiValueMap();
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 map = new LinkedMultiValueMap();
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
map = new LinkedMultiValueMap (); 千万不要替换为Map与HashMap,否则参数无法传递!
RestTemplateUtil类中的 getHttpEntity 方法:
public static HttpEntity getHttpEntity(Object t, MediaType mediaType) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(mediaType);
return new HttpEntity(t, headers);
}