Servlet實現(xiàn)反向代理實踐總結(jié)

一促绵、反向代理

反向代理顧名思義便贵,是和正向代理相反九府,所以我們可以借助于正向代理來理解反向代理。

正向代理:多個客戶端(Client)通過一個代理服務(wù)器(Proxy)上網(wǎng)父阻,對網(wǎng)絡(luò)上的一臺服務(wù)器(Server)進行訪問愈涩,此時一個Proxy可以對多個Client提供服務(wù)。和我們平常掛代理上網(wǎng)一樣加矛,Proxy可以隱藏Client的信息履婉,以及Proxy可以將Client與本不可以訪問的服務(wù)器鏈接(fq)等。

反向代理:在Server的入口前布置代理服務(wù)器斟览,使得Client訪問Server必須經(jīng)過Proxy毁腿,此時Proxy相當(dāng)于Server的正向代理,可以隱藏Server的信息苛茂,同時也可以實現(xiàn)不同網(wǎng)絡(luò)連通的功能已烤。

反向代理與正向代理(圖為轉(zhuǎn)載)[1]

對于正向代理與反向代理,網(wǎng)絡(luò)上的介紹數(shù)不勝數(shù)妓羊,我就寫出自己的理解胯究,不多bb了。

二躁绸、使用Servlet實現(xiàn)反向代理

使用到反向代理的開發(fā)任務(wù)概況為:生產(chǎn)網(wǎng)段部署了一臺zabbix服務(wù)器裕循,需要在辦公網(wǎng)段訪問。同時通過統(tǒng)一辦公平臺對訪問權(quán)限進行認(rèn)證净刮,如果有權(quán)訪問剥哑,則直接使用辦公平臺賬號擁有的權(quán)限登錄;否則403淹父。反向代理Servlet布置在辦公平臺下星持。

實現(xiàn)思路(步驟):代理功能 -> 自動登錄 -> 權(quán)限控制。

1.代理功能

代理功能的實現(xiàn)主要是通過Servlet做請求轉(zhuǎn)發(fā)弹灭。首先督暂,需要設(shè)置一個入口url揪垄。在訪問該url時,通過反向代理Servlet對請求進行處理(應(yīng)該是常說的過濾器)逻翁。


反向代理圖示

web.xml中的配置如下:

//web.xml

<servlet>
        <servlet-name>zabbixProxy</servlet-name>
        <servlet-class>xxx</servlet-class>    //本地路徑
</servlet>
<servlet-mapping>
        <servlet-name>zabbixProxy</servlet-name>
        <url-pattern>/zabbix1/*</url-pattern>   //入口url
</servlet-mapping>

代理功能使用okhttp3來實現(xiàn)[2]饥努,可能用到的jar包如下列所示。代理功能的實質(zhì)就是請求與響應(yīng)的轉(zhuǎn)發(fā)八回,其中請求的轉(zhuǎn)發(fā)包括請求頭的轉(zhuǎn)發(fā)酷愧,響應(yīng)的轉(zhuǎn)發(fā)包括響應(yīng)頭和響應(yīng)實體的轉(zhuǎn)發(fā)。這部分實現(xiàn)起來很簡單缠诅,通過okhttpClient創(chuàng)建請求/響應(yīng)即可溶浴,代碼如下:

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();  //創(chuàng)建代理客戶端

//從servletRequest中取得瀏覽器發(fā)出的請求信息并構(gòu)造proxyRequest,
//然后將proxyResponse作相應(yīng)處理寫入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();   //創(chuàng)建表單

Request proxyRequest = new Request.builder()
             .url(xxx)
             .post(body)
             .build()

try{
        Response proxyResponse = proxyClient.newCall(proxyRequest).execute();  //創(chuàng)建代理請求,取得代理響應(yīng)

{
        copyResponseHeaders();
        copyResponseEntity();
        ...  
}  //將代理響應(yīng)中的信息寫入servletResponse中褥伴,返回給瀏覽器
}catch (Exception e){
        ...
}finally {
try{
        proxyResponse.body().close();
        proxyResponse.close();   //response need to be closed
}catch(Exception e){...}
}
}
}

有兩個需要注意的點:
1.在對請求頭/響應(yīng)頭進行轉(zhuǎn)發(fā)的時候谅将,要特別注意cookie的轉(zhuǎn)發(fā),包括cookie的maxage重慢、path等屬性饥臂,要注意謹(jǐn)慎設(shè)置,否則cookie不匹配可能導(dǎo)致沒有權(quán)限訪問等問題似踱。
2.在代理的過程中隅熙,url可能會與原直接訪問源站不同,所以應(yīng)該根據(jù)需要核芽,在響應(yīng)實體的轉(zhuǎn)發(fā)中猛们,對頁面中的url進行改寫,否則可能出現(xiàn)404等問題狞洋。

2.自動登錄

眾所周知登錄功能最普遍的做法就是弯淘,用戶輸入正確的用戶名和密碼點擊登錄,瀏覽器發(fā)出登錄的請求吉懊,若參數(shù)都正確庐橙,服務(wù)器會set一個cookie傳給瀏覽器,在cookie規(guī)定的時限內(nèi)用戶可以保持登錄狀態(tài)借嗽。如果要實現(xiàn)自動登錄态鳖,那就要在copyRequestHeader時將登錄后的cookie傳給服務(wù)器。使用服務(wù)器set的cookie即可直接自動登錄恶导。代碼如下:

copyRequestHeaders(HttpServletRequest servletRequest, Request proxyRequerst, String haderName, Request.builder builder){
   Enumeration<String> 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(.../*其它參數(shù)*/)
                       .build();

               Request request = new Request.Builder()
                       .url(loginUrl)
                       .post(body)
                       .build();


               Response response = proxyClient.newCall(request).execute();
               loginedCookie = response.header("Set-Cookie");  //登錄后服務(wù)器set的cookie
      
           } catch (IOException e) {
               e.printStackTrace();
           }
           return loginCookie;
       }
3.權(quán)限控制

關(guān)于權(quán)限控制浆竭,需要注意的有以下幾點:

  • zabbix的登錄權(quán)限根據(jù)辦公平臺的登錄賬號分配,如何判斷登錄的賬號以及擁有的權(quán)限;
  • 在同一個客戶端上邦泄,當(dāng)一個用戶登出后另一個用戶登入删窒,如何處理;
  • 不同客戶端同時登錄是否有影響顺囊。

解決:
1.zabbix反向代理是部署在辦公平臺的大系統(tǒng)下肌索,在訪問的時候servletRequest色session中有已登錄用戶的角色信息,可以判斷用戶擁有的角色特碳,同時可以直接將登錄zabbix的用戶名和密碼寫入用戶的角色信息中诚亚,直接取用。

List<String> listRole = (List<String>) servletRequest.getSession().getAttribute("xxx");
//xxx是項目中已寫入用戶角色信息的數(shù)據(jù)午乓,可以從session中取到站宗,xxx可以從項目的其他模塊中給用戶分配

2.每次請求都需要判斷是否用戶是否切換。如果用戶已經(jīng)切換益愈,則作登出操作梢灭。然后如果新用戶沒有登錄權(quán)限,返回403腕唧;如果有權(quán)限則使用新用戶的賬號登錄或辖。那么怎樣判斷用戶是否已經(jīng)切換呢瘾英?我使用的方法是將用戶信息寫入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<String> headers = servletRequest.getHeaders(headerName);
   While (headers.hasMoreElements()){
   String headerValue = headers.nextElement();
       if  (headerName.equals("Cookie")){
  
        String role_in_session = getRole(listRole);  //listRole是一個String數(shù)組,此處省略取值過程
        String role_in_cookie = getRole(servletRequest.getCookies);  //cookie數(shù)組 同上


         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(.../*登出參數(shù)*/)
                       .build();

               Request request = new Request.Builder()
                       .url(llogoutUrl)
                       .post(body)
                       .build();


               Response response = proxyClient.newCall(request).execute();
               logoutCookie = response.header("Set-Cookie");  //登出后服務(wù)器set的cookie
      
           } catch (IOException e) {
               e.printStackTrace();
           }
           return logoutCookie;
       }
//響應(yīng)回傳給瀏覽器時要將我們自己添加的cookie設(shè)置過期
protected void copyResponseHeaders(...){
if (flag = logout){
    Cookie cookie = new cookie("Role", xxx)  //xxx為角色信息
              cookie.setPath(...)
              cookie.setMaxAge(0)   //設(shè)置cookie過期
              servletRequest.addCookie(cookie);
    }
}

當(dāng)辦公平臺的用戶登出阳啥,或者切換時添谊,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<Cookie> cookies) {
    }

    @Override public List<Cookie> loadForRequest(HttpUrl url) {
      return Collections.emptyList();
    }
  };

  /**
   * Saves {@code cookies} from an HTTP response to this store according to this jar's policy.
   *
   * <p>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<Cookie> 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.
   *
   * <p>Simple implementations will return the accepted cookies that have not yet expired and that
   * {@linkplain Cookie#matches match} {@code url}.
   */
  List<Cookie> loadForRequest(HttpUrl url);
}

對于CookieJar秕岛,需要重寫它兩個方法的代碼,在實現(xiàn)對網(wǎng)站的反向代理時,如果需要后臺保存cookie继薛,我們使用的方法是在創(chuàng)建okhttpClient時在CookieJar中定義一個hashmap變量cookieStore修壕,用來存儲cookie。代碼如下:

    protected OkHttpClient createHttpClient() {
        OkHttpClient client = new OkHttpClient.Builder()
                ...
                .cookieJar(new CookieJar() {
                    private final HashMap<String, List<okhttp3.Cookie>> cookieStore = new HashMap<>();

                    @Override
                    public void saveFromResponse(HttpUrl httpUrl, List<okhttp3.Cookie> list) {
                        if (/*登錄*/) {
                            cookieStore.put(httpUrl.host(), list);
                        }
                    }

                    @Override
                    public List<okhttp3.Cookie> loadForRequest(HttpUrl httpUrl) {
                        if (/*登出*/) {
                            cookieStore.remove(httpUrl.host());
                        }
                        List<okhttp3.Cookie> cookies = cookieStore.get(httpUrl.host());

                        return cookies != null ? cookies : new ArrayList<okhttp3.Cookie>();
                    }
                    
                }).build();
        return client;
    }

在對cookie的管理中惋增,如果用戶從瀏覽器登錄叠殷,則cookieStore會將用戶的cookie存儲起來。但是對CookieJar來說诈皿,最好的cookie存儲方法是cookie與host對應(yīng)林束。而在多用戶登錄同一臺服務(wù)器時,如果兩個用戶的權(quán)限不同稽亏,會導(dǎo)致cookieStore一直put和remove兩個用戶的cookie壶冒,可以看成兩個用戶互相頂,可能造成cookie使用混亂而導(dǎo)致登錄賬號的混亂(該問題應(yīng)該只在本項目需求中存在截歉,且目前還沒有好的解決辦法胖腾,前臺管理cookie不會有問題)。

三瘪松、nginx反向代理

如果使用nginx反向代理對多臺服務(wù)器進行反向代理[3]咸作,那么可以依靠http模塊中l(wèi)ocation模塊設(shè)置的不同上下文根來區(qū)別客戶端訪問的是哪臺服務(wù)器。但是這樣存在一個問題:

  • 無法使用同一個端口對多臺上下文根相同的服務(wù)器進行反代宵睦。

雖然有一個萬能的參數(shù):cookie记罚,但是在實際應(yīng)用中,還是會導(dǎo)致cookie的錯亂壳嚎。 要解決這個問題桐智,只能將nginx多開端口,把不同的服務(wù)器放在不同的端口上(暫時沒有想到其他更好的解決方法)烟馅。由于我司網(wǎng)絡(luò)架構(gòu)的原因说庭,沒有按照多端口開放的方式實行。其實郑趁,nginx更多用在負(fù)載均衡上刊驴,這方面網(wǎng)絡(luò)上文章也很多,不再贅述寡润。

四捆憎、總結(jié)

其實對于一個剛開始做開發(fā)的小白來說,雖然能踩的坑都踩了一遍悦穿,但是實際上servlet實現(xiàn)反向代理是一個很好的練手項目攻礼。它不像springMVC那樣枯燥,它與數(shù)據(jù)傳輸栗柒、網(wǎng)絡(luò)請求轉(zhuǎn)發(fā)礁扮、權(quán)限之類的關(guān)聯(lián)度更大一些知举。接下來的任務(wù)是告警信息的解析與轉(zhuǎn)發(fā),希望能順利完成太伊。

參考資料

[1] https://www.cnblogs.com/Anker/p/6056540.html
[2] okhttp3官方文檔
[3] nginx反向代理配置

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末雇锡,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子僚焦,更是在濱河造成了極大的恐慌锰提,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,826評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件芳悲,死亡現(xiàn)場離奇詭異立肘,居然都是意外死亡,警方通過查閱死者的電腦和手機名扛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評論 3 395
  • 文/潘曉璐 我一進店門谅年,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人肮韧,你說我怎么就攤上這事融蹂。” “怎么了弄企?”我有些...
    開封第一講書人閱讀 164,234評論 0 354
  • 文/不壞的土叔 我叫張陵超燃,是天一觀的道長。 經(jīng)常有香客問我拘领,道長意乓,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,562評論 1 293
  • 正文 為了忘掉前任院究,我火速辦了婚禮洽瞬,結(jié)果婚禮上本涕,老公的妹妹穿的比我還像新娘业汰。我一直安慰自己,他們只是感情好菩颖,可當(dāng)我...
    茶點故事閱讀 67,611評論 6 392
  • 文/花漫 我一把揭開白布样漆。 她就那樣靜靜地躺著,像睡著了一般晦闰。 火紅的嫁衣襯著肌膚如雪放祟。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,482評論 1 302
  • 那天呻右,我揣著相機與錄音跪妥,去河邊找鬼。 笑死声滥,一個胖子當(dāng)著我的面吹牛眉撵,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 40,271評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼纽疟,長吁一口氣:“原來是場噩夢啊……” “哼罐韩!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起污朽,我...
    開封第一講書人閱讀 39,166評論 0 276
  • 序言:老撾萬榮一對情侶失蹤散吵,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后蟆肆,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體矾睦,經(jīng)...
    沈念sama閱讀 45,608評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,814評論 3 336
  • 正文 我和宋清朗相戀三年炎功,在試婚紗的時候發(fā)現(xiàn)自己被綠了顷锰。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,926評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡亡问,死狀恐怖官紫,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情州藕,我是刑警寧澤束世,帶...
    沈念sama閱讀 35,644評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站床玻,受9級特大地震影響毁涉,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜锈死,卻給世界環(huán)境...
    茶點故事閱讀 41,249評論 3 329
  • 文/蒙蒙 一贫堰、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧待牵,春花似錦其屏、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至贰拿,卻和暖如春蛤袒,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背膨更。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評論 1 269
  • 我被黑心中介騙來泰國打工妙真, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人荚守。 一個月前我還...
    沈念sama閱讀 48,063評論 3 370
  • 正文 我出身青樓珍德,卻偏偏與公主長得像癌椿,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子菱阵,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,871評論 2 354

推薦閱讀更多精彩內(nèi)容