一促绵、反向代理
反向代理顧名思義便贵,是和正向代理相反九府,所以我們可以借助于正向代理來理解反向代理。
正向代理:多個客戶端(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ò)連通的功能已烤。
對于正向代理與反向代理,網(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反向代理配置