者 | Lyndsey Padget
譯者 |月滿西樓
本文來自公眾號“EAWorld”,ID:eaworld
今天我們來聊聊 Java中的微服務(wù)。雖說 Java EE提供了一個強大的平臺,供我們創(chuàng)建、部署和管理企業(yè)級微服務(wù),但在本文中,我將展示如何創(chuàng)建一個盡可能小的 RESTful微服務(wù)。
放心,在這個過程中,我們不會浪費時間精力去重復(fù)做些數(shù)據(jù)處理之類的事情。我們會通過 JBoss RESTEasy來進行搭建。而確保該微服務(wù)的輕量級,目的是為了向大家展示,在一個全新或者現(xiàn)存的微服務(wù)前端,建立一個 RESTful接口,真的非常簡單。
與此同時,我會進一步證明,通過 RESTEasy構(gòu)建的微服務(wù)具備很大的靈活性,不僅可以兼容包括 JSON,XML在內(nèi)的多種數(shù)據(jù)傳輸格式,還支持將其部署到 Apache Tomcat[1]服務(wù)器而非 JBoss企業(yè)應(yīng)用平臺 (EAP)[2]上。誠然,每個工具都有自己的優(yōu)勢,但是我認為先在 KISS原則 [3]下探討技術(shù)可用性會很有幫助,然后才是根據(jù)軟件的長期目標和需求來決定應(yīng)該為服務(wù)架構(gòu)添加哪些特性。
本文中提到的代碼示例都可以在 GitHub[4]上查閱,包括“starter” 和 “final”這兩個分支。下面是我采用的環(huán)境,當然你的實際情況可能有所不同:
Java Development Kit[5] (JDK)1.8.0-131 (amd64)
apache Tomcat[6] 9
apache Maven[7] 3.5.0
Eclipse Java EE IDE[8] 4.7.0 (Oxygen)
Linux Mint[9] 18.2 (Sonya)64位
就技術(shù)而言...
微服務(wù) [10]是一種體積小、更為精煉的服務(wù),其目標是“做好一件事”。微服務(wù)之間通過一些接口進行交互是很普遍的現(xiàn)象。如果該接口可以通過 web訪問 (使用 HTTP),那么它就是一個 web服務(wù)。部分 web服務(wù)是基于 RESTful這種架構(gòu)風格的,另一些則不是。注意,微服務(wù)并不都是 web服務(wù),web服務(wù)并不都是 RESTful web服務(wù),RESTful web服務(wù)也并不都是微服務(wù)!
REST和 XML……能否共存?
如果你此前在使用 RESTful web服務(wù)時,沒用過除 JSON 以外的文本數(shù)據(jù)交換格式 [11]來進行內(nèi)容傳輸,那么你可能會認為二者是不相關(guān)的。但是回想下,REST是定義 API的一種架構(gòu)風格,REST和 JSON這兩者又碰巧一起流行起來 (注意,這并非偶然)。XML多年的發(fā)展使其擁有大量的客戶群,能夠接納和提供 XML數(shù)據(jù)傳輸格式的 RESTful web服務(wù), 不管是對那些已經(jīng)依賴于這類內(nèi)容交互系統(tǒng)的組織,還是對僅僅是更熟悉 XML的用戶來說,都非常有用。當然,通常情況下,JSON依然是首選,因為其消息體更小,但有時 XML只是一個更簡單的“sell”。擁有一個能同時支持這兩種格式的 RESTful微服務(wù)是最理想的 ;從部署的角度來說,它不僅簡潔,具備可擴展性,還有足夠的靈活性,可以支持不同類型的內(nèi)容,從而滿足那些其他有調(diào)用需求的應(yīng)用程序。
為什么選擇 RESTEasy?
RESTEasy[12]是 Jboss的一個框架,可以用來構(gòu)建 RESTful web服務(wù)。通過 RESTEasy構(gòu)建的 RESTful web服務(wù),可以根據(jù)四個函數(shù)庫來實現(xiàn)對 XML和 JSON這兩種數(shù)據(jù)傳輸格式的支持:
resteasy-jaxrs,實現(xiàn)了 JAX-RS 2.0 (用于 RESTful Web服務(wù)的 Java API) [13]
resteasy-jaxb-provider,其 JAXB[14]綁定能有效支持 XML
resteasy-jettison-provider,用 Jettison[15]將 XML轉(zhuǎn)換為 JSON
resteasy-servlet-initializer,將服務(wù)部署到 Servlet 3.0容器 (在 Tomcat服務(wù)器上)
首先,創(chuàng)建一個內(nèi)含 pom.xml數(shù)據(jù)包的 web服務(wù)項目:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.lyndseypadget</groupId> <artifactId>resteasy</artifactId> <packaging>war</packaging> <version>0.0.1-SNAPSHOT</version> <name>resteasy</name> <repositories> <repository> <id>org.jboss.resteasy</id> <url>http://repository.jboss.org/maven2/</url> </repository> </repositories> <dependencies> <dependency> <groupId>org.jboss.resteasy</groupId> <artifactId>resteasy-jaxrs</artifactId> <version>3.1.4.Final</version> </dependency> <dependency> <groupId>org.jboss.resteasy</groupId> <artifactId>resteasy-jaxb-provider</artifactId> <version>3.1.4.Final</version> </dependency> <dependency> <groupId>org.jboss.resteasy</groupId> <artifactId>resteasy-jettison-provider</artifactId> <version>3.1.4.Final</version> </dependency> <dependency> <groupId>org.jboss.resteasy</groupId> <artifactId>resteasy-servlet-initializer</artifactId> <version>3.1.4.Final</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>2.0.2</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins> <finalName>resteasy</finalName> </build> </project>
(左右滑動可查看全部代碼,下同)
這些數(shù)據(jù)庫的大小大概在 830KB。當然,這些直接的依賴環(huán)境(dependency),運用 Maven一起構(gòu)建項目也會帶來部分傳遞性依賴。
接下來,我將用“Maven方法”來構(gòu)建這個項目,例如在 src/main/java中,使用 Maven構(gòu)建命令等,不想用 Maven的話,你也可以直接從下載頁面 [16]下載 RESTEasy jar數(shù)據(jù)包。下載的時候不用理會 RESTEasy站點上彈出的這個提示:JBoss僅僅是在嘗試引導(dǎo)你采用更“企業(yè)化”的方法。你只需點擊“繼續(xù)下載”,來開展后面的操作。
項目設(shè)計
下面這個微服務(wù)可以用非常簡單的方法來演示一些基本概念。如下圖所示,它包括 5個等級。
此處,F(xiàn)ruitApplication是微服務(wù)的切入點。FruitService提供了主要的路徑 (/fruits),同時它也充當了路由器的功能。蘋果和水果都是模型;水果包含一些抽象的功能,蘋果則會具體地擴展它的功能。
和你設(shè)想一致的是,F(xiàn)ruitComparator可以提供比較功能。不熟悉 Java comparator的讀者,可以在這篇文章中了解一下對象的等同性和比較,這里我用字符來取代。雖然 FruitComparator不是一個模型,但我更喜歡將比較器與它想要比較的對象類型保持相類似的命名。
模型
讓我們從 Fruit這級開始
package com.lyndseypadget.resteasy.model; import javax.xml.bind.annotation.XmlElement; public abstract class Fruit { private String id; private String variety; @XmlElement public String getId { return id; } public void setId(String id) { this.id = id; } @XmlElement public String getVariety { return variety; } public void setVariety(String variety) { this.variety = variety; } }
然后 Apple這級對其展開:
package com.lyndseypadget.resteasy.model; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; @XmlRootElement(name = "apple") public class Apple extends Fruit { private String color; @XmlElement public String getColor { return color; } public void setColor(String color) { this.color = color; } }
以上并不是什么特別驚人的代碼,你可能會覺得都不值得拿出來炫耀,就是一個 Java繼承的簡單實例。但重點在于這兩個注釋 @XmlElement 和 @XmlRootElement,它們定義了 XML apple結(jié)構(gòu)的樣子:
<apple> <id>1</id> <variety>Golden delicious</variety> <color>yellow</color> </apple>
因為沒有約定明顯的構(gòu)造函數(shù):Java使用了隱式的、無參數(shù)的默認構(gòu)造函數(shù),所以一些更微妙的事情在發(fā)生。這個無參數(shù)的構(gòu)造函數(shù)對 JAXB 施展魔法般效果的工作是十分必要的(本文解釋了這一點,以及必要的話,如何用 XMLAdapter來讓它工作)。
現(xiàn)在我們有了一個對象:被定義的蘋果。它有三個屬性: ID、多樣性和顏色。
服務(wù)
FruitService 被用來作為與微服務(wù)交互的主要路徑 (/fruits)。在本例中,我使用 @path注釋直接在該層級中定義了第一個路徑,/fruits/apples。隨著 RESTful微服務(wù)的擴展,你可能希望在自己的層級中定義多個最終路徑 (例如 /apples, /bananas, /oranges)。
package com.lyndseypadget.resteasy; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.TreeMap; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import com.lyndseypadget.resteasy.model.Apple; import com.lyndseypadget.resteasy.model.FruitComparator; @Path("/fruits") public class FruitService { private static Map<String, Apple> apples = new TreeMap<String, Apple>; private static Comparator comparator = new FruitComparator; @GET @Path("/apples") @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON }) public List getApples { List retVal = new ArrayList(apples.values); Collections.sort(retVal, comparator); return retVal; } }
這張?zhí)O果的地圖幫助我們根據(jù) id跟蹤蘋果的數(shù)據(jù),從而模擬某些類型的數(shù)據(jù)持久層。利用 getApples方法(常用的 HTTP請求方式)將會返回地圖跟蹤到的相關(guān)蘋果數(shù)據(jù)。GET /apples route是用 @GET和 @path注釋定義的,它可以生成數(shù)據(jù)傳輸格式 XML或 JSON的內(nèi)容。
這個方法需要返回一個 List< apple >對象,然后用這個比較器按品種屬性來對列表進行排序。
FruitComparator看起來是像這樣的:
package com.lyndseypadget.resteasy.model; import java.util.Comparator; public class FruitComparator implements Comparator { public int compare(F f1, F f2) { return f1.getVariety.compareTo(f2.getVariety); } }
注意,如果想要對蘋果的一個特定屬性進行排序,比如顏色,我們就必須創(chuàng)建一個新的比較器去取代,并取個名字,比如 AppleComparator。
應(yīng)用程序
在 RESTEasy3.1.x中, 你需要定義一個擴展應(yīng)用的層級。RESTEasy示例文檔說明這是一個單例模式注冊表(singleton registry),如下所示:
package com.lyndseypadget.resteasy; import javax.ws.rs.core.Application; import java.util.HashSet; import java.util.Set; public class FruitApplication extends Application { HashSet singletons = new HashSet; public FruitApplication { singletons.add(new FruitService); } @Override public Set<Class> getClasses { HashSet<Class> set = new HashSet<Class>; return set; } @Override public Set getSingletons { return singletons; } }
如果僅為了說明本例,就不需要對這個層級做太多工作,但是我們需要在 web.xml文件中將它連接起來,這會在后面的章節(jié)“web服務(wù)連接”中進行介紹。
對象的構(gòu)建集合
GET /apples調(diào)用將返回如下數(shù)據(jù):
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <collection> <apple> <id>1</id> <variety>Golden delicious</variety> <color>yellow</color> </apple> </collection>
[ { "apple": { "id": 1, "variety": "Golden delicious", "color": "yellow" } } ]
但是,我們可以將數(shù)據(jù)更改成看起來稍有點不同:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <apples> <apple> <id>1</id> <variety>Golden delicious</variety> <color>yellow</color> </apple> </apples>
{ "apples": { "apple": { "id": 1, "variety": "Golden delicious", "color": "yellow" } } }
第二個選項在 XML中看起來更好一些,但是對 JSON產(chǎn)生了不太好的影響。如果你喜歡這個結(jié)構(gòu),可以用它自己的類型打包 List< Apple >,并修改 FruitService.getApples方法來返回這種類型:
package com.lyndseypadget.resteasy.model; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.List; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; @XmlAccessorType(XmlAccessType.FIELD) @XmlRootElement(name = "apples") public class Apples { private static Comparator comparator = new FruitComparator; @XmlElement(name = "apple", type = Apple.class) private List apples; public List getApples { Collections.sort(apples, comparator); return apples; } public void setApples(Collection apples) { this.apples = new ArrayList(apples); } }
這些注釋有效地“重新標記”了根元素,即 collection/list。通過讀取用于 javax.xml.bind.annotation的 javadoc文檔,你可以嘗試用它和不同的 XML Schema映射注釋。
當然,如果實在不能搞定一般的方法簽名(method signature),則可以編碼寫入不同的方法——一個用于 XML,另一個用于 JSON。
一些 web服務(wù)連接
從將該服務(wù)部署到 Tomcat開始,我用一個放在 src/main/webapp/web inf/web.xml的 web應(yīng)用部署描述符文件。它所包含的內(nèi)容如下:
<?xml version="1.0"?> <!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd"> <web-app> <display-name>resteasy</display-name> <context-param> <param-name>javax.ws.rs.core.Application</param-name> <param-value>com.lyndseypadget.resteasy.FruitApplication</param-value> </context-param> <context-param> <param-name>resteasy.servlet.mapping.prefix</param-name> <param-value>/v1</param-value> </context-param> <listener> <listener-class> org.jboss.resteasy.plugins.server.servlet.ResteasyBootstrap </listener-class> </listener> <servlet> <servlet-name>Resteasy</servlet-name> <servlet-class>org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher</servlet-class> </servlet> <servlet-mapping> <servlet-name>Resteasy</servlet-name> <url-pattern>/v1/*</url-pattern> </servlet-mapping> </web-app>
沒錯,“servlet-name”表示 servlet(即 Service)的名稱是 Resteasy。servlet-mapping url-pattern (/v1/*)要求 Tomcat服務(wù)器將包含該模式的傳入請求傳輸?shù)?Resteasy服務(wù)。關(guān)于如何建立這個文件的更多信息,以及可用的不同選項,請參閱 Tomcat的應(yīng)用程序部署文檔 [17]。
構(gòu)建及部署
從項目的根目錄中,可以運行以下內(nèi)容來構(gòu)建 WAR(web application resource,web應(yīng)用程序資源)文件:
mvn clean install
這將在 target文件夾中創(chuàng)建一個包含 WAR文件的新文件夾。雖然用 Maven或其他工具來部署該文件也可以,但我只用一個簡單的復(fù)制命令就可以。需要注意的是,每次將 WAR重新部署到 Tomcat服務(wù)器時,應(yīng)該首先暫停服務(wù)器運行,并刪除服務(wù)應(yīng)用程序文件夾 (在本例中,是這個文件夾:< tomcatDirectory >/webapps/resteasy)和舊的 WAR文件 (< tomcatDirectory >/webapps/resteasy.war)。
[sudo] cp target/resteasy.war <tomcatDirectory>/webapps/resteasy.war
如果此時 Tomcat服務(wù)器正在運行,那么會即刻部署 web服務(wù)。如果不是,下次服務(wù)器啟動時,該服務(wù)也會被自動部署上去。然后,就可以通過如下地址訪問 web服務(wù):http://< tomcatHost >:< tomcatPort >/resteasy/v1/fruits/apples。我的范例中是這個地址 http://localhost:8080/resteasy/v1/fruits/apples。
通過“內(nèi)容協(xié)商(Content negotiation)”測試服務(wù)
內(nèi)容協(xié)商(Content negotiation)是一種機制,它可以提供不同資源 (URI)的表現(xiàn)形式。最基本的,這意味著可以:
詳細設(shè)置 Accept header,以指示希望從服務(wù)中接受的內(nèi)容類型
詳細設(shè)置 Content-Type header,以指示發(fā)送給服務(wù)的內(nèi)容類型
要獲取更多關(guān)于內(nèi)容協(xié)商(Content negotiation)和 header的信息,請參閱 RFC 2616[18]的第 12和 14章。在本例中,你真正需要了解的是:
@Produces annotation(注釋)指明了該方法能夠生成哪些內(nèi)容 (這將嘗試匹配請求上的 Accept header)。
@Consumes annotation(注釋)指明了該方法能夠使用哪些內(nèi)容 (這將嘗試匹配請求的 content-type header)。
如果您試圖對一個有效端點進行 HTTP調(diào)用,但是內(nèi)容不能被協(xié)商,這意味著沒有 @Produces匹配該 Accept數(shù)據(jù),或者沒有 @Consumes匹配 Content-Type數(shù)據(jù),將被返回 HTTP狀態(tài)碼 415:不支持的數(shù)據(jù)傳輸格式。
返回常見數(shù)據(jù)傳輸格式的 GET調(diào)用實際上可以直接進入瀏覽器。對于 GET /apples這樣的調(diào)用,默認情況下您將獲得 XML:
不過,使用像 Postman[19]這類工具可能會更有幫助,因為它明確地指定 Accept header作為 application/xml:
這兩種方法都返回了一些有效但沒有多大意義的 XML,即一個空的蘋果列表。但是這里有一些很酷的東西。將 Accept header更改為 application/json,太好了,瞧!JSON*生效*了:
不只是“讀取”
你可能會發(fā)現(xiàn),很多 RESTful web服務(wù)的例子,都是只讀的,部分也不會有進一步的提示,比如如何去創(chuàng)建、更新和刪除這些操作。雖然我們現(xiàn)在已經(jīng)有了 web服務(wù)的框架,但這是一個不能更改的空列表,這并沒多大意義。所以我們應(yīng)該運用一些其他方法,將蘋果添加到這個列表中或從列表中將其刪除。
package com.lyndseypadget.resteasy; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.TreeMap; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import com.lyndseypadget.resteasy.model.Apple; import com.lyndseypadget.resteasy.model.FruitComparator; @Path("/fruits") public class FruitService { private static Comparator comparator = new FruitComparator; private static Map apples = new TreeMap; private static int appleCount = 0; @GET @Path("/apples") @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON }) public List getApples { List retVal = new ArrayList(apples.values); Collections.sort(retVal, comparator); return retVal; } @GET @Path("/apples/{id}") @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON }) public Response getApple(@PathParam("id") String id) { Apple found = apples.get(id); if(found == ) { return Response.status(404).build; } return Response.ok(found).build; } @DELETE @Path("/apples/{id}") public Response deleteApple(@PathParam("id") String id) { apples.remove(id); return Response.status(200).build; } @POST @Path("/apples") @Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON }) public Response createApple(Apple apple) { String newId = Integer.toString(++appleCount); apple.setId(newId); apples.put(newId, apple); return Response.status(201).header("Location", newId).build; } }
如此,就增加了一些新的功能:
通過 id檢索蘋果數(shù)據(jù) (如果在地圖中沒有找到,則返回狀態(tài)代碼 404)
通過 id刪除蘋果數(shù)據(jù)
創(chuàng)建新的蘋果數(shù)據(jù) (如果成功的話,返回狀態(tài)代碼 201)
這些方法完善了很多功能,確保了服務(wù)可以按照預(yù)期工作。更新蘋果 (使用 @PUT和 /或 @PATCH),以及更多的關(guān)于端點、邏輯和管理持久性方面的功能操作,都留給讀者你們來練習吧。
當我們再次進行構(gòu)建和部署時會發(fā)現(xiàn) (如果用 Maven或者 Tomcat來進行設(shè)置,請參閱上文“構(gòu)建和部署”),現(xiàn)在已經(jīng)可以在服務(wù)中創(chuàng)建、檢索和刪除蘋果了。而且即使不在服務(wù)器上做任何重新配置,也可以在 XML和 JSON之間進行選擇性調(diào)用。
來創(chuàng)建一個擁有“application/json”內(nèi)容類型和 JSON主體的蘋果,如下圖所示:
這是另一個例子:創(chuàng)建一個具有“application/xml”內(nèi)容類型和 XML主體的蘋果。
在 XML中檢索所有的蘋果數(shù)據(jù):
在 JSON中通過 id檢索 apple 2的數(shù)據(jù):
通過 id刪除 apple 1的數(shù)據(jù):
在 JSON中檢索所有蘋果的數(shù)據(jù):
小結(jié)
在此我們已經(jīng)探討了 RESTEasy架構(gòu)如何在 Java web服務(wù)中無縫支持 XML和 JSON數(shù)據(jù)傳輸格式。我還解釋了 REST、Media type(數(shù)據(jù)傳輸格式)、web服務(wù)和微服務(wù)之間的技術(shù)差異,因為在這些術(shù)語中有很多容易混淆的灰色地帶。
我這里列舉的例子可能有點勉強,生活中我其實從來沒有真正需要過水果相關(guān)的數(shù)據(jù),我也沒有在食品行業(yè)工作過。之所以用水果來舉例,是因為我覺得這個“規(guī)模”能有助于大家理解微服務(wù)的概念,你也可以想象其他例如蔬菜,罐頭或海鮮這樣的微服務(wù)是如何共同構(gòu)成一個食物分配系統(tǒng)。現(xiàn)實世界中,食品雜貨店的食物分配系統(tǒng)實際上非常復(fù)雜,它必須考慮到包括銷售、優(yōu)惠券、過期日期、營養(yǎng)信息等各方面的問題。
當然,你可以選擇其他方式去對系統(tǒng)進行分割,但當你需要一種快速高效、輕量級工具來支持多種數(shù)據(jù)格式時,RESTEasy真的是個非常不錯的選擇。
參考地址:
[1]、[2]、[6] https://tomcat.apache.org/
[3] https://en.wikipedia.org/wiki/KISS_principle
[4] https://github.com/lyndseypadget/resteasy-demo
[5] http://www.oracle.com/technetwork/java/javase/downloads/index.html
[7] https://maven.apache.org/
[8] https://www.eclipse.org/downloads/
[9] https://linuxmint.com/edition.php?id=237
[10] https://stackify.com/what-are-microservices/
[11] https://www.iana.org/assignments/media-types/media-types.xhtml
[12] http://resteasy.jboss.org/
[13] https://jcp.org/aboutJava/communityprocess/final/jsr339/index.html
[14] http://www.oracle.com/technetwork/articles/javase/index-140168.html
[15] https://github.com/jettison-json/jettison
[16] http://resteasy.jboss.org/downloads
[17] https://tomcat.apache.org/tomcat-9.0-doc/appdev/deployment.html
[18] https://www.ietf.org/rfc/rfc2616.txt
[19] https://www.getpostman.com/
期望得到更多優(yōu)質(zhì)技術(shù)干貨,歡迎加入 EAWorld社區(qū),與近萬名技術(shù)人一起成長。入群暗號:328
者:小不點啊
來源:www.cnblogs.com/leeSmall/p/9356535.html
一、Nginx Rewrite 規(guī)則
Rewrite規(guī)則含義就是某個URL重寫成特定的URL(類似于Redirect),從某種意義上說為了美觀或者對搜索引擎友好,提高收錄量及排名等。
語法:
rewrite <regex> <replacement> [flag]
關(guān)鍵字 || 正則 || 替代內(nèi)容 || flag標記
Rewrite規(guī)則的flag標記主要有以下幾種:
last和break用來實現(xiàn)URL重寫,瀏覽器地址欄URL地址不變
a) 例如用戶訪問www.dbspread.com,想直接跳轉(zhuǎn)到網(wǎng)站下面的某個頁面,www.dbspread.com/new.index.html如何來實現(xiàn)呢?我們可以使用Nginx Rewrite 來實現(xiàn)這個需求,具體如下:在server中加入如下語句即可:
效果圖如下:
rewrite ^/$ http://www.dbspread.com/new.index.html permanent;
對應(yīng)如下語法:
rewrite <regex> <replacement> [flag];
關(guān)鍵字 正則 替代內(nèi)容 flag標記
正則表達式說明:
*代表前面0或更多個字符 +代表前面1或更多個字符
?代表前面0或1個字符 ^代表字符串的開始位置
$代表字符串結(jié)束的位置 。為通配符,代表任何字符
b)例如多個域名跳轉(zhuǎn)到同一個域名,nginx rewrite規(guī)則寫法如下:
格式:
rewrite <regex> <replacement> [flag];
關(guān)鍵字 || 正則 || 替代內(nèi)容 || flag標記
說明:
比如http://www.dbspread.com/download/av123.rmvb 這個視頻下載地址被其他網(wǎng)站引用,比如在www.test.com的index.html引用download/av123.rmvb就叫盜鏈,我們要禁止這種引用就叫做防盜鏈
在nginx的nginx.conf的server里面配置如下代碼
三、Nginx 動靜分離
Nginx動靜分離是讓動態(tài)網(wǎng)站里的動態(tài)網(wǎng)頁根據(jù)一定規(guī)則把不變的資源和經(jīng)常變的資源區(qū)分開來,動靜資源做好了拆分以后,我們就可以根據(jù)靜態(tài)資源的特點將其做緩存操作,這就是網(wǎng)站靜態(tài)化處理的核心思路。
1). WEB項目開發(fā)時要注意,將靜態(tài)資源盡量放在一個static文件夾2). 將static靜態(tài)資源文件夾放到Nginx可以取到的位置3). 頁面要建立全局變量路徑,方便修改路徑4). 修改nginx.conf的location, 匹配靜態(tài)資源請求
body {
margin: 10px 20px;
text-align: center;
font-family: Arial, sans-serif;
background-color: red;
}
4.4 在nginx的nginx.conf中server節(jié)點新增靜態(tài)資源分離的配置
對于Nginx基礎(chǔ)配置,推薦之前的:后端實踐:Nginx日志配置(超詳細)
4.5 訪問頁面查看效果
Keepalived軟件起初是專為LVS負載均衡軟件設(shè)計的,用來管理并監(jiān)控LVS集群系統(tǒng)中各個服務(wù)節(jié)點的狀態(tài),后來又加入了可以實現(xiàn)高可用的VRRP (Virtual Router Redundancy Protocol ,虛擬路由器冗余協(xié)議)功能。因此,Keepalived除了能夠管理LVS軟件外,還可以作為其他服務(wù)(例如:Nginx、Haproxy、MySQL等)的高可用解決方案軟件
管理LVS負載均衡軟件實現(xiàn)LVS集群節(jié)點的健康檢查作為系統(tǒng)網(wǎng)絡(luò)服務(wù)的高可用性(failover)
Keepalived高可用服務(wù)之間的故障切換轉(zhuǎn)移,是通過 VRRP 來實現(xiàn)的。在 Keepalived服務(wù)正常工作時,主 Master節(jié)點會不斷地向備節(jié)點發(fā)送(多播的方式)心跳消息,用以告訴備Backup節(jié)點自己還活著,當主 Master節(jié)點發(fā)生故障時,就無法發(fā)送心跳消息,備節(jié)點也就因此無法繼續(xù)檢測到來自主 Master節(jié)點的心跳了,于是調(diào)用自身的接管程序,接管主Master節(jié)點的 IP資源及服務(wù)。而當主 Master節(jié)點恢復(fù)時,備Backup節(jié)點又會釋放主節(jié)點故障時自身接管的IP資源及服務(wù),恢復(fù)到原來的備用角色。
說明:keepalived的主從切換和redis的主從切換是不一樣的,keepalived的主節(jié)點掛了以后,從節(jié)點變?yōu)橹鞴?jié)點,之前的主節(jié)點恢復(fù)以后繼續(xù)做主節(jié)點。redis的主節(jié)點掛了以后,重新恢復(fù)以后變?yōu)閺墓?jié)點
說明:
虛擬ip(VIP):192.168.152.200,對外提供服務(wù)的ip,也可稱作浮動ip192.168.152.130:nginx + keepalived master 主192.168.152.129:nginx + keepalived backup 從192.168.152.129:tomcat-8080192.168.152.129:tomcat-8081
環(huán)境準備:
centos6、jdk
虛擬ip(VIP):192.168.152.200,對外提供服務(wù)的ip,也可稱作浮動ip
192.168.152.130:nginx + keepalived master 主
192.168.152.129:nginx + keepalived backup 從
192.168.152.129:tomcat-8080
192.168.152.129:tomcat-8081
nginx和tomcat的環(huán)境準備請查看我的前一篇關(guān)于nginx的文章
注:192.168.152.129(keepalived從節(jié)點) 與 192.168.152.130(keepalived主節(jié)點)先安裝好nginx + keepalived
下載壓縮包:
wget www.keepalived.org/software/keepalived-1.3.5.tar.gz
解壓縮:
tar -zxvf keepalived-1.3.5.tar.gz
進入解壓縮以后的文件目錄:
cd keepalived-1.3.5
編譯安裝:./configure --prefix=/usr/local/keepalived系統(tǒng)提示警告 *** WARNING - this build will not support IPVS with IPv6. Please install libnl/libnl-3 dev libraries to support IPv6 with IPVS.yum -y install libnl libnl-devel再次執(zhí)行./configure --prefix=/usr/local/keepalived系統(tǒng)提示錯誤 configure: error: libnfnetlink headers missingyum install -y libnfnetlink-devel再次執(zhí)行./configure --prefix=/usr/local/keepalived
make && make install
到此keepalived安裝完成,但是接下來還有最關(guān)鍵的一步,如果這一步?jīng)]有做后面啟動keepalived的時候會報找不到配置文件的錯誤
Configuration file '/etc/keepalived/keepalived.conf' is not a regular non-executable file
安裝完成后,進入安裝目錄的etc目錄下,將keepalived相應(yīng)的配置文件拷貝到系統(tǒng)相應(yīng)的目錄當中。keepalived啟動時會從/etc/keepalived目錄下查找keepalived.conf配置文件
mkdir /etc/keepalived
cp /usr/local/keepalived/etc/keepalived/keepalived.conf /etc/keepalived
5.3 修改keepalived從節(jié)點192.168.152.129的/etc/keepalived/keepalived.conf配置文件
5.4 檢查nginx是否啟動的shell腳本
/usr/local/src/check_nginx_pid.sh
#!/bin/bash
#檢測nginx是否啟動了
A=`ps -C nginx --no-header |wc -l`
if [ $A -eq 0 ];then #如果nginx沒有啟動就啟動nginx
/usr/local/nginx/sbin/nginx #重啟nginx
if [ `ps -C nginx --no-header |wc -l` -eq 0 ];then #nginx重啟失敗,則停掉keepalived服務(wù),進行VIP轉(zhuǎn)移
killall keepalived
fi
fi
5.5 192.168.152.130(keepalived主節(jié)點)和 192.168.152.129(keepalived從節(jié)點)的nginx的配置文件nginx.conf
user root root; #使用什么用戶啟動NGINX 在運行時使用哪個用戶哪個組
worker_processes 4; #啟動進程數(shù),一般是1或8個,根據(jù)你的電腦CPU數(shù),一般8個
worker_cpu_affinity 00000001 00000010 00000100 00001000; #CPU邏輯數(shù)——把每個進程分別綁在CPU上面,為每個進程分配一個CPU
#pid /usr/local/nginx/logs/nginx.pid
worker_rlimit_nofile 102400; #一個進程打開的最大文件數(shù)目,與NGINX并發(fā)連接有關(guān)系
#工作模式及連接數(shù)上限
events
{
use epoll; #多路復(fù)用IO 基于LINUX2.6以上內(nèi)核,可以大大提高NGINX的性能 uname -a查看內(nèi)核版本號
worker_connections 102400; #單個worker process最大連接數(shù),其中NGINX最大連接數(shù)=連接數(shù)*進程數(shù),一般1GB內(nèi)存的機器上可以打開的最大數(shù)大約是10萬左右
multi_accept on; #盡可能多的接受請求,默認是關(guān)閉狀態(tài)
}
#處理http請求的一個應(yīng)用配置段
http
{
#引用mime.types,這個類型定義了很多,當web服務(wù)器收到靜態(tài)的資源文件請求時,依據(jù)請求文件的后綴名在服務(wù)器的MIME配置文件中找到對應(yīng)的MIME #Type,根據(jù)MIMETYPE設(shè)置并response響應(yīng)類型(Content-type)
include mime.types;
default_type application/octet-stream; #定義的數(shù)據(jù)流,有的時候默認類型可以指定為text,這跟我們的網(wǎng)頁發(fā)布還是資源下載是有關(guān)系的
fastcgi_intercept_errors on; #表示接收fastcgi輸出的http 1.0 response code
charset utf-8;
server_names_hash_bucket_size 128; #保存服務(wù)器名字的hash表
#用來緩存請求頭信息的,容量4K,如果header頭信息請求超過了,nginx會直接返回400錯誤,先根據(jù)client_header_buffer_size配置的值分配一個buffer,如果##分配的buffer無法容納request_line/request_header,那么就會##再次根據(jù)large_client_header_buffers配置的參數(shù)分配large_buffer,如果large_buffer還是無#法容納,那么就會返回414(處理request_line)/400(處理request_header)錯誤。
client_header_buffer_size 4k;
large_client_header_buffers 4 32k;
client_max_body_size 300m; #允許客戶端請求的最大單文件字節(jié)數(shù) 上傳文件時根據(jù)需求設(shè)置這個參數(shù)
#指定NGINX是否調(diào)用這個函數(shù)來輸出文件,對于普通的文件我們必須設(shè)置為ON,如果NGINX專門做為一個下載端的話可以關(guān)掉,好處是降低磁盤與網(wǎng)絡(luò)的IO處理數(shù)及#系統(tǒng)的UPTIME
sendfile on;
#autoindex on;開啟目錄列表訪問,適合下載服務(wù)器
tcp_nopush on; #防止網(wǎng)絡(luò)阻塞
#非常重要,根據(jù)實際情況設(shè)置值,超時時間,客戶端到服務(wù)端的連接持續(xù)有效時間,60秒內(nèi)可避免重新建立連接,時間也不能設(shè)太長,太長的話,若請求數(shù)10000##,都占用連接會把服務(wù)托死
keepalive_timeout 60;
tcp_nodelay on; #提高數(shù)據(jù)的實時響應(yīng)性
client_body_buffer_size 512k; #緩沖區(qū)代理緩沖用戶端請求的最大字節(jié)數(shù)(請求多)
proxy_connect_timeout 5; #nginx跟后端服務(wù)器連接超時時間(代理連接超時)
proxy_read_timeout 60; #連接成功后,后端服務(wù)器響應(yīng)時間(代理接收超時)
proxy_send_timeout 5; #后端服務(wù)器數(shù)據(jù)回傳時間(代理發(fā)送超時)
proxy_buffer_size 16k; #設(shè)置代理服務(wù)器(nginx)保存用戶頭信息的緩沖區(qū)大小
proxy_buffers 4 64k; #proxy_buffers緩沖區(qū),網(wǎng)頁平均在32k以下的話,這樣設(shè)置
proxy_busy_buffers_size 128k; #高負荷下緩沖大小
proxy_temp_file_write_size 128k; #設(shè)定緩存文件夾大小,大于這個值,將從upstream服務(wù)器傳
gzip on; #NGINX可以壓縮靜態(tài)資源,比如我的靜態(tài)資源有10M,壓縮后只有2M,那么瀏覽器下載的就少了
gzip_min_length 1k;
gzip_buffers 4 16k;
gzip_http_version 1.1;
gzip_comp_level 2; #壓縮級別大小,最小1,最大9.值越小,壓縮后比例越小,CPU處理更快,為1時,原10M壓縮完后8M,但設(shè)為9時,壓縮完可能只有2M了。一般設(shè)置為2
gzip_types text/plain application/x-javascript text/css application/xml; #壓縮類型:text,js css xml 都會被壓縮
gzip_vary on; #作用是在http響應(yīng)中增加一行目的是改變反向代理服務(wù)器的緩存策略
#日志格式
log_format main '$remote_addr - $remote_user [$time_local] "$request" ' #ip 遠程用戶 當?shù)貢r間 請求URL
'$status $body_bytes_sent "$http_referer" ' #狀態(tài) 發(fā)送的大小 響應(yīng)的頭
'"$http_user_agent" $request_time'; #客戶端使用的瀏覽器 頁面響應(yīng)的時間
#動態(tài)轉(zhuǎn)發(fā)
upstream web1 {
#每個請求按訪問ip的hash結(jié)果分配,這樣每個訪客固定訪問一個后端服務(wù)器,可以解決session的問題。配置了ip_hash就沒有負載均衡的效果了,每次訪問的都是同一個tomcat
#ip_hash;
#轉(zhuǎn)發(fā)的后端的tomcat服務(wù)器,weight表示轉(zhuǎn)發(fā)的權(quán)重,越大轉(zhuǎn)發(fā)的次數(shù)越多,機器性能不一樣配置的weight值不一樣
server 192.168.152.129:8080 weight=1 max_fails=2 fail_timeout=30s;
server 192.168.152.129:8081 weight=1 max_fails=2 fail_timeout=30s;
}
upstream web2 {
server 192.168.152.129:8090 weight=1 max_fails=2 fail_timeout=30s;
server 192.168.152.129:8091 weight=1 max_fails=2 fail_timeout=30s;
}
server {
listen 80; #監(jiān)聽80端口
server_name www.dbspread.com; #域名
#rewrite規(guī)則
index index.jsp index.html index.htm;
root /usr/local/nginx/html; #定義服務(wù)器的默認網(wǎng)站根目錄位置
#重定向
if ($host != 'www.dbspread.com' ){
rewrite ^/(.*)$ http://www.dbspread.com/$1 permanent;
}
#防盜鏈
location ~* \.(rmvb|jpg|png|swf|flv)$ { #rmvb|jpg|png|swf|flv表示對rmvb|jpg|png|swf|flv后綴的文件實行防盜鏈
valid_referers none blocked www.dbspread.com; #表示對www.dbspread.com此域名開通白名單,比如在www.test.com的index.html引用download/av123.rmvb,無效
root html/b;
if ($invalid_referer) { #如果請求不是從www.dbspread.com白名單發(fā)出來的請求,直接重定向到403.html這個頁面或者返回403
#rewrite ^/ http://www.dbspread.com/403.html;
return 403;
}
}
#監(jiān)聽完成以后通過斜桿(/)攔截請求轉(zhuǎn)發(fā)到后端的tomcat服務(wù)器
location /
{
#如果后端的服務(wù)器返回502、504、執(zhí)行超時等錯誤,自動將請求轉(zhuǎn)發(fā)到upstream負載均衡池中的另一臺服務(wù)器,實現(xiàn)故障轉(zhuǎn)移。
proxy_next_upstream http_502 http_504 error timeout invalid_header;
proxy_set_header Host $host; #獲取客戶端的主機名存到變量Host里面,從而讓tomcat取到客戶端機器的信息
proxy_set_header X-Real-IP $remote_addr; #獲取客戶端的主機名存到變量X-Real-IP里面,從而讓tomcat取到客戶端機器的信息
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
#rewrite ^/$ http://www.dbspread.com/new.index.html permanent;#用戶訪問www.dbspread.com,想直接跳轉(zhuǎn)到網(wǎng)站下面的某個頁面:www.dbspread.com/new.index.html
proxy_pass http://web1; #跳轉(zhuǎn)到對應(yīng)的應(yīng)用web1
}
# location ~ .*\.(php|jsp|cgi|shtml)?$ #動態(tài)分離 ~匹配 以.*結(jié)尾(以PHP JSP結(jié)尾走這段)
# {
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_pass http://jvm_web2;
# }
#靜態(tài)分離 ~匹配 以.*結(jié)尾(以html|htm|gif|jpg|jpeg|bmp|png|ico|txt|js|css結(jié)尾走這段),當然不是越久越好,如果有10000個用戶在線,都保存幾個月,系統(tǒng)托跨
location ~ .*\.(html|htm|gif|jpg|jpeg|bmp|png|ico|txt|js|css)$
{
root /var/local/static; #靜態(tài)資源存放在nginx的安裝機器上
#proxy_pass http://www.static.com; #靜態(tài)資源也可存放在遠程服務(wù)器上
expires 30d;
}
#日志級別有[debug|info|notice|warn|error|crit] error_log 級別分為 debug, info, notice, warn, error, crit 默認為crit, 生產(chǎn)環(huán)境用error
#crit 記錄的日志最少,而debug記錄的日志最多
access_log /usr/local/logs/web2/access.log main;
error_log /usr/local/logs/web2/error.log crit;
}
}
到這一步環(huán)境準備已完成,相關(guān)的配置也修改完成,下面我們來查看效果
5.6 配置hosts域名映射
192.168.152.200 www.dbspread.com
注意:這里192.168.152.200 是keepalived里面virtual_ipaddress配置的虛擬ip
virtual_ipaddress {
192.168.152.200 # 定義虛擬ip(VIP),可多設(shè),每行一個
}
到這一步環(huán)境準備已完成,相關(guān)的配置也修改完成,下面我們來查看效果
5.7 分別啟動192.168.152.129的兩個tomcat
5.8 分別啟動192.168.152.130(keepalived主節(jié)點)和
192.168.152.129(keepalived從節(jié)點)的keepalived的
啟動命令:
/usr/local/keepalived/sbin/keepalived
可以看到keepalived和nginx都啟動了
在瀏覽器輸入www.dpspread.com域名訪問
可以看到從節(jié)點變?yōu)橹鞴?jié)點了
在瀏覽器輸入地址www.dpspread.com訪問,可以看到訪問正常
可以看到主節(jié)點重新啟動以后變?yōu)橹鞴?jié)點了
之前變?yōu)橹鞴?jié)點的從節(jié)點又變回從節(jié)點了
譯:h4d35
預(yù)估稿費:120RMB
投稿方式:發(fā)送郵件至linwei#360.cn,或登陸網(wǎng)頁版在線投稿
前言
本篇文章主要介紹了在一次漏洞懸賞項目中如何利用配置錯誤挖到一個認證繞過漏洞。
從JS文件中發(fā)現(xiàn)認證繞過漏洞
本文內(nèi)容源自一個私有漏洞賞金計劃。在這個漏洞計劃中,接受的漏洞范圍限于目標網(wǎng)站少數(shù)幾個公開的功能。基于前期發(fā)現(xiàn)的問題(當我被邀請進這個計劃時,其他人一共提交了5個漏洞),似乎很難再挖到新的漏洞。同時,在賞金詳情中提到了這樣一句話:
如果你成功進入管理頁面,請立即報告,請勿在/admin中進行進一步的測試。
然而,目標網(wǎng)站中存在一個僅限于未認證和未經(jīng)授權(quán)的用戶訪問的管理頁面。當我們訪問/login或/admin時會跳轉(zhuǎn)到https://bountysite.com/admin/dashboard?redirect=/。
對登錄頁面進行暴力破解也許是一個可行方案,但是我并不喜歡這種方式。看一下網(wǎng)頁源碼,沒什么有用的內(nèi)容。于是我開始查看目標網(wǎng)站的結(jié)構(gòu)。似乎目標網(wǎng)站的JS文件都放在少數(shù)幾個文件夾中,如/lib、/js、/application等。
有意思!
祭出神器BurpSuite,使用Intruder跑一下看能否在上述文件夾中找到任何可訪問的JS文件。將攻擊點設(shè)置為https://bountysite.com/admin/dashboard/js/*attack*.js。注意,不要忘記.js擴展名,這樣如果文件能夠訪問則返回200響應(yīng)。確實有意思!因為我找到了一些可訪問的JS文件,其中一個文件是/login.js。
訪問這個JS文件https://bountysite.com/admin/dashboard/js/login.js,請求被重定向至管理頁面:) 。但是,我并沒有查看該文件的權(quán)限,只能看到部分接口信息。
但是我并沒有就此止步。這看起來很奇怪,為什么我訪問一個.js文件卻被作為HTML加載了呢?經(jīng)過一番探查,終于發(fā)現(xiàn),我能夠訪問管理頁面的原因在于*login*。是的,只要在請求路徑/dashboard/后的字符串中含有*login*(除了'login',這只會使我回到登錄頁面),請求就會跳轉(zhuǎn)到這個管理接口,但是卻沒有正確的授權(quán)。
我繼續(xù)對這個受限的管理接口進行了進一步的測試。再一次查看了頁面源碼,試著搞清楚網(wǎng)站結(jié)構(gòu)。在這個管理接口中,有其他一些JS文件能夠幫助我理解管理員是如何執(zhí)行操作的。一些管理操作需要一個有效的令牌。我試著使用從一個JS文件中泄露的令牌執(zhí)行相關(guān)管理操作,然并卵。請求還是被重定向到了登錄頁面。我發(fā)現(xiàn)另外一個真實存在的路徑中也部署了一些內(nèi)容,那就是/dashboard/controllers/*.php。
再一次祭出BurpSuite,使用Intruder檢查一下是否存在可以從此處訪問的其他任何路徑。第二次Intruder的結(jié)果是,我發(fā)現(xiàn)幾乎不存在其他無需授權(quán)即可訪問的路徑。這是基于服務(wù)器返回的500或者200響應(yīng)得出的結(jié)論。
回到我在上一步偵察中了解到的網(wǎng)站結(jié)構(gòu)中,我發(fā)現(xiàn)這些路徑是在/controllers中定義的,通過/dashboard/*here*/進行訪問。但是直接訪問這些路徑會跳轉(zhuǎn)到登錄頁面,似乎網(wǎng)站對Session檢查得還挺嚴格。此時我又累又困,幾乎都打算放棄了,但是我想最后再試一把。如果我利用與訪問管理頁面相同的方法去執(zhí)行這些管理操作會怎么樣呢?很有趣,高潮來了:) 我能夠做到這一點。
通過訪問/dashboard/photography/loginx,請求跳轉(zhuǎn)到了Admin Photography頁面,并且擁有完整的權(quán)限!
從這里開始,我能夠執(zhí)行和訪問/dashboard/*路徑下的所有操作和目錄,這些地方充滿了諸如SQL注入、XSS、文件上傳、公開重定向等漏洞。但是,我沒有繼續(xù)深入測試,因為這些都不在賞金計劃之內(nèi),根據(jù)計劃要求,一旦突破管理授權(quán)限制,應(yīng)立即報告問題。此外,根據(jù)管理頁面顯示的調(diào)試錯誤信息可知,我之所以能夠訪問到管理頁面,是因為應(yīng)用程序在/dashboard/controllers/*文件中存在錯誤配置。期望達到的效果是:只要請求鏈接中出現(xiàn)*login*,就重定向至主登錄頁面,然而,實際情況并不如人所愿。
后記
總之,這是有趣的一天!我拿到了這個漏洞賞金計劃最大金額的獎勵。
*請認真填寫需求信息,我們會在24小時內(nèi)與您取得聯(lián)系。