我们开发微信公众号,微信官方规定,公众号请求必须配置已备案的域名,并且只支持80和443端口。以往的做法是每次写完代码发布到映射了域名的服务器上,通过打印日志来调试,这种做法很麻烦,也不利于调试,还有就是利用花生壳这类内网穿透软件,在自己的电脑上啊映射花生壳域名,但是现在大多数内网穿透软件都要收费,而且还比较昂贵,免费的要么不稳定,要么域名经常变。那么,我们还有什么办法可以实现公众号的本地化调试呢?本Chat将告诉你答案!
本文的本章标题是如何实现微信公众号本地化调试,其实本文的主题远不止这些,作者写本文的目的是为了解决一些大型企业或者大型国企对于网络环境的限制,以及我们如何突破这些限制而完成我们的工作。
本文以微信公众号调试为例,告诉大家,如何在内网环境进行微信公众号的调试,包括一些企业对一些端口比如 3389 的禁用,如何绕过这些限制,进而远程控制服务器。
我们开发微信公众号,需要在公众号后台设置一些安全域名,而微信公众号规定:必须有已经备案的域名,并且只支持 80 和 443 端口。
如果我们在内网环境,公众号是无法调用回来的,而一些大型企业往往会有一个称之为安全域的服务器,只有该服务器可以对外访问,并且映射域名,其有一个内网 IP 和访问到我们的内网。而对于前后端架构来说,前端部署在安全域,通过域名来访问,但是当用户在外网访问前端界面时,如果指定到内网的后端接口地址,是访问不通的。
这时,我们就想到需要在安全域部署一个代理服务器转到内网地址。对于一些大型企业尤其是国企,是禁止使用类似 nginx 的反向代理软件的。鉴于这种情况,我们首先想到的就是自己实现一个代理功能。
利用 netty 的长连接机制在安全域服务器和内网服务器之间建立一个长连接通道,通过 TCP 协议接受外部的请求,将外部的请求流原样发送给内网服务器,而内网服务器处理完请求后,再将返回的数据发送给安全域的代理应用,代理应用再原样返回给客户端,这样我们就能利用在安全域部署的代理服务器间接地请求到内网地址。
说了这么多废话,那到底是怎样实现的呢,下面我们就来看一看代理服务器的核心代码(全部代码已上传,请自行下载):
UTF-8 1.18.0 org.springframework.boot spring-boot-dependencies 2.0.3.RELEASE pom import org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-configuration-processor org.springframework.boot spring-boot-devtools io.netty netty-all org.projectlombok lombok ${lombok.version} provided
@SpringBootApplicationpublic class StartProgram { public static void main(String[] args) { SpringApplication.run(StartProgram.class, args); }}
app-config: #尝试重连间隔时间(单位:毫秒) interval: 1000 proxy[0]: host: 127.0.0.1 port: 8080 port: 8088server: # WebServer bind port port: 9010
连接代码如下:
@Async("frontendWorkTaskExecutor") public Future initProxyServer() { EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(frontendPipeline) .childOption(ChannelOption.AUTO_READ, false); ChannelFuture f = b.bind(appConfig.getPort()).sync(); System.out.println("启动代理服务,端口:" + ((InetSocketAddress) f.channel().localAddress()).getPort()); f.channel().closeFuture().sync(); } catch (InterruptedException e) { LOGGER.debug("代理服务关闭!"); } catch (Exception e) { LOGGER.error("代理服务启动失败!", e); } finally { workerGroup.shutdownGracefully(); bossGroup.shutdownGracefully(); } return new AsyncResult<>(true); }
然后编写接收客户端的请求 Handler 和从目标服务器返回的数据 Handler:
@Component@Scope(scopeName = ConfigurableBeanFactory.SCOPE_PROTOTYPE)public class ProxyFrontendHandler extends SimpleChannelInboundHandler { private static final Logger log = LoggerFactory.getLogger(ProxyFrontendHandler.class); // 代理服务器和目标服务器之间的通道(从代理服务器出去所以是outbound过境)// private volatile ChannelGroup allChannels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); @Autowired private AppConfig appConfig; private volatile Queue queue; private volatile boolean frontendConnectStatus = false; /** * Closes the specified channel after all queued write requests are flushed. */ public static void closeOnFlush(Channel ch) { if (ch.isActive()) { ch.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); } } /** * 当客户端和代理服务器建立通道连接时,调用此方法 * * @param ctx * @throws Exception */ @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { frontendConnectStatus = true; SocketAddress clientAddress = ctx.channel().remoteAddress(); log.info("客户端地址:" + clientAddress); List proxy = appConfig.getProxy(); if(null == queue){ queue = new ArrayBlockingQueue<>(proxy.size()); } /** * 客户端和代理服务器的连接通道 入境的通道 */ Channel inboundChannel = ctx.channel(); proxy.stream().forEach(item -> createBootstrap(inboundChannel, item.getHost(), item.getPort())); } /** * 在这里接收客户端的消息 在客户端和代理服务器建立连接时,也获得了代理服务器和目标服务器的通道outbound, * 通过outbound写入消息到目标服务器 * * @param ctx * @param msg * @throws Exception */ @Override public void channelRead0(final ChannelHandlerContext ctx, byte[] msg) throws Exception { log.info("客户端消息"); ChannelGroup channels = queue.poll(); channels.writeAndFlush(msg).addListener((ChannelGroupFutureListener)future -> ctx.channel().read()); queue.add(channels); } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { log.info("代理服务器和客户端断开连接"); frontendConnectStatus = false; ChannelGroup channels = queue.poll(); if(null != queue){ channels.close(); queue.add(channels); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { log.error("发生异常:", cause); ctx.channel().close(); } public void createBootstrap(final Channel inboundChannel, final String host, final int port) { try { Bootstrap bootstrap = new Bootstrap(); bootstrap.group(inboundChannel.eventLoop()); bootstrap.channel(NioSocketChannel.class); bootstrap.handler(new BackendPipeline(inboundChannel, ProxyFrontendHandler.this, host, port)); ChannelFuture f = bootstrap.connect(host, port); f.addListener((ChannelFutureListener)future -> { if (future.isSuccess()) { ChannelGroup allChannels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); allChannels.add(future.channel()); queue.offer(allChannels); } else { if (inboundChannel.isActive()) { log.info("Reconnect"); final EventLoop loop = future.channel().eventLoop(); loop.schedule(()->ProxyFrontendHandler.this.createBootstrap(inboundChannel, host, port), appConfig.getInterval(), TimeUnit.MILLISECONDS); } else { log.info("notActive"); } } inboundChannel.read(); }); } catch (Exception e) { } } public boolean isConnect() { return frontendConnectStatus; }}
public class ProxyBackendHandler extends SimpleChannelInboundHandler { private static final Logger LOGGER = LoggerFactory.getLogger(ProxyBackendHandler.class); private Channel inboundChannel; private ProxyFrontendHandler proxyFrontendHandler; private String host; private int port; public ProxyBackendHandler(Channel inboundChannel, ProxyFrontendHandler proxyFrontendHandler, String host, int port) { this.inboundChannel = inboundChannel; this.proxyFrontendHandler = proxyFrontendHandler; this.host = host; this.port = port; } // 当和目标服务器的通道连接建立时 @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { LOGGER.info("服务器地址:" + ctx.channel().remoteAddress()); } /** * msg是从目标服务器返回的消息 * * @param ctx * @param msg * @throws Exception */ @Override public void channelRead0(final ChannelHandlerContext ctx, byte[] msg) throws Exception { LOGGER.info("服务器返回消息"); /** * 接收目标服务器发送来的数据并打印 然后把数据写入代理服务器和客户端的通道里 */ // 通过inboundChannel向客户端写入数据 inboundChannel.writeAndFlush(msg).addListener((ChannelFutureListener)future -> { if (!future.isSuccess()) { future.channel().close(); } }); } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { LOGGER.info("关闭服务器连接"); if (proxyFrontendHandler.isConnect()) { proxyFrontendHandler.createBootstrap(inboundChannel, host, port); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { LOGGER.error("发生异常:", cause); ctx.channel().close(); }}
我们通过 ProxyFrontendHandler
将接收到的客户端请求发送给目标服务器,而在 ProxyBackendHandler
将目标服务器返回的数据拿到返回给客户端。通过这样的一个流程,就能实现外网客户端请求到内网地址的这样一个需求。
java -jar --server.port=9999 proxy.jar --app-config.port=8383 --app-config.proxy[0].host=10.120.133.39 --app-config.proxy[0].port=3389
其中,server.port
为应用启动端口,app-config
为代理服务器启动端口,app-config.proxy[0]
为目标服务器的IP和端口,可以指定多个。
因为我们通过 netty 实现的是一个 TCP 长连接,他的作用不止于转发 http 请求,他可以代理任何一个网络请求,比如公司对 3389 端口禁用了,而远程服务器的端口为 3389,那么,我们可以找一个能连接上 3389 端口的主机,将代理服务器部署到该电脑上,设置代理服务器端口,如上面指定的为 8383,我们就可以通过 8383 间接地连接到目标服务器。
本文首发于GitChat,未经授权不得转载,转载需与GitChat联系。
阅读全文: http://gitbook.cn/gitchat/activity/5b7d11a8cacd636ab8dfec9b
您还可以下载 CSDN 旗下精品原创内容社区 GitChat App ,阅读更多 GitChat 专享技术内容哦。