背景

最近在开发一款微信小程序,实现学校教务管理的信息(成绩、课表等)的抓取与发布。
教务系统服务器处于校内网的环境,在外网下,有且仅有通过vpn账号和学校内部服务器通信。

要实现小程序查询功能,有以下两种方式

  1. 查询服务器完全部署在云端,通过vpn账号与学校联通;
  2. 查询服务器部署在内网环境下,通过内网穿透的方式,与云端的代理连通。

一般来说,vpn账号关乎个人隐私,与网络费用挂钩。此次小程序的开发,我希望遵循**“简化、安全”**的宗旨,希望借助ngrok内网穿透服务,来代理网络的https请求,转发到内网的服务器上。

安装GO环境

网络服务器的环境为ubuntu 18.04 x64,借助apt包直接安装

1
sudo apt install build-essential golang openssl
  • 查看go语言版本
1
go version

下载ngrok源码

当前最新的ngrok版本为2.x,但是最新的2.x版本不开源,仅提供1.x可用。且ngrok分服务端和用户端两部分,一般来说,云端服务器做服务端,内网服务端做用户端,且编译依赖于SSL证书,相当于仅仅能一对一使用。

1
2
git clone https://github.com/inconshreveable/ngrok.git
cd ngrok

配置私有SSL证书信息

  • 注意:使用私有的SSL证书,并不会被浏览器/微信小程序开发承认。小程序要求所有request要求必须是经过https加密传输,并且安全SSL证书认证的。当前https认证第三方组织的少有免费的。
1
2
3
4
5
6
7
8
9
10
11
NGROK_DOMAIN="btbuquery.top"    #注意域名换成你自己的
openssl genrsa -out base.key 2048
openssl req -new -x509 -nodes -key base.key -days 10000 -subj "/CN=$NGROK_DOMAIN" -out base.pem
openssl genrsa -out server.key 2048
openssl req -new -key server.key -subj "/CN=$NGROK_DOMAIN" -out server.csr
openssl x509 -req -in server.csr -CA base.pem -CAkey base.key -CAcreateserial -days 10000 -out server.crt

#将生成的证书文件拷贝到指定位置,替代默认证书
cp base.pem assets/client/tls/ngrokroot.crt
cp server.crt assets/server/tls/snakeoil.crt
cp server.key assets/server/tls/snakeoil.key

使用阿里云免费的SSL一年认证

来自由DigiCert Inc公司提供了一年时长的免费SSL认证,因为在编译依赖SSL证书

从阿里云下载到认证的证书

共四个文件,将第二个后缀重命名为xxxxxxxxxxx.crt,传到ngrok的目录下

复制到assets/server/tls/文件夹下

1
2
cp *.crt assets/server/tls/snakeoil.crt
cp *.key assets/server/tls/snakeoil.key

编译服务端ngrokd

1
2
3
4
5
6
7
# 编译64位linux平台服务端
GOOS=linux GOARCH=amd64 make release-server
# 编译64位windows客户端
GOOS=windows GOARCH=amd64 make release-server
# 编译是64位mac客户端
GOOS=darwin GOARCH=amd64 make release-server
# 如果是32位,GOARCH=386
  • 关于arm的编译
1
GOOS=linux GOARCH=arm make release-server

编译客户端ngrok

按照平台需求,设置不同参数,与编译服务端一致。

1
make release-client

执行后会在ngrok/bin目录及其子目录下看到服务端ngrokd和客户端ngrok二进制可执行文件

运行服务端ngrokd

1
2
cd bin/
sudo ./ngrokd -domain="btbuquery.top" -httpAddr=":80" -httpsAddr=":443" -tunnelAddr=":4443"

httpAddr:设置代理的http端口,默认80;
httpsAddr:设置代理的https端口,默认443;
tunnelAddr:设置ngrok通信端口,默认4443。

运行客户端ngrok

将云服务器上的ngrok/bin/ngrok文件传输到在内网服务器上,以在ngrok文件下为例:

  1. 新建一ngrok.cfg文件,写入以下内容
  • http代理:设置子域名(以www为例),本地端口(以8000为例)
  • ssh代理:远程端口22
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
server_addr: btbuquery.top:4443
trust_host_root_certs: true # 需要第三方SSL认证
inspect_addr: 0.0.0.0:4040

tunnels:
http:
proto:
http: 8000
subdomain: "www"

https:
proto:
https: 8000
subdomain: "www"

ssh:
remote_port: 222
proto:
tcp: 22


  • 注意:若使用私有SSL认证,必须设置trust_host_root_certs为false
  1. 运行客户端
1
./ngrok -log=ngrok.log -config=ngrok.cfg start http ssh

相当于实现
http://www.btbuquery.top 转发到内网 127.0.0.1:8000
https://www.btbuquery.top 转发到内网 127.0.0.1:8000
tcp://btbuquery.top:222 转发到内网 120.0.0.1:222

服务端加入系统启动服务

  1. 在ngrok文件下新建start.sh脚本,写入以下内容
1
/root/project/ngrok/bin/ngrokd -domain="btbuquery.top" -httpAddr=":80" -httpsAddr=":443" -tunnelAddr=":4443"
  1. 在/etc/init.d新建ngrok文件,写入以下内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/sh
### BEGIN INIT INFO
# Provides: ngrok
# Required-Start:
# Required-Stop:
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: Start or stop the ngrok Proxy.
### END INIT INFO

ngrok_path=/root/project/ngrok #指定ngrok文件夹

case "$1" in
start)
echo "start ngrok service.."
sh ${ngrok_path}/start.sh
;;
*)
exit 1
;;
esac
  1. 加入开机启动
1
2
3
cd /etc/init.d/
sudo chmod 755 ngrok
sudo update-rc.d ngrok defaults 90

用文件作为Swap分区

  1. 创建要作为swap分区的文件:增加1GB大小的交换分区,则命令写法如下
1
dd if=/dev/zero of=/root/swapfile bs=1M count=1024

文件大小 size = bs * count
if 为输入文件, of 指定输出文件

  1. 格式化为交换分区文件
1
mkswap /root/swapfile 			# 建立swap的文件系统
  1. 启用交换分区文件
1
swapon /root/swapfile 			# 启用swap文件
  1. 系统开机时自启用
1
2
sudo nano /etc/fstab 			# 在文件/etc/fstab中添加一行
/root/swapfile swap swap defaults 0 0

ubuntu安装shadowsocks-libev

本有一篇博文写的是使用docker快速部署shadowsocks,但是存在以下问题:

  • docker部署上是基于python2.7下的shadowsocks包,效率不高;
  • python(包括2/3)下的shadowsocks包上次更新已经是2015年8月,距今时间过久,存在版本迭代上的一些bug。

这次的安装使用shadowsocks-libev,其使用C写的ss服务端,附上github项目地址:

https://github.com/shadowsocks/shadowsocks-libev

1
2
sudo apt-get update
sudo apt-get install shadowsocks-libev

相关文件位置说明

启动文件:/etc/init.d/shadowsocks-libev
配置文件: /etc/shadowsocks-libev/config.json
一些默认启动配置: /etc/default/shadowsocks-libev

编辑shadowsocks参数

修改配置文件:

1
sudo nano /etc/shadowsocks-libev/config.json

修改样例:

1
2
3
4
5
6
7
8
{
"server":["[::0]", "0.0.0.0"],
"server_port":8388,
"local_port":1080,
"password":"PASSWORD",
"timeout":60,
"method":"aes-256-cfb"
}

"server"中:使用["[::0]", "0.0.0.0"]分别监听ipv6、ipv4所有请求
"password"中:修改为所想设置的密码

  • 注意:以前在shadowsock-python有使用"server":"::"会默认监听ipv4、ipv6,但是该设置在shadowsocks-libev不生效,仅仅监听ipv6

控制shadowsocks

1
2
3
sudo service shadowsocks-libev start    # 重启shadowsocks
sudo service shadowsocks-libev stop # 关闭shadowsocks
sudo service shadowsocks-libev restart # 参数改变后重启生效

查看是否启动,返回结果样例

1
2
3
4
ps aux |grep ss-server

nobody 16623 0.0 0.9 32088 4672 ? Ss 12:05 0:00 /usr/bin/ss-server -c /etc/shadowsocks-libev/config.json -u
root 16825 0.0 0.2 14856 1068 pts/1 S+ 13:12 0:00 grep --color=auto ss-server

注意:使用shadowsocks的ipv6必须保证双栈(服务器和客户端均启用ipv6)支持。附一个ipv6检测网址

本来听说校园网是支持ipv6的,还有可能不限流量,但是……

环境说明

Linux环境:deepin 15.5 (debian / x64)
tomcat:tomcat8
IDEA:jetbrain IntelliJ IDEA Ultimate(支持java web环境)

tomcat 安装配置

安装tomcat8

1
2
sudo apt-get update
sudo apt-get install tomcat8

设置tomcat文件权限

  • Tomcat home directory : /usr/share/tomcat8
  • Tomcat base directory : /var/lib/tomcat8
1
2
cd /usr/share/tomcat8 && sudo chmod -R 755 *
cd /var/lib/tomcat8 && sudo chmod -R 755 *

启用与关闭tomcat

1
2
sudo service tomcat8 start  #开启tomcat8服务,会占用8080端口
sudo service tomcat8 stop #关闭tomcat8服务

文件环境

run/debug 参数

项目结构

文件结构

JcaptchaServlet源码:后端生成验证图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import javax.imageio.ImageIO;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Random;

public class JcaptchaServlet extends HttpServlet {
private Random random = new Random();
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {

int height = 220; //图片高
int width = 220; //图片宽
BufferedImage image = new BufferedImage(width,height,BufferedImage.TYPE_INT_RGB);
Graphics2D g = (Graphics2D) image.getGraphics();
String picPath= JcaptchaServlet.class.getClassLoader().getResource("../image/"+(random.nextInt(4)+1)+".jpg").getPath(); //读取本地图片,做背景图片
g.drawImage(ImageIO.read(new File(picPath)), 0, 20, width, height, null); //将背景图片从高度20开始
g.setColor(Color.white); //设置颜色
g.drawRect(0, 0, width-1, height-1); //画边框

g.setFont(new Font("宋体",Font.BOLD,20)); //设置字体
Integer x=null,y=null; //用于记录坐标
String target=null; // 用于记录文字
for(int i=0;i<4;i++){ //随机产生4个文字,坐标,颜色都不同
g.setColor(new Color(random.nextInt(50)+200, random.nextInt(150)+100, random.nextInt(50)+200));
String str=getRandomChineseChar();
int a=random.nextInt(width-100) + 50;
int b=random.nextInt(height-70) + 55;
if(x==null){
x=a; //记录第一个x坐标
}
if(y==null){
y=b;//记录第一个y坐标
}
if(target==null){
target=str; //记录第一个文字
}
g.drawString(str, a, b);
}
g.setColor(Color.white);
g.drawString("点击"+target, 0,20);//写入验证码第一行文字 “点击..”
request.getSession().setAttribute("gap",x+":"+y);//将坐标放入session
//5.释放资源
g.dispose();
//6.利用ImageIO进行输出
ImageIO.write(image, "jpg", response.getOutputStream()); //将图片输出

}
//网上找的,随机产生汉字的方法
private String getRandomChineseChar()
{
String str = null;
int hs, ls;
Random random = new Random();
hs = (176 + Math.abs(random.nextInt(39)));
ls = (161 + Math.abs(random.nextInt(93)));
byte[] b = new byte[2];
b[0] = (new Integer(hs).byteValue());
b[1] = (new Integer(ls).byteValue());
try
{
str = new String(b, "GBk"); //转成中文
}
catch (UnsupportedEncodingException ex)
{
ex.printStackTrace();
}
return str;
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}

}

Login源码:用于登陆正确与否确认

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

/**
* Servlet implementation class Login
*/
@WebServlet("/Login")
public class Login extends HttpServlet {
private static final long serialVersionUID = 1L;


protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// TODO Auto-generated method stub
response.setContentType("text/html;charset=utf-8"); //设置编码
//获取前端传来的坐标

String xs=request.getParameter("x");
String ys=request.getParameter("y");
HttpSession session = request.getSession();

String str = (String) session.getAttribute("gap");//获取session中的gap
if(str==null){
response.getWriter().write("验证码超时");
return;
}
String[] split2 = str.split(":");
int x= Integer.parseInt(xs);
int y=Integer.parseInt(ys);
int x1= Integer.parseInt(split2[0]);
int y1=Integer.parseInt(split2[1]);
if(x1-2<x && x<x1+22 && y1-22<y && y<y1+2){ //若前端上传的坐标在session中记录的坐标的一定范围内则验证成功
response.getWriter().write("验证成功");
}else{
response.getWriter().write("验证失败");
}
}

/**
* @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
*/
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// TODO Auto-generated method stub
doGet(request, response);
}

}

web.xml:servlet配置参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" id="WebApp_ID" version="3.1">
<display-name>CheckCode</display-name>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
<welcome-file>index.jsp</welcome-file>
<welcome-file>default.html</welcome-file>
<welcome-file>default.htm</welcome-file>
<welcome-file>default.jsp</welcome-file>
</welcome-file-list>

<servlet>
<servlet-name>captcha</servlet-name>
<servlet-class>JcaptchaServlet</servlet-class>
</servlet>
<servlet>
<servlet-name>Login</servlet-name>
<servlet-class>Login</servlet-class>
</servlet>

<servlet-mapping>
<servlet-name>Login</servlet-name>
<url-pattern>*.shtml</url-pattern>
</servlet-mapping>

<servlet-mapping>
<servlet-name>captcha</servlet-name>
<url-pattern>/captcha.svl</url-pattern>
</servlet-mapping>
</web-app>

index.jsp:前端界面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
<html lang = "zh-CN">
<head>
<meta charset="UTF-8">
<script type="text/javascript" src="{pageContext.request.contextPath}/js/jquery.min.js"> </span>(function(){
<span class="katex math inline">("#image").click(function(event){ var obj=this; var x=event.offsetX;//获取点击时鼠标相对图片坐标 var y=event.offsetY; </span>.ajax({
url:"login.shtml", //ajax提交
type:"post",
data:{'x':x,"y":y},
success:function(data){

alert(data)
obj.src=obj.src+"?date="+new Date();
}
})
});
})
&lt;/script&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;img id="image" src="${pageContext.request.contextPath}/captcha.svl" style="cursor: pointer;" &gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<ul>
<li>注意:需要另行配置依赖(位置见结构图)<br />
jar包:<code>javax.servlet-api-4.0.0.jar</code><br />
jQuery包:<code>jquery.min.js</code>:<code>v1.11.3</code></li>
</ul>
</div>

<div class="section section-blog-info">
<div class="row">
<div class="col-md-6">
<div class="entry-categories">分类: <span class="label label-primary"><a href="http://liuchang.men/category/uncategorized/">未分类</a></span> </div>
</div>

<div class="col-md-6">
<div class="entry-social">
<a target="_blank" rel="tooltip"
data-original-title="分享到 Facebook"
class="btn btn-just-icon btn-round btn-facebook"
href="https://www.facebook.com/sharer.php?u=http://liuchang.men/2018/05/12/linux%e4%b8%8b%e7%94%a8idea%e8%b0%83%e8%af%95java-web%ef%bc%9a%e7%82%b9%e5%87%bb%e5%9b%be%e7%89%87%e6%96%87%e5%ad%97%e9%aa%8c%e8%af%81%e7%a0%81/">
<i class="fa fa-facebook"></i>
</a>

<a target="_blank" rel="tooltip"
data-original-title="分享至微博"
class="btn btn-just-icon btn-round btn-twitter"
href="http://twitter.com/share?url=http://liuchang.men/2018/05/12/linux%e4%b8%8b%e7%94%a8idea%e8%b0%83%e8%af%95java-web%ef%bc%9a%e7%82%b9%e5%87%bb%e5%9b%be%e7%89%87%e6%96%87%e5%ad%97%e9%aa%8c%e8%af%81%e7%a0%81/&#038;text=linux%E4%B8%8B%E7%94%A8idea%E8%B0%83%E8%AF%95java%20web%EF%BC%9A%E7%82%B9%E5%87%BB%E5%9B%BE%E7%89%87%E6%96%87%E5%AD%97%E9%AA%8C%E8%AF%81%E7%A0%81">
<i class="fa fa-twitter"></i>
</a>

<a rel="tooltip"
data-original-title=" Share on Email"
class="btn btn-just-icon btn-round"
href="mailto:?subject=linux下用idea调试java%20web:点击图片文字验证码&#038;body=http://liuchang.men/2018/05/12/linux%e4%b8%8b%e7%94%a8idea%e8%b0%83%e8%af%95java-web%ef%bc%9a%e7%82%b9%e5%87%bb%e5%9b%be%e7%89%87%e6%96%87%e5%ad%97%e9%aa%8c%e8%af%81%e7%a0%81/">
<i class="fa fa-envelope"></i>
</a>
</div>
</div> </div>
<hr>

<div id="comments" class="section section-comments">
<div class="row">
<div class="col-md-12">
<div class="media-area">
<h3 class="hestia-title text-center">
</h3>
</div>
<div class="media-body">
<div id="respond" class="comment-respond">
<h3 class="hestia-title text-center">发表评论 <small><a rel="nofollow" id="cancel-comment-reply-link" href="/2018/05/12/linux%e4%b8%8b%e7%94%a8idea%e8%b0%83%e8%af%95java-web%ef%bc%9a%e7%82%b9%e5%87%bb%e5%9b%be%e7%89%87%e6%96%87%e5%ad%97%e9%aa%8c%e8%af%81%e7%a0%81/#respond" style="display:none;">取消回复</a></small></h3> <span class="pull-left author"> <div class="avatar"><img alt='' src='http://1.gravatar.com/avatar/783dfac778f4f63a2889a4d32384232c?s=64&#038;d=mm&#038;r=g' srcset='http://1.gravatar.com/avatar/783dfac778f4f63a2889a4d32384232c?s=128&#038;d=mm&#038;r=g 2x' class='avatar avatar-64 photo' height='64' width='64' /></div> </span> <form action="http://liuchang.men/wp-comments-post.php" method="post" id="commentform" class="form media-body">
<p class="logged-in-as"><a href="http://liuchang.men/wp-admin/profile.php" aria-label="已登入为admin。编辑您的个人资料。">已登入为admin</a>。<a href="http://liuchang.men/wp-login.php?action=logout&amp;redirect_to=http%3A%2F%2Fliuchang.men%2F2018%2F05%2F12%2Flinux%25e4%25b8%258b%25e7%2594%25a8idea%25e8%25b0%2583%25e8%25af%2595java-web%25ef%25bc%259a%25e7%2582%25b9%25e5%2587%25bb%25e5%259b%25be%25e7%2589%2587%25e6%2596%2587%25e5%25ad%2597%25e9%25aa%258c%25e8%25af%2581%25e7%25a0%2581%2F&amp;_wpnonce=92457eb6ae">登出?</a></p><div class="form-group label-floating is-empty"> <label class="control-label">在想些什么?</label><textarea id="comment" name="comment" class="form-control" rows="6" aria-required="true"></textarea><span class="hestia-input"></span> </div><p class="form-submit"><input name="submit" type="submit" id="submit" class="btn btn-primary pull-right" value="发表评论" /> <input type='hidden' name='comment_post_ID' value='625' id='comment_post_ID' />
<input type='hidden' name='comment_parent' id='comment_parent' value='0' />
</p><input type="hidden" id="_wp_unfiltered_html_comment_disabled" name="_wp_unfiltered_html_comment_disabled" value="7d727c2e29" /><script>(function(){if(window===window.parent){document.getElementById('_wp_unfiltered_html_comment_disabled').name='_wp_unfiltered_html_comment';}})();


0%