一、反向代理
反向代理顾名思义,是和正向代理相反,所以我们可以借助于正向代理来理解反向代理。
正向代理:多个客户端(Client)通过一个代理服务器(Proxy)上网,对网络上的一台服务器(Server)进行访问,此时一个Proxy可以对多个Client提供服务。和我们平常挂代理上网一样,Proxy可以隐藏Client的信息,以及Proxy可以将Client与本不可以访问的服务器链接(fq)等。
反向代理:在Server的入口前布置代理服务器,使得Client访问Server必须经过Proxy,此时Proxy相当于Server的正向代理,可以隐藏Server的信息,同时也可以实现不同网络连通的功能。
对于正向代理与反向代理,网络上的介绍数不胜数,我就写出自己的理解,不多bb了。
二、使用Servlet实现反向代理
使用到反向代理的开发任务概况为:生产网段部署了一台zabbix服务器,需要在办公网段访问。同时通过统一办公平台对访问权限进行认证,如果有权访问,则直接使用办公平台账号拥有的权限登录;否则403。反向代理Servlet布置在办公平台下。
实现思路(步骤):代理功能 -> 自动登录 -> 权限控制。
1.代理功能
代理功能的实现主要是通过Servlet做请求转发。首先,需要设置一个入口url。在访问该url时,通过反向代理Servlet对请求进行处理(应该是常说的过滤器)。
web.xml中的配置如下:
//web.xml
zabbixProxy
xxx //本地路径
zabbixProxy
/zabbix1/* //入口url
代理功能使用okhttp3来实现[2],可能用到的jar包如下列所示。代理功能的实质就是请求与响应的转发,其中请求的转发包括请求头的转发,响应的转发包括响应头和响应实体的转发。这部分实现起来很简单,通过okhttpClient创建请求/响应即可,代码如下:
import okhttp3.Callback;
import okhttp3.CookieJar;
import okhttp3.FormBody;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.internal.http.HttpDate; //jar包
public class ProxyServlet extends HttpServlet{
protected void service (HttpServletRequest servletRequest, HttpServletResponse servletResponse)
throws ServletException, IOException{
proxyClient = createHttpClient(); //创建代理客户端
//从servletRequest中取得浏览器发出的请求信息并构造proxyRequest,
//然后将proxyResponse作相应处理写入servletResponse中,即完成代理功能。
{
copyRequestHeasers();
rewriteUrlFromRequest();
...
} //从servletRequest取得请求信息
//get 请求
Request proxyRequest = new Request.builder()
.url(xxx)
.build();
//post 请求
RequestBody body = new FormBody.builder()
.add(key1, value1)
.add(key2, value2)
...
.build(); //创建表单
Request proxyRequest = new Request.builder()
.url(xxx)
.post(body)
.build()
try{
Response proxyResponse = proxyClient.newCall(proxyRequest).execute(); //创建代理请求,取得代理响应
{
copyResponseHeaders();
copyResponseEntity();
...
} //将代理响应中的信息写入servletResponse中,返回给浏览器
}catch (Exception e){
...
}finally {
try{
proxyResponse.body().close();
proxyResponse.close(); //response need to be closed
}catch(Exception e){...}
}
}
}
有两个需要注意的点:
1.在对请求头/响应头进行转发的时候,要特别注意cookie的转发,包括cookie的maxage、path等属性,要注意谨慎设置,否则cookie不匹配可能导致没有权限访问等问题。
2.在代理的过程中,url可能会与原直接访问源站不同,所以应该根据需要,在响应实体的转发中,对页面中的url进行改写,否则可能出现404等问题。
2.自动登录
众所周知登录功能最普遍的做法就是,用户输入正确的用户名和密码点击登录,浏览器发出登录的请求,若参数都正确,服务器会set一个cookie传给浏览器,在cookie规定的时限内用户可以保持登录状态。如果要实现自动登录,那就要在copyRequestHeader时将登录后的cookie传给服务器。使用服务器set的cookie即可直接自动登录。代码如下:
copyRequestHeaders(HttpServletRequest servletRequest, Request proxyRequerst, String haderName, Request.builder builder){
Enumeration headers = servletRequest.getHeaders(headerName);
While (headers.hasMoreElements()){
String headerValue = headers.nextElement();
if (headerName.equals("Cookie")){
if (/*登录条件*/) {
String loignedCookie = login(user, password, loginUrl);
headerValue = loginedCookie;
}
}
builder.addHeader(headerName, headerValue);
}
}
protected String login(String user, String password, String loginUrl){
String loginCookie = "";
try {
RequestBody body = new FormBody.Builder()
.add("name", username)
.add("password", password)
.add(.../*其它参数*/)
.build();
Request request = new Request.Builder()
.url(loginUrl)
.post(body)
.build();
Response response = proxyClient.newCall(request).execute();
loginedCookie = response.header("Set-Cookie"); //登录后服务器set的cookie
} catch (IOException e) {
e.printStackTrace();
}
return loginCookie;
}
3.权限控制
关于权限控制,需要注意的有以下几点:
- zabbix的登录权限根据办公平台的登录账号分配,如何判断登录的账号以及拥有的权限;
- 在同一个客户端上,当一个用户登出后另一个用户登入,如何处理;
- 不同客户端同时登录是否有影响。
解决:
1.zabbix反向代理是部署在办公平台的大系统下,在访问的时候servletRequest色session中有已登录用户的角色信息,可以判断用户拥有的角色,同时可以直接将登录zabbix的用户名和密码写入用户的角色信息中,直接取用。
List listRole = (List) servletRequest.getSession().getAttribute("xxx");
//xxx是项目中已写入用户角色信息的数据,可以从session中取到,xxx可以从项目的其他模块中给用户分配
2.每次请求都需要判断是否用户是否切换。如果用户已经切换,则作登出操作。然后如果新用户没有登录权限,返回403;如果有权限则使用新用户的账号登录。那么怎样判断用户是否已经切换呢?我使用的方法是将用户信息写入cookie中。在第一次登录时,将用户session中的角色set到cookie中,然后每次请求判断用户使用的cookie中的角色信息与session中是否相同。相同则说明没切换用户,不需要登出;否则做登出操作。
//登录时将用户角色加入cookie
protected String login(String user, String password, String loginUrl){
...
flag = login;
}
protected void copyResponseHeaders(...){
if (flag = login){
Cookie cookie = new cookie("Role", xxx) //xxx为角色信息
cookie.setPath(...)
cookie.setMaxAge(...)
servletRequest.addCookie(cookie);
}
}
//判断用户是否切换
copyRequestHeaders(HttpServletRequest servletRequest, Request proxyRequerst, String haderName, Request.builder builder){
Enumeration headers = servletRequest.getHeaders(headerName);
While (headers.hasMoreElements()){
String headerValue = headers.nextElement();
if (headerName.equals("Cookie")){
String role_in_session = getRole(listRole); //listRole是一个String数组,此处省略取值过程
String role_in_cookie = getRole(servletRequest.getCookies); //cookie数组 同上
if (/*登录条件*/) {
String loignedCookie = login(user, password, loginUrl);
headerValue = loginedCookie;
}
if(role_in_session == null || !cookie_in_session.equals(role_in_cookie)){
logout();
flag = logout;
}
builder.addHeader(headerName, headerValue);
}
}
protected void logout(){
String logoutCookie = "";
String logoutUrl = "xxx";
try {
RequestBody body = new FormBody.Builder()
.add(.../*登出参数*/)
.build();
Request request = new Request.Builder()
.url(llogoutUrl)
.post(body)
.build();
Response response = proxyClient.newCall(request).execute();
logoutCookie = response.header("Set-Cookie"); //登出后服务器set的cookie
} catch (IOException e) {
e.printStackTrace();
}
return logoutCookie;
}
//响应回传给浏览器时要将我们自己添加的cookie设置过期
protected void copyResponseHeaders(...){
if (flag = logout){
Cookie cookie = new cookie("Role", xxx) //xxx为角色信息
cookie.setPath(...)
cookie.setMaxAge(0) //设置cookie过期
servletRequest.addCookie(cookie);
}
}
当办公平台的用户登出,或者切换时,session中的角色信息会清除或切换,此时做登出zabbix的操作。
3.从2可以看出我是直接将cookie交给前端浏览器保存,这样在不同客户端进行登录、登出操作是没有问题的。但是之前做过一版,在okhttpClient中通过CookieJar将cookie保存在后台,这样在使用中会有问题。首先看cookiejar接口的声明:
public interface CookieJar {
/** A cookie jar that never accepts any cookies. */
CookieJar NO_COOKIES = new CookieJar() {
@Override public void saveFromResponse(HttpUrl url, List cookies) {
}
@Override public List loadForRequest(HttpUrl url) {
return Collections.emptyList();
}
};
/**
* Saves {@code cookies} from an HTTP response to this store according to this jar's policy.
*
* Note that this method may be called a second time for a single HTTP response if the response
* includes a trailer. For this obscure HTTP feature, {@code cookies} contains only the trailer's
* cookies.
*/
void saveFromResponse(HttpUrl url, List cookies);
/**
* Load cookies from the jar for an HTTP request to {@code url}. This method returns a possibly
* empty list of cookies for the network request.
*
* Simple implementations will return the accepted cookies that have not yet expired and that
* {@linkplain Cookie#matches match} {@code url}.
*/
List loadForRequest(HttpUrl url);
}
对于CookieJar,需要重写它两个方法的代码,在实现对网站的反向代理时,如果需要后台保存cookie,我们使用的方法是在创建okhttpClient时在CookieJar中定义一个hashmap变量cookieStore,用来存储cookie。代码如下:
protected OkHttpClient createHttpClient() {
OkHttpClient client = new OkHttpClient.Builder()
...
.cookieJar(new CookieJar() {
private final HashMap> cookieStore = new HashMap<>();
@Override
public void saveFromResponse(HttpUrl httpUrl, List list) {
if (/*登录*/) {
cookieStore.put(httpUrl.host(), list);
}
}
@Override
public List loadForRequest(HttpUrl httpUrl) {
if (/*登出*/) {
cookieStore.remove(httpUrl.host());
}
List cookies = cookieStore.get(httpUrl.host());
return cookies != null ? cookies : new ArrayList();
}
}).build();
return client;
}
在对cookie的管理中,如果用户从浏览器登录,则cookieStore会将用户的cookie存储起来。但是对CookieJar来说,最好的cookie存储方法是cookie与host对应。而在多用户登录同一台服务器时,如果两个用户的权限不同,会导致cookieStore一直put和remove两个用户的cookie,可以看成两个用户互相顶,可能造成cookie使用混乱而导致登录账号的混乱(该问题应该只在本项目需求中存在,且目前还没有好的解决办法,前台管理cookie不会有问题)。
三、nginx反向代理
如果使用nginx反向代理对多台服务器进行反向代理[3],那么可以依靠http模块中location模块设置的不同上下文根来区别客户端访问的是哪台服务器。但是这样存在一个问题:
- 无法使用同一个端口对多台上下文根相同的服务器进行反代。
虽然有一个万能的参数:cookie,但是在实际应用中,还是会导致cookie的错乱。 要解决这个问题,只能将nginx多开端口,把不同的服务器放在不同的端口上(暂时没有想到其他更好的解决方法)。由于我司网络架构的原因,没有按照多端口开放的方式实行。其实,nginx更多用在负载均衡上,这方面网络上文章也很多,不再赘述。
四、总结
其实对于一个刚开始做开发的小白来说,虽然能踩的坑都踩了一遍,但是实际上servlet实现反向代理是一个很好的练手项目。它不像springMVC那样枯燥,它与数据传输、网络请求转发、权限之类的关联度更大一些。接下来的任务是告警信息的解析与转发,希望能顺利完成。
参考资料
[1] https://www.cnblogs.com/Anker/p/6056540.html
[2] okhttp3官方文档
[3] nginx反向代理配置