基于SpringBoot的API网关实现
一、背景&目标
在微服务架构已经很普及的今天,API网关是整个微服务体系中是必不可少的基础服务。提到API网关大家可能会想到Zuul、Spring Cloud Gateway等开源API网关,Zuul2.x、Spring Cloud GateWay这些基于Reactor模式(响应式模式)的开源网关在高并发、高可用的需求场景下也已经被很多组织在生产环境中所验证。
我们在实际业务场景中可以直接使用Zuul、SpringCloud GateWay来满足我们业务的需求,即使需要在网关层实现一些具体的业务逻辑,我们也可以在开源的基础上进行二次开发。但如果我们只需要使用API网关核心的能力,同时需要在API层实现一些业务逻辑,我们基于SpringBoot自己来实现API网关,我们可以怎样来实现呢?通过结合实际业务需求以及对开源API网关的的学习,梳理出API网关的核心能力目标,具体如下:
1、基础能力
鉴权
路由转发
标准化返回
自定义异常
2、一定的高性能&高可用
API网关作为微服务体系下的基础服务,API网关本身需要有比较高的性能,完整的一次请求在网关层消耗的时间要尽可能的小。
业务服务出现异常时,要能够保证网关不因为雪崩效应导致网关失去处理、自恢复的能力。
3、安全性
API网关直接暴露在公网,对于恶意IP,API网关服务要有能力对恶意IP进行访问限制。
API网关要防止业务日志从API网关数据泄露。
二、基于SpringBoot的API网关架构
2.1、概要架构图
2.2、架构说明
- 基础能力
按照DDD思想,划分为Pre routing filters、Routing filters、Response filters、Error filters四个微观领域,分别对应转发前处理、转发处理、返回处理、错误处理四个方面。
- 高性能
为了满足网关的基础能力,同时保证网关具有一定的高性能,基于NIO2(AIO)模式来构建基础架构。
- 高可用
为了保证网关具有高可用性,使用Alibaba Sentlnel进行限流、熔断降级提高网关服务的健壮性。
- 安全性
为了能够对黑名单IP进行拒绝访问,提供灵活的配置能力。
业务数据日志默认不在网关中日志中记录
2.3、实现说明
2.3.1 基础能力
Pre routing filters
使用Java Filter来实现Pre routing filters相关能力,包括IP黑名单、登录态、鉴权、白名单等过滤器实现。下面是IP黑名单过滤器的示例,可参考:
1 /** 2 * 黑名单IP过滤器 3 */ 4 @Component 5 @WebFilter(urlPatterns = "/*", filterName = "backListFilter") 6 @Slf4j 7 public class BackListFilter implements Filter, Ordered { 8 9 @Override 10 public void init(FilterConfig filterConfig) throws ServletException { 11 Filter.super.init(filterConfig); 12 } 13 14 @Override 15 public void destroy() { 16 Filter.super.destroy(); 17 } 18 19 @Override 20 public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { 21 HttpServletRequest request = (HttpServletRequest) servletRequest; 22 String remoteAddress=""; 23 String blackList=""; 24 if(blackList.contains(remoteAddress)){ 25 HttpServletResponse response = (HttpServletResponse) servletResponse; 26 response.setStatus(HttpStatus.FORBIDDEN.value()); 27 PrintWriter out = response.getWriter(); 28 out.write("request refused"); 29 return; 30 } 31 // 继续进行后续流程处理 32 filterChain.doFilter(servletRequest, servletResponse); 33 } 34 35 @Override 36 public int getOrder() { 37 // 设置Filter执行顺序(越小优先级越高) 38 return 0; 39 }
Routing filters
转发层主要提供路由解析、标准Header构建、转发能力。
(1)路由解析因为跟具体的业务场景有关,这里不展开描述。简单实用的方式,比如可以根据业务服务在网关侧注册的转发规则进行转发。
(2)HTTP转发组件需要支持GET、POST等常见类型的请求,可以根据实际场景或个人习惯选择一些优秀成熟的HTTP工具,我们此处选择Apache HttpClient作为Http工具。为了提高API网关的转发性能,同时提高网关的可用性,需要对HttpClient进行连接池、超时时间、重试机制、默认Header等配置。参照如下:
1 /** 2 * RestTemplate配置类 3 */ 4 @Configuration 5 public class RestTemplateConfig { 6 7 /** 8 * 从连池中获取连接的超时时间 9 */ 10 @Value("${httpclient.connection-request-timeout}") 11 private Integer connectionRequestTimeout; 12 13 /** 14 * 建立TCP连接超时时间 15 */ 16 @Value("${httpclient.connect-timeout}") 17 private Integer connectTimeout; 18 19 /** 20 * TCP连接Socket数据返回超时时间 21 */ 22 @Value("${httpclient.socket-timeout}") 23 private Integer socketTimeout; 24 25 /** 26 * 创建RestTemplate Bean 27 * @return 28 */ 29 @Bean 30 public RestTemplate restTemplate() { 31 RestTemplate restTemplate = new RestTemplate(httpRequestFactory()); 32 return restTemplate; 33 } 34 35 /** 36 * 通过HttpClient创建Http请求工厂 37 * @return 38 */ 39 @Bean 40 public ClientHttpRequestFactory httpRequestFactory() { 41 HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(httpClient()); 42 // 缓冲请求数据,默认值是true。通过POST或者PUT大量发送数据时,建议将此属性更改为false,以免耗尽内存。 43 clientHttpRequestFactory.setBufferRequestBody(false); 44 return clientHttpRequestFactory; 45 } 46 47 /** 48 * 创建 HttpClient Bean 49 * @return 50 */ 51 @Bean 52 public HttpClient httpClient() { 53 Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create() 54 .register("http", PlainConnectionSocketFactory.getSocketFactory()) 55 .register("https", SSLConnectionSocketFactory.getSocketFactory()) 56 .build(); 57 PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(registry); 58 // 1、连接管理 59 // 设置整个连接池最大连接数 根据自己的场景决定 60 connectionManager.setMaxTotal(200); 61 // 路由是对maxTotal的细分 62 connectionManager.setDefaultMaxPerRoute(100); 63 // 2、请求相关配置 64 RequestConfig requestConfig = RequestConfig.custom() 65 // 从连接池中获取连接的超时时间,超过该时间未拿到可用连接,会抛出org.apache.http.conn.ConnectionPoolTimeoutException: 66 // Timeout waiting for connection from pool 67 .setConnectionRequestTimeout(connectionRequestTimeout) 68 // 连接上服务器(握手成功)的时间,超出该时间抛出connect timeout 69 .setConnectTimeout(connectTimeout) 70 // 服务器返回数据(response)的时间,超过该时间抛出read timeout 71 .setSocketTimeout(socketTimeout) 72 .build(); 73 74 // 3、重试机制 75 HttpRequestRetryHandler httpRequestRetryHandler = new HttpRequestRetryHandler() { 76 @Override 77 public boolean retryRequest(IOException e, int retryTimes, HttpContext httpContext) { 78 // 重试次数最多2次 79 if(retryTimes > CommonConstant.HTTPCLIENT_RETRY_TIMES){ 80 return false; 81 } 82 // 请求发生一些IO类型的异常时,进行重试 83 if (e instanceof UnknownHostException || e instanceof ConnectTimeoutException 84 || !(e instanceof SSLException) || e instanceof NoHttpResponseException) { 85 return true; 86 } 87 88 return false; 89 } 90 }; 91 92 // 4、设置默认的Header 93 List<Header> headers = new ArrayList<>(); 94 headers.add(new BasicHeader("Accept-Encoding","gzip,deflate")); 95 headers.add(new BasicHeader("Connection", "Keep-Alive")); 96 97 // 创建httpclient对象 98 return HttpClientBuilder.create() 99 .setDefaultRequestConfig(requestConfig) 100 .setConnectionManager(connectionManager) 101 .setRetryHandler(httpRequestRetryHandler) 102 .setDefaultHeaders(headers) 103 .build(); 104 } 105 106 }
3)转发时,因为业务服务返回的数据格式不确定(可能是JSON、HTML、PDF等各种形式),所以为了保证转发的通用性,使用输入流进行数据接收。因为是基于Srping架构的,我们选择org.springframework.core.io.Resource进行数据接收。
1 restTemplate.exchange(forwardUrl,httpMethod,httpEntity,org.springframework.core.io.Resource.class);
Response filters
(1)API网关大部分场景返回的数据结构是JSON类型的数据,但一些特殊的场景需要支持HTML、PDF等格式的数据呈现,这种场景下,就可以根据业务接口Response中的"Content-Type"来统一进行处分处理。
(2)另外如果需要对外提供标准的HTTP Header等信息,也可以在此领域进行处理。
Error filters
API网关核心的使用场景是将业务接口暴露给各种客户端使用,因此标准、统一的错误就显得尤为重要。因为是基于Srping架构的,所以可以结合Spring框架的能力加上自定义异常来实现标准化的异常处理。具体可参照代码:
1 /** 2 * 鉴权自定义异常 3 */ 4 public class AuthResultException extends RuntimeException { 5 6 private static final long serialVersionUID = 1L; 7 8 /** 9 * 设置异常对应的Http状态码,业务错误码,业务提示信息,可指定是否生成详细的错误信息 10 */ 11 public AuthResultException(ResultCode resultCode) { 12 super(resultCode.getMessage()); 13 } 14 15 } 16 17 @ControllerAdvice 18 public class GlobalExceptionHandler { 19 @ExceptionHandler(Exception.class) 20 @ResponseBody 21 public void handleException(HttpServletRequest request, HttpServletResponse response,Exception e) { 22 // 鉴权自定义异常,返回401 23 if(e instanceof AuthResultException) { 24 result.setErrcode(110002); 25 result.setMessage("未登录或用户不存在"); 26 response.setStatus(HttpStatus.UNAUTHORIZED.value()); 27 } 28 } 29 }
2.3.2 高性能
- JAVA的IO方式包括BIO、NIO、NIO2(JDK1.7后新引入,主要实现AIO),为了方便理解,先对这几种IO进行简单的比较。
差异 | BIO | NIO | NIO2(AIO) |
1 | 面向流 | 面向通道、缓冲区、选择器(Selectors) | 面向通道、缓冲区、选择器(Selectors) |
2 | 阻塞同步IO | 非阻塞同步IO | 非阻塞异步IO |
3 | 用户线程直接参与读写,整个数据读写过冲中用户线程都处于阻塞状态 | 用户调用读写方法时是不阻塞的,立刻返回。但需要用户进程来检查IO状态,如果发现有可以操作的IO,那么用户进行还是会阻塞等待内核复制数据到用户进程,它与BIO的主要区别是BIO是全程处于阻塞等待状态。 | 内核线程负责数据读写,内核线程读写数据处于堵塞状态时,不影响用户线程 |
- API网关作为一个基础服务,在客户端与业务服务中间起到了承上启下的作用的,因此我们需要尽量的保证网关的高性能。
我们API网关本质上是基于Tomcat Servelt架构的,Tomcat7.x以上实现了Http11Nio2协议,但默认协议仍然使用的是Http11Nio的协议的,官方文档推荐高版本的Tomcat使用Http11Nio2协议。因此我们将API网关默认的IO协议修改成了Http11Nio2协议,以实现在有限的部署资源的情况下,提高API网关的吞吐量。设置Http11Nio2协议可以参照如下代码。
1 /** 2 * 配置tomcat使用Nio2作为IO协议 3 */ 4 @Configuration 5 public class TomcatCustomizer implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> { 6 7 @Override 8 public void customize(ConfigurableServletWebServerFactory factory) { 9 ((TomcatServletWebServerFactory)factory).setProtocol("org.apache.coyote.http11.Http11Nio2Protocol"); 10 } 11 }
- 压测验证
我们在相同的软、硬配置下,使用Http11Nio与Http11Nio2协议分别对单节点进行压测,从服务的吞吐量结果来看,Http11Nio2协议下,吞吐量提高了至少20%以上,特别是并发线程越高的场景下,Http11Nio协议下,请求吞吐量下降很明显。从监控数据来看,在并发越高的场景,Http11Nio协议下CPU的平均使用率更高,基本也说明了在NIO的模式在高并发的场景下IO阻塞是很明显的。
另外我们也发现,Tomcat自身最大的处理线程如果设置的比较大,服务本身的吞吐量也出现了明显的下降,通过对监控的分析,这种情况应该是线程太多导致CPU将大量的时间花费在上下文切换上导致的。
2.3.3 高可用
API网关作为整个微服务体系中的重要的基础服务,我们对API网关在可用性方面的目标是:
- 即使其他业务服务都挂了,网关服务都应该尽可能保证可用;
- 即使网关服务不可用了,在业务服务恢复后,网关也要能够快速自恢复。
要达到以上的目标,API网关必须拥有限流、熔断降级等方面的能力。目前限流熔断的组件主要有SpringCloud Hystrix,Alibaba Sentinel,经过对两者的比较,同时结合公司的技术背景,我们最终选择使用Alibaba Sentinel组件来实现限流、熔断降级。
因为在实现中,我们是基于公司自研的配置中心+Sentinel来实现动态配置的,所以此处不直接将代码贴出来。但是这里有一篇以Apollo为配置中心,同时结合Sentinel的文章,实现思路清晰,讲解详细,需要的同学可以参照。Apollo+Sentinel示例:https://anilople.github.io/Sentinel/#/zh/README
2.3.4 安全性
- API网关作为业务服务暴露数据最重要的途径之一,网关层需要有能力在出现恶意请求时快速切断外部请求以保证公司数据安全的能力。除了借助集团安全风控的整体能力,目前网关层提供了IP黑名单配置能力,以方便快速切断恶意IP的请求。
- API网关作为客户端请求的第一站,在日常排查问题中往往希望网关层能够提供尽可能多日志以方便排查问题,从问题排查的角度提出这个想法无可厚非。但如果考虑到数据安全性,网关层是不应该过多的打印出业务服务的业务数据日志的,因为网关层绝大数情况下是不关注也不了解业务的,如果将业务服务的数据都打印出来,则可能在网关层造成业务数据泄漏。因此结合数据安全性和排查问题的便捷性两个诉求,默认情况下不支持业务数据日志的记录,但在一些特殊的时候可以通过开关打开日志的记录。
- 后面会结合限流组件的能力,提供对特定IP在自动限流的能力,以实现更智能的安全校验。
三、总结
1、API网关作为微服务体系中重要的基础服务,在设计的时候,需要重点考虑可维护性,因此在微观实现层面可以借鉴DDD的思想,将整个流程分为预处理、转发、返回、异常四个领域来实现结构清晰, 易于扩展,提高了可维护性。
2、API网关本身不涉及或者很少涉及业务逻辑,其更多关注的是IO的问题,因此在设计API网关时,需要对BIO、NIO、NIO2、AIO、IO多通道复用等概念有比较好的理解,这样才能结合实际场景选择合适的技术方案。我们对IO的核心概念有了比较好的理解时,你可能也就大概知道了Zuul1.x与Zuul2.x和Spring Cloud GateWay的核心区别,同时对像Redis这样的中间件如何在单线程的情况下做到高性能有了一些理解。
3、准确理解限流、熔断、降级的概念,可以更好的帮助我们选择合适的技术方案来实现高可用。
限流、熔断针对的主体不一样,其中
限流是相对于调用方而言的,当调用方在一段时间内请求量超过某个阀值,则对调用方进行请求限速处理;
熔断是相对于服务方而言的,当依赖的服务出现异常时,如果不进行任何处理,API网关会因为雪崩被某一个业务服务拖垮,进而导致整个API网关时去响应。这种情况下,为了保证API网关的可用性,需要对异常的服务实行快速失败的策略,这种策略就是熔断。
降级是一种结果,对客户端请求限流、或者对依赖方进行熔断的结果都是一种降级。
springboot 部分转发(不使用拦截器)
前言
本篇文章实现springboot在没有接口可以访问的时候,也就是正常来说服务器应该返回404的接口转发到其他的服务器上的功能需要有一定的springboot基础
一、Interceptor
一想到转发,首先我想到的就是拦截器了,噼里啪啦就开始操作,代码类似如下:
1 import java.nio.charset.StandardCharsets; 2 import java.util.Enumeration; 3 import java.util.HashMap; 4 import java.util.List; 5 import java.util.Map; 6 7 import javax.servlet.http.HttpServletRequest; 8 import javax.servlet.http.HttpServletResponse; 9 10 import com.aw.ccpos.util.HttpServletRequestReader; 11 import com.aw.ccpos.util.HttpUtils; 12 import com.aw.ccpos.util.SysUtil; 13 import com.aw.service.pos.base.model.RequestModel; 14 import org.slf4j.Logger; 15 import org.slf4j.LoggerFactory; 16 import org.springframework.beans.factory.annotation.Autowired; 17 import org.springframework.stereotype.Component; 18 import org.springframework.util.StreamUtils; 19 import org.springframework.web.servlet.HandlerInterceptor; 20 import org.springframework.web.servlet.ModelAndView; 21 22 @Component 23 public class InfoInterceptor implements HandlerInterceptor { 24 private static final Logger LOG = LoggerFactory.getLogger(InfoInterceptor.class); 25 @Override 26 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) 27 throws Exception { 28 return true;// 只有返回true才会继续向下执行,返回false取消当前请求 29 } 30 31 @Override 32 public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, 33 ModelAndView modelAndView) throws Exception { 34 35 } 36 }
postHandle方法中的ModelAndView 可以自定义404的错误页面,HttpServletResponse 则是返回,但是尝试过后发现所有的接口都要这么搞一遍,意思是如果通过这个办法所有的接口都会被拦截转发,这并不是我们想要的
二、/**
左思右想过后采用了如下方法
1 import com.aw.ccpos.constants.Constants; 2 import io.swagger.annotations.Api; 3 import org.springframework.beans.factory.annotation.Autowired; 4 import org.springframework.http.MediaType; 5 import org.springframework.http.ResponseEntity; 6 import org.springframework.web.bind.annotation.RequestMapping; 7 import org.springframework.web.bind.annotation.RequestMethod; 8 import org.springframework.web.bind.annotation.RestController; 9 10 11 import javax.servlet.http.HttpServletRequest; 12 import javax.servlet.http.HttpServletResponse; 13 14 @RestController 15 @Api(value = "GraphDB", tags = { 16 "graphdb-Api" 17 }) 18 public class GraphDBController { 19 20 21 22 23 @Autowired 24 private RoutingDelegate routingDelegate; 25 26 @RequestMapping(value = "/**", method = {RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.DELETE}, produces = MediaType.TEXT_PLAIN_VALUE) 27 public ResponseEntity catchAll(HttpServletRequest request, HttpServletResponse response) { 28 return routingDelegate.redirect(request, response, Constants.SERVER_URL, null); 29 } 30 }
既然我们只需要处理服务器找不到的方法,那么只需要/**然后包含我们页面的请求类型就好了,因为我代理转发的工具类需要MediaType.TEXT_PLAIN_VALUE类型,所以我这么设置,读者可以如不需要可以自己写转发,RoutingDelegate 转发工具类会在后文提供
1.RoutingDelegate
转发工具类 代码如下(示例):
1 import com.aw.ccpos.util.HttpsUtils; 2 import org.apache.http.conn.ssl.NoopHostnameVerifier; 3 import org.apache.http.conn.ssl.SSLConnectionSocketFactory; 4 import org.apache.http.conn.ssl.TrustStrategy; 5 import org.apache.http.impl.client.CloseableHttpClient; 6 import org.apache.http.impl.client.HttpClients; 7 import org.springframework.http.*; 8 import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; 9 import org.springframework.stereotype.Service; 10 import org.springframework.util.MultiValueMap; 11 import org.springframework.util.StreamUtils; 12 import org.springframework.web.client.RestTemplate; 13 14 import javax.net.ssl.*; 15 import javax.servlet.http.HttpServletRequest; 16 import javax.servlet.http.HttpServletResponse; 17 import java.io.IOException; 18 import java.io.InputStream; 19 import java.net.URI; 20 import java.net.URISyntaxException; 21 import java.security.KeyManagementException; 22 import java.security.KeyStoreException; 23 import java.security.NoSuchAlgorithmException; 24 import java.util.Collections; 25 import java.util.List; 26 27 28 import java.io.IOException; 29 import java.net.HttpURLConnection; 30 import java.security.SecureRandom; 31 import java.security.cert.X509Certificate; 32 33 @Service 34 public class RoutingDelegate { 35 36 37 public ResponseEntity<String> redirect(HttpServletRequest request, HttpServletResponse response,String routeUrl, String prefix) { 38 try { 39 // build up the redirect URL 40 String redirectUrl = createRedictUrl(request,routeUrl, prefix); 41 RequestEntity requestEntity = createRequestEntity(request, redirectUrl); 42 return route(requestEntity); 43 } catch (Exception e) { 44 return new ResponseEntity("REDIRECT ERROR", HttpStatus.INTERNAL_SERVER_ERROR); 45 } 46 } 47 48 private String createRedictUrl(HttpServletRequest request, String routeUrl, String prefix) { 49 String queryString = request.getQueryString(); 50 return routeUrl + request.getRequestURI() + 51 (queryString != null ? "?" + queryString : ""); 52 } 53 54 55 private RequestEntity createRequestEntity(HttpServletRequest request, String url) throws URISyntaxException, IOException { 56 String method = request.getMethod(); 57 HttpMethod httpMethod = HttpMethod.resolve(method); 58 MultiValueMap<String, String> headers = parseRequestHeader(request); 59 byte[] body = parseRequestBody(request); 60 return new RequestEntity<>(body, headers, httpMethod, new URI(url)); 61 } 62 63 private ResponseEntity<String> route(RequestEntity requestEntity) { 64 ResponseEntity<String> responseEntity; 65 responseEntity = getRestTemplate().exchange(requestEntity, String.class); 66 return responseEntity; 67 } 68 69 70 private byte[] parseRequestBody(HttpServletRequest request) throws IOException { 71 InputStream inputStream = request.getInputStream(); 72 return StreamUtils.copyToByteArray(inputStream); 73 } 74 75 private MultiValueMap<String, String> parseRequestHeader(HttpServletRequest request) { 76 HttpHeaders headers = new HttpHeaders(); 77 List<String> headerNames = Collections.list(request.getHeaderNames()); 78 for (String headerName : headerNames) { 79 List<String> headerValues = Collections.list(request.getHeaders(headerName)); 80 for (String headerValue : headerValues) { 81 headers.add(headerName, headerValue); 82 } 83 } 84 return headers; 85 } 86 87 public static RestTemplate getRestTemplate() { 88 try { 89 SSLContext sslContext = SSLContext.getInstance("TLS"); 90 KeyManager[] keyManagers = HttpsUtils.prepareKeyManager(null, null); 91 TrustManager[] trustManagers = HttpsUtils.prepareTrustManager(null); 92 TrustManager trustManager = null; 93 if (trustManagers != null) { 94 trustManager = new HttpsUtils.MyTrustManager( HttpsUtils.chooseTrustManager(trustManagers)); 95 } else { 96 trustManager = new HttpsUtils.UnSafeTrustManager(); 97 } 98 sslContext.init(keyManagers, new TrustManager[]{trustManager}, new SecureRandom()); 99 SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(sslContext, 100 new String[]{"TLSv1"}, 101 null, 102 NoopHostnameVerifier.INSTANCE); 103 104 CloseableHttpClient httpClient = HttpClients.custom() 105 .setSSLSocketFactory(csf) 106 .build(); 107 108 HttpComponentsClientHttpRequestFactory requestFactory = 109 new HttpComponentsClientHttpRequestFactory(); 110 111 requestFactory.setHttpClient(httpClient); 112 RestTemplate restTemplate = new RestTemplate(requestFactory); 113 return restTemplate; 114 }catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException e) { 115 throw new AssertionError(e); 116 } 117 } 118 }
就是一个比较标准的https的转发类,比较值得一提的是getRestTemplate()方法,它封装了一个信任任何证书的RestTemplate转发,不然https请求转发会报错,工具类HttpsUtil在下文会给出
2.HttpsUtils(https支持)
代码如下(示例):
1 import java.io.IOException; 2 import java.io.InputStream; 3 import java.lang.reflect.Field; 4 import java.security.KeyManagementException; 5 import java.security.KeyStore; 6 import java.security.KeyStoreException; 7 import java.security.NoSuchAlgorithmException; 8 import java.security.SecureRandom; 9 import java.security.cert.CertificateException; 10 import java.security.cert.CertificateFactory; 11 import java.security.cert.X509Certificate; 12 13 import javax.net.ssl.HostnameVerifier; 14 import javax.net.ssl.KeyManager; 15 import javax.net.ssl.KeyManagerFactory; 16 import javax.net.ssl.SSLContext; 17 import javax.net.ssl.SSLSession; 18 import javax.net.ssl.SSLSocketFactory; 19 import javax.net.ssl.TrustManager; 20 import javax.net.ssl.TrustManagerFactory; 21 import javax.net.ssl.X509TrustManager; 22 23 import okhttp3.OkHttpClient; 24 import org.slf4j.LoggerFactory; 25 26 public class HttpsUtils { 27 private static final org.slf4j.Logger Logger = LoggerFactory.getLogger(HttpsUtils.class); 28 public static SSLSocketFactory getSslSocketFactory(InputStream[] certificates, InputStream bksFile, String password) { 29 try { 30 TrustManager[] trustManagers = prepareTrustManager(certificates); 31 KeyManager[] keyManagers = prepareKeyManager(bksFile, password); 32 SSLContext sslContext = SSLContext.getInstance("TLS"); 33 TrustManager trustManager = null; 34 if (trustManagers != null) { 35 trustManager = new MyTrustManager(chooseTrustManager(trustManagers)); 36 } else { 37 trustManager = new UnSafeTrustManager(); 38 } 39 sslContext.init(keyManagers, new TrustManager[]{trustManager}, new SecureRandom()); 40 return sslContext.getSocketFactory(); 41 } catch (NoSuchAlgorithmException e) { 42 throw new AssertionError(e); 43 } catch (KeyManagementException e) { 44 throw new AssertionError(e); 45 } catch (KeyStoreException e) { 46 throw new AssertionError(e); 47 } 48 } 49 50 public static TrustManager[] prepareTrustManager(InputStream... certificates) { 51 if (certificates == null || certificates.length <= 0) return null; 52 try { 53 CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); 54 KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); 55 keyStore.load(null); 56 int index = 0; 57 for (InputStream certificate : certificates) { 58 String certificateAlias = Integer.toString(index++); 59 keyStore.setCertificateEntry(certificateAlias, certificateFactory.generateCertificate(certificate)); 60 try { 61 if (certificate != null) 62 certificate.close(); 63 } catch (IOException e) { 64 Logger.error("error to close certificate", e); 65 } 66 } 67 TrustManagerFactory trustManagerFactory = null; 68 trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); 69 trustManagerFactory.init(keyStore); 70 TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); 71 return trustManagers; 72 } catch (Exception e) { 73 Logger.error("fail to prepareTrustManager", e); 74 } 75 return null; 76 77 } 78 79 public static KeyManager[] prepareKeyManager(InputStream bksFile, String password) { 80 try { 81 if (bksFile == null || password == null) return null; 82 KeyStore clientKeyStore = KeyStore.getInstance("BKS"); 83 clientKeyStore.load(bksFile, password.toCharArray()); 84 KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); 85 keyManagerFactory.init(clientKeyStore, password.toCharArray()); 86 return keyManagerFactory.getKeyManagers(); 87 } catch (Exception e) { 88 Logger.error("fail to prepareKeyManager", e); 89 } 90 return null; 91 } 92 93 public static X509TrustManager chooseTrustManager(TrustManager[] trustManagers) { 94 for (TrustManager trustManager : trustManagers) { 95 if (trustManager instanceof X509TrustManager) { 96 return (X509TrustManager) trustManager; 97 } 98 } 99 return null; 100 } 101 102 public static void ignoreSSLCheck(OkHttpClient sClient) { 103 SSLContext sc = null; 104 try { 105 sc = SSLContext.getInstance("SSL"); 106 sc.init(null, new TrustManager[]{new X509TrustManager() { 107 @Override 108 public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { 109 110 } 111 112 @Override 113 public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { 114 115 } 116 117 @Override 118 public X509Certificate[] getAcceptedIssuers() { 119 return null; 120 } 121 }}, new SecureRandom()); 122 } catch (Exception e) { 123 Logger.error("fail to ignoreSSLCheck", e); 124 } 125 126 HostnameVerifier hv1 = new HostnameVerifier() { 127 @Override 128 public boolean verify(String hostname, SSLSession session) { 129 return true; 130 } 131 }; 132 133 String workerClassName = "okhttp3.OkHttpClient"; 134 try { 135 Class workerClass = Class.forName(workerClassName); 136 Field hostnameVerifier = workerClass.getDeclaredField("hostnameVerifier"); 137 hostnameVerifier.setAccessible(true); 138 hostnameVerifier.set(sClient, hv1); 139 140 Field sslSocketFactory = workerClass.getDeclaredField("sslSocketFactory"); 141 sslSocketFactory.setAccessible(true); 142 sslSocketFactory.set(sClient, sc.getSocketFactory()); 143 } catch (Exception e) { 144 Logger.error("fail to hostnameVerifier", e); 145 } 146 } 147 148 public static class UnSafeTrustManager implements X509TrustManager { 149 @Override 150 public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { 151 } 152 153 @Override 154 public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { 155 } 156 157 @Override 158 public X509Certificate[] getAcceptedIssuers() { 159 return new X509Certificate[]{}; 160 } 161 } 162 163 public static class MyTrustManager implements X509TrustManager { 164 private X509TrustManager defaultTrustManager; 165 private X509TrustManager localTrustManager; 166 167 public MyTrustManager(X509TrustManager localTrustManager) throws NoSuchAlgorithmException, KeyStoreException { 168 TrustManagerFactory var4 = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); 169 var4.init((KeyStore) null); 170 defaultTrustManager = chooseTrustManager(var4.getTrustManagers()); 171 this.localTrustManager = localTrustManager; 172 } 173 174 @Override 175 public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { 176 } 177 178 @Override 179 public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { 180 try { 181 defaultTrustManager.checkServerTrusted(chain, authType); 182 } catch (CertificateException ce) { 183 localTrustManager.checkServerTrusted(chain, authType); 184 } 185 } 186 187 @Override 188 public X509Certificate[] getAcceptedIssuers() { 189 return new X509Certificate[0]; 190 } 191 } 192 193 private class UnSafeHostnameVerifier implements HostnameVerifier { 194 @Override 195 public boolean verify(String hostname, SSLSession session) { 196 return true; 197 } 198 } 199 200 201 202 }
总结
以上完成springboot部分代理转发,共勉
参考:https://blog.csdn.net/Knight_hf/article/details/122885349
参考:https://blog.csdn.net/shop_and_sleep/article/details/123986433
参考:https://blog.csdn.net/m0_46459413/article/details/123871658
参考:https://blog.csdn.net/weixin_38192427/article/details/121236731
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 上周热点回顾(3.3-3.9)
2017-09-14 Android Studio调试报错am startservice