Servlet实现反向代理实践总结

一、反向代理

反向代理顾名思义,是和正向代理相反,所以我们可以借助于正向代理来理解反向代理。

正向代理:多个客户端(Client)通过一个代理服务器(Proxy)上网,对网络上的一台服务器(Server)进行访问,此时一个Proxy可以对多个Client提供服务。和我们平常挂代理上网一样,Proxy可以隐藏Client的信息,以及Proxy可以将Client与本不可以访问的服务器链接(fq)等。

反向代理:在Server的入口前布置代理服务器,使得Client访问Server必须经过Proxy,此时Proxy相当于Server的正向代理,可以隐藏Server的信息,同时也可以实现不同网络连通的功能。

Servlet实现反向代理实践总结_第1张图片
反向代理与正向代理(图为转载)[1]

对于正向代理与反向代理,网络上的介绍数不胜数,我就写出自己的理解,不多bb了。

二、使用Servlet实现反向代理

使用到反向代理的开发任务概况为:生产网段部署了一台zabbix服务器,需要在办公网段访问。同时通过统一办公平台对访问权限进行认证,如果有权访问,则直接使用办公平台账号拥有的权限登录;否则403。反向代理Servlet布置在办公平台下。

实现思路(步骤):代理功能 -> 自动登录 -> 权限控制。

1.代理功能

代理功能的实现主要是通过Servlet做请求转发。首先,需要设置一个入口url。在访问该url时,通过反向代理Servlet对请求进行处理(应该是常说的过滤器)。


Servlet实现反向代理实践总结_第2张图片
反向代理图示

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反向代理配置

你可能感兴趣的:(Servlet实现反向代理实践总结)