整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

揭秘:RESTEasy如何完美支持JAVA 微服务中的多种数据格式

者 | Lyndsey Padget

译者 |月满西楼

本文来自公众号“EAWorld”,ID:eaworld

今天我们来聊聊 Java中的微服务。虽说 Java EE提供了一个强大的平台,供我们创建、部署和管理企业级微服务,但在本文中,我将展示如何创建一个尽可能小的 RESTful微服务。

放心,在这个过程中,我们不会浪费时间精力去重复做些数据处理之类的事情。我们会通过 JBoss RESTEasy来进行搭建。而确保该微服务的轻量级,目的是为了向大家展示,在一个全新或者现存的微服务前端,建立一个 RESTful接口,真的非常简单。

与此同时,我会进一步证明,通过 RESTEasy构建的微服务具备很大的灵活性,不仅可以兼容包括 JSON,XML在内的多种数据传输格式,还支持将其部署到 Apache Tomcat[1]服务器而非 JBoss企业应用平台 (EAP)[2]上。诚然,每个工具都有自己的优势,但是我认为先在 KISS原则 [3]下探讨技术可用性会很有帮助,然后才是根据软件的长期目标和需求来决定应该为服务架构添加哪些特性。

本文中提到的代码示例都可以在 GitHub[4]上查阅,包括“starter” 和 “final”这两个分支。下面是我采用的环境,当然你的实际情况可能有所不同:

  • 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位

就技术而言...

微服务 [10]是一种体积小、更为精炼的服务,其目标是“做好一件事”。微服务之间通过一些接口进行交互是很普遍的现象。如果该接口可以通过 web访问 (使用 HTTP),那么它就是一个 web服务。部分 web服务是基于 RESTful这种架构风格的,另一些则不是。注意,微服务并不都是 web服务,web服务并不都是 RESTful web服务,RESTful web服务也并不都是微服务!

REST和 XML……能否共存?

如果你此前在使用 RESTful web服务时,没用过除 JSON 以外的文本数据交换格式 [11]来进行内容传输,那么你可能会认为二者是不相关的。但是回想下,REST是定义 API的一种架构风格,REST和 JSON这两者又碰巧一起流行起来 (注意,这并非偶然)。XML多年的发展使其拥有大量的客户群,能够接纳和提供 XML数据传输格式的 RESTful web服务, 不管是对那些已经依赖于这类内容交互系统的组织,还是对仅仅是更熟悉 XML的用户来说,都非常有用。当然,通常情况下,JSON依然是首选,因为其消息体更小,但有时 XML只是一个更简单的“sell”。拥有一个能同时支持这两种格式的 RESTful微服务是最理想的 ;从部署的角度来说,它不仅简洁,具备可扩展性,还有足够的灵活性,可以支持不同类型的内容,从而满足那些其他有调用需求的应用程序。

为什么选择 RESTEasy?

RESTEasy[12]是 Jboss的一个框架,可以用来构建 RESTful web服务。通过 RESTEasy构建的 RESTful web服务,可以根据四个函数库来实现对 XML和 JSON这两种数据传输格式的支持:

  • resteasy-jaxrs,实现了 JAX-RS 2.0 (用于 RESTful Web服务的 Java API) [13]

  • resteasy-jaxb-provider,其 JAXB[14]绑定能有效支持 XML

  • resteasy-jettison-provider,用 Jettison[15]将 XML转换为 JSON

  • resteasy-servlet-initializer,将服务部署到 Servlet 3.0容器 (在 Tomcat服务器上)

首先,创建一个内含 pom.xml数据包的 web服务项目:

<?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>

(左右滑动可查看全部代码,下同)

这些数据库的大小大概在 830KB。当然,这些直接的依赖环境(dependency),运用 Maven一起构建项目也会带来部分传递性依赖。

接下来,我将用“Maven方法”来构建这个项目,例如在 src/main/java中,使用 Maven构建命令等,不想用 Maven的话,你也可以直接从下载页面 [16]下载 RESTEasy jar数据包。下载的时候不用理会 RESTEasy站点上弹出的这个提示:JBoss仅仅是在尝试引导你采用更“企业化”的方法。你只需点击“继续下载”,来开展后面的操作。

项目设计

下面这个微服务可以用非常简单的方法来演示一些基本概念。如下图所示,它包括 5个等级。

此处,FruitApplication是微服务的切入点。FruitService提供了主要的路径 (/fruits),同时它也充当了路由器的功能。苹果和水果都是模型;水果包含一些抽象的功能,苹果则会具体地扩展它的功能。

和你设想一致的是,FruitComparator可以提供比较功能。不熟悉 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结构的样子:

<apple> <id>1</id> <variety>Golden delicious</variety> <color>yellow</color> </apple>

因为没有约定明显的构造函数:Java使用了隐式的、无参数的默认构造函数,所以一些更微妙的事情在发生。这个无参数的构造函数对 JAXB 施展魔法般效果的工作是十分必要的(本文解释了这一点,以及必要的话,如何用 XMLAdapter来让它工作)。

现在我们有了一个对象:被定义的苹果。它有三个属性: ID、多样性和颜色。

服务

FruitService 被用来作为与微服务交互的主要路径 (/fruits)。在本例中,我使用 @path注释直接在该层级中定义了第一个路径,/fruits/apples。随着 RESTful微服务的扩展,你可能希望在自己的层级中定义多个最终路径 (例如 /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; } }

这张苹果的地图帮助我们根据 id跟踪苹果的数据,从而模拟某些类型的数据持久层。利用 getApples方法(常用的 HTTP请求方式)将会返回地图跟踪到的相关苹果数据。GET /apples route是用 @GET和 @path注释定义的,它可以生成数据传输格式 XML或 JSON的内容。

这个方法需要返回一个 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); } }

注意,如果想要对苹果的一个特定属性进行排序,比如颜色,我们就必须创建一个新的比较器去取代,并取个名字,比如 AppleComparator。

应用程序

在 RESTEasy3.1.x中, 你需要定义一个扩展应用的层级。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文件中将它连接起来,这会在后面的章节“web服务连接”中进行介绍。

对象的构建集合

GET /apples调用将返回如下数据:

<?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" } } ]

但是,我们可以将数据更改成看起来稍有点不同:

<?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产生了不太好的影响。如果你喜欢这个结构,可以用它自己的类型打包 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服务连接

从将该服务部署到 Tomcat开始,我用一个放在 src/main/webapp/web inf/web.xml的 web应用部署描述符文件。它所包含的内容如下:

<?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服务器将包含该模式的传入请求传输到 Resteasy服务。关于如何建立这个文件的更多信息,以及可用的不同选项,请参阅 Tomcat的应用程序部署文档 [17]。

构建及部署

从项目的根目录中,可以运行以下内容来构建 WAR(web application resource,web应用程序资源)文件:

mvn clean install

这将在 target文件夹中创建一个包含 WAR文件的新文件夹。虽然用 Maven或其他工具来部署该文件也可以,但我只用一个简单的复制命令就可以。需要注意的是,每次将 WAR重新部署到 Tomcat服务器时,应该首先暂停服务器运行,并删除服务应用程序文件夹 (在本例中,是这个文件夹:< tomcatDirectory >/webapps/resteasy)和旧的 WAR文件 (< tomcatDirectory >/webapps/resteasy.war)。

[sudo] cp target/resteasy.war <tomcatDirectory>/webapps/resteasy.war

如果此时 Tomcat服务器正在运行,那么会即刻部署 web服务。如果不是,下次服务器启动时,该服务也会被自动部署上去。然后,就可以通过如下地址访问 web服务:http://< tomcatHost >:< tomcatPort >/resteasy/v1/fruits/apples。我的范例中是这个地址 http://localhost:8080/resteasy/v1/fruits/apples。

通过“内容协商(Content negotiation)”测试服务

内容协商(Content negotiation)是一种机制,它可以提供不同资源 (URI)的表现形式。最基本的,这意味着可以:

  • 详细设置 Accept header,以指示希望从服务中接受的内容类型

  • 详细设置 Content-Type header,以指示发送给服务的内容类型

要获取更多关于内容协商(Content negotiation)和 header的信息,请参阅 RFC 2616[18]的第 12和 14章。在本例中,你真正需要了解的是:

  • @Produces annotation(注释)指明了该方法能够生成哪些内容 (这将尝试匹配请求上的 Accept header)。

  • @Consumes annotation(注释)指明了该方法能够使用哪些内容 (这将尝试匹配请求的 content-type header)。

如果您试图对一个有效端点进行 HTTP调用,但是内容不能被协商,这意味着没有 @Produces匹配该 Accept数据,或者没有 @Consumes匹配 Content-Type数据,将被返回 HTTP状态码 415:不支持的数据传输格式。

返回常见数据传输格式的 GET调用实际上可以直接进入浏览器。对于 GET /apples这样的调用,默认情况下您将获得 XML:

不过,使用像 Postman[19]这类工具可能会更有帮助,因为它明确地指定 Accept header作为 application/xml:

这两种方法都返回了一些有效但没有多大意义的 XML,即一个空的苹果列表。但是这里有一些很酷的东西。将 Accept header更改为 application/json,太好了,瞧!JSON*生效*了:

不只是“读取”

你可能会发现,很多 RESTful web服务的例子,都是只读的,部分也不会有进一步的提示,比如如何去创建、更新和删除这些操作。虽然我们现在已经有了 web服务的框架,但这是一个不能更改的空列表,这并没多大意义。所以我们应该运用一些其他方法,将苹果添加到这个列表中或从列表中将其删除。

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检索苹果数据 (如果在地图中没有找到,则返回状态代码 404)

  • 通过 id删除苹果数据

  • 创建新的苹果数据 (如果成功的话,返回状态代码 201)

这些方法完善了很多功能,确保了服务可以按照预期工作。更新苹果 (使用 @PUT和 /或 @PATCH),以及更多的关于端点、逻辑和管理持久性方面的功能操作,都留给读者你们来练习吧。

当我们再次进行构建和部署时会发现 (如果用 Maven或者 Tomcat来进行设置,请参阅上文“构建和部署”),现在已经可以在服务中创建、检索和删除苹果了。而且即使不在服务器上做任何重新配置,也可以在 XML和 JSON之间进行选择性调用。

来创建一个拥有“application/json”内容类型和 JSON主体的苹果,如下图所示:

这是另一个例子:创建一个具有“application/xml”内容类型和 XML主体的苹果。

在 XML中检索所有的苹果数据:

在 JSON中通过 id检索 apple 2的数据:

通过 id删除 apple 1的数据:

在 JSON中检索所有苹果的数据:

小结

在此我们已经探讨了 RESTEasy架构如何在 Java web服务中无缝支持 XML和 JSON数据传输格式。我还解释了 REST、Media type(数据传输格式)、web服务和微服务之间的技术差异,因为在这些术语中有很多容易混淆的灰色地带。

我这里列举的例子可能有点勉强,生活中我其实从来没有真正需要过水果相关的数据,我也没有在食品行业工作过。之所以用水果来举例,是因为我觉得这个“规模”能有助于大家理解微服务的概念,你也可以想象其他例如蔬菜,罐头或海鲜这样的微服务是如何共同构成一个食物分配系统。现实世界中,食品杂货店的食物分配系统实际上非常复杂,它必须考虑到包括销售、优惠券、过期日期、营养信息等各方面的问题。

当然,你可以选择其他方式去对系统进行分割,但当你需要一种快速高效、轻量级工具来支持多种数据格式时,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/

期望得到更多优质技术干货,欢迎加入 EAWorld社区,与近万名技术人一起成长。入群暗号:328

者:小不点啊

来源:www.cnblogs.com/leeSmall/p/9356535.html

一、Nginx Rewrite 规则


1. Nginx rewrite规则


Rewrite规则含义就是某个URL重写成特定的URL(类似于Redirect),从某种意义上说为了美观或者对搜索引擎友好,提高收录量及排名等。


语法:


rewrite <regex> <replacement> [flag]
关键字 || 正则 || 替代内容 || flag标记


Rewrite规则的flag标记主要有以下几种:


  • last :相当于Apache里的(L)标记,表示完成rewrite;
  • break:本条规则匹配完成后,终止匹配,不再匹配后面的规则
  • redirect:返回302临时重定向,浏览器地址会显示跳转后的URL地址
  • permanent:返回301永久重定向,浏览器地址栏会显示跳转后的URL地址


last和break用来实现URL重写,浏览器地址栏URL地址不变


2. Nginx rewrite例子


a) 例如用户访问www.dbspread.com,想直接跳转到网站下面的某个页面,www.dbspread.com/new.index.html如何来实现呢?我们可以使用Nginx Rewrite 来实现这个需求,具体如下:在server中加入如下语句即可:


效果图如下:

                rewrite     ^/$    http://www.dbspread.com/new.index.html  permanent;
对应如下语法:
                rewrite    <regex>    <replacement>                 [flag];
                关键字      正则        替代内容                    flag标记

正则表达式说明:

*代表前面0或更多个字符                +代表前面1或更多个字符
?代表前面0或1个字符                  ^代表字符串的开始位置
$代表字符串结束的位置                 。为通配符,代表任何字符

b)例如多个域名跳转到同一个域名,nginx rewrite规则写法如下:


格式:

rewrite <regex> <replacement> [flag];
关键字 || 正则 || 替代内容 || flag标记


说明:


  • rewrite为固定关键字,表示开始进行rewrite匹配规则、
  • regex部分是 ^/(.*) ,这是一个正则表达式,匹配完整的域名和后面的路径地址
  • replacement部分是http://www.dbspread.com/,是取自regex部分( )里的内容。匹配成功后跳转到的URL。
  • flag部分 permanent表示永久301重定向标记,即跳转到新的 http://www.dbspread.com/ 地址上



二、Nginx 防盗链


1. 什么是防盗链


比如http://www.dbspread.com/download/av123.rmvb 这个视频下载地址被其他网站引用,比如在www.test.com的index.html引用download/av123.rmvb就叫盗链,我们要禁止这种引用就叫做防盗链



2. 怎么实现防盗链


在nginx的nginx.conf的server里面配置如下代码


三、Nginx 动静分离

1. 动静分离是什么

Nginx动静分离是让动态网站里的动态网页根据一定规则把不变的资源和经常变的资源区分开来,动静资源做好了拆分以后,我们就可以根据静态资源的特点将其做缓存操作,这就是网站静态化处理的核心思路。

2. 动静分离原理图

3. Nginx动静分离应该注意的地方

1). WEB项目开发时要注意,将静态资源尽量放在一个static文件夹2). 将static静态资源文件夹放到Nginx可以取到的位置3). 页面要建立全局变量路径,方便修改路径4). 修改nginx.conf的location, 匹配静态资源请求

4. Nginx动静分离步骤

4.1 准备一个静态资源button.css

body {
    margin: 10px 20px;
    text-align: center;
    font-family: Arial, sans-serif;
    background-color: red;
}

4.2 在/var/local下新建一个static文件夹用来存放静态资源button.css

4.3 在tomcat-8080/webapps/ROOT下的index.html里面引入button.css


4.4 在nginx的nginx.conf中server节点新增静态资源分离的配置


对于Nginx基础配置,推荐之前的:后端实践:Nginx日志配置(超详细)

4.5 访问页面查看效果

四、Nginx+keepalived 实现高可用

1. keepalived是什么

Keepalived软件起初是专为LVS负载均衡软件设计的,用来管理并监控LVS集群系统中各个服务节点的状态,后来又加入了可以实现高可用的VRRP (Virtual Router Redundancy Protocol ,虚拟路由器冗余协议)功能。因此,Keepalived除了能够管理LVS软件外,还可以作为其他服务(例如:Nginx、Haproxy、MySQL等)的高可用解决方案软件

2. keepalived主要功能

管理LVS负载均衡软件实现LVS集群节点的健康检查作为系统网络服务的高可用性(failover)

3. keepalived故障转移

Keepalived高可用服务之间的故障切换转移,是通过 VRRP 来实现的。在 Keepalived服务正常工作时,主 Master节点会不断地向备节点发送(多播的方式)心跳消息,用以告诉备Backup节点自己还活着,当主 Master节点发生故障时,就无法发送心跳消息,备节点也就因此无法继续检测到来自主 Master节点的心跳了,于是调用自身的接管程序,接管主Master节点的 IP资源及服务。而当主 Master节点恢复时,备Backup节点又会释放主节点故障时自身接管的IP资源及服务,恢复到原来的备用角色。

说明:keepalived的主从切换和redis的主从切换是不一样的,keepalived的主节点挂了以后,从节点变为主节点,之前的主节点恢复以后继续做主节点。redis的主节点挂了以后,重新恢复以后变为从节点

4. keepalived高可用架构示意图

说明:

虚拟ip(VIP):192.168.152.200,对外提供服务的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

5. keepalived安装

环境准备:

centos6、jdk

虚拟ip(VIP):192.168.152.200,对外提供服务的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的环境准备请查看我的前一篇关于nginx的文章

5.1 安装keepalived的步骤:

注:192.168.152.129(keepalived从节点) 与 192.168.152.130(keepalived主节点)先安装好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系统提示警告 *** 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再次执行./configure --prefix=/usr/local/keepalived系统提示错误 configure: error: libnfnetlink headers missingyum install -y libnfnetlink-devel再次执行./configure --prefix=/usr/local/keepalived

make && make install

到此keepalived安装完成,但是接下来还有最关键的一步,如果这一步没有做后面启动keepalived的时候会报找不到配置文件的错误

Configuration file '/etc/keepalived/keepalived.conf' is not a regular non-executable file

安装完成后,进入安装目录的etc目录下,将keepalived相应的配置文件拷贝到系统相应的目录当中。keepalived启动时会从/etc/keepalived目录下查找keepalived.conf配置文件

mkdir /etc/keepalived

cp /usr/local/keepalived/etc/keepalived/keepalived.conf /etc/keepalived

5.2 修改keepalived主节点192.168.152.130的/etc/keepalived/keepalived.conf配置文件


5.3 修改keepalived从节点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服务,进行VIP转移
              killall keepalived                    
      fi
fi


5.5 192.168.152.130(keepalived主节点)和 192.168.152.129(keepalived从节点)的nginx的配置文件nginx.conf

user root root; #使用什么用户启动NGINX 在运行时使用哪个用户哪个组
worker_processes 4; #启动进程数,一般是1或8个,根据你的电脑CPU数,一般8个
worker_cpu_affinity 00000001 00000010 00000100 00001000; #CPU逻辑数——把每个进程分别绑在CPU上面,为每个进程分配一个CPU
#pid /usr/local/nginx/logs/nginx.pid
worker_rlimit_nofile 102400; #一个进程打开的最大文件数目,与NGINX并发连接有关系

#工作模式及连接数上限
events
{
  use epoll; #多路复用IO 基于LINUX2.6以上内核,可以大大提高NGINX的性能 uname -a查看内核版本号
  worker_connections 102400; #单个worker process最大连接数,其中NGINX最大连接数=连接数*进程数,一般1GB内存的机器上可以打开的最大数大约是10万左右
  multi_accept on;   #尽可能多的接受请求,默认是关闭状态
}

#处理http请求的一个应用配置段
http
{
  #引用mime.types,这个类型定义了很多,当web服务器收到静态的资源文件请求时,依据请求文件的后缀名在服务器的MIME配置文件中找到对应的MIME #Type,根据MIMETYPE设置并response响应类型(Content-type)
  include       mime.types; 
  default_type  application/octet-stream; #定义的数据流,有的时候默认类型可以指定为text,这跟我们的网页发布还是资源下载是有关系的
  fastcgi_intercept_errors on; #表示接收fastcgi输出的http 1.0 response code
  charset utf-8;
  server_names_hash_bucket_size 128; #保存服务器名字的hash表
  #用来缓存请求头信息的,容量4K,如果header头信息请求超过了,nginx会直接返回400错误,先根据client_header_buffer_size配置的值分配一个buffer,如果##分配的buffer无法容纳request_line/request_header,那么就会##再次根据large_client_header_buffers配置的参数分配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; #允许客户端请求的最大单文件字节数 上传文件时根据需求设置这个参数
  #指定NGINX是否调用这个函数来输出文件,对于普通的文件我们必须设置为ON,如果NGINX专门做为一个下载端的话可以关掉,好处是降低磁盘与网络的IO处理数及#系统的UPTIME
  sendfile on; 
  #autoindex on;开启目录列表访问,适合下载服务器
  tcp_nopush on; #防止网络阻塞
  #非常重要,根据实际情况设置值,超时时间,客户端到服务端的连接持续有效时间,60秒内可避免重新建立连接,时间也不能设太长,太长的话,若请求数10000##,都占用连接会把服务托死
  keepalive_timeout 60;
  tcp_nodelay on; #提高数据的实时响应性
  client_body_buffer_size 512k; #缓冲区代理缓冲用户端请求的最大字节数(请求多)

  proxy_connect_timeout   5; #nginx跟后端服务器连接超时时间(代理连接超时)
  proxy_read_timeout      60; #连接成功后,后端服务器响应时间(代理接收超时)
  proxy_send_timeout      5; #后端服务器数据回传时间(代理发送超时)
  proxy_buffer_size       16k; #设置代理服务器(nginx)保存用户头信息的缓冲区大小
  proxy_buffers           4 64k; #proxy_buffers缓冲区,网页平均在32k以下的话,这样设置
  proxy_busy_buffers_size 128k; #高负荷下缓冲大小
  proxy_temp_file_write_size 128k; #设定缓存文件夹大小,大于这个值,将从upstream服务器传

  gzip on; #NGINX可以压缩静态资源,比如我的静态资源有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,但设为9时,压缩完可能只有2M了。一般设置为2
  gzip_types       text/plain application/x-javascript text/css application/xml; #压缩类型:text,js css xml 都会被压缩
  gzip_vary on; #作用是在http响应中增加一行目的是改变反向代理服务器的缓存策略

#日志格式 
log_format  main '$remote_addr - $remote_user [$time_local] "$request" ' #ip 远程用户 当地时间  请求URL
                 '$status $body_bytes_sent "$http_referer" ' #状态  发送的大小  响应的头
         '"$http_user_agent" $request_time'; #客户端使用的浏览器  页面响应的时间

#动态转发         
upstream web1 {
    #每个请求按访问ip的hash结果分配,这样每个访客固定访问一个后端服务器,可以解决session的问题。配置了ip_hash就没有负载均衡的效果了,每次访问的都是同一个tomcat
    #ip_hash; 
    #转发的后端的tomcat服务器,weight表示转发的权重,越大转发的次数越多,机器性能不一样配置的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; #监听80端口
    server_name  www.dbspread.com; #域名
    #rewrite规则
    index  index.jsp index.html index.htm;
    root   /usr/local/nginx/html; #定义服务器的默认网站根目录位置
    #重定向
    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白名单发出来的请求,直接重定向到403.html这个页面或者返回403 
                     #rewrite ^/ http://www.dbspread.com/403.html;
                     return 403;
                }
        }

    #监听完成以后通过斜杆(/)拦截请求转发到后端的tomcat服务器
    location / 
        {
            #如果后端的服务器返回502、504、执行超时等错误,自动将请求转发到upstream负载均衡池中的另一台服务器,实现故障转移。
            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,想直接跳转到网站下面的某个页面:www.dbspread.com/new.index.html
            proxy_pass http://web1; #跳转到对应的应用web1
        }

       # location ~ .*\.(php|jsp|cgi|shtml)?$ #动态分离 ~匹配 以.*结尾(以PHP JSP结尾走这段)
       #  {
       #     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;
       # }

        #静态分离 ~匹配 以.*结尾(以html|htm|gif|jpg|jpeg|bmp|png|ico|txt|js|css结尾走这段),当然不是越久越好,如果有10000个用户在线,都保存几个月,系统托跨
        location ~ .*\.(html|htm|gif|jpg|jpeg|bmp|png|ico|txt|js|css)$ 
        {
            root /var/local/static; #静态资源存放在nginx的安装机器上
            #proxy_pass http://www.static.com; #静态资源也可存放在远程服务器上
            expires    30d;
        }

        #日志级别有[debug|info|notice|warn|error|crit]  error_log 级别分为 debug, info, notice, warn, error, crit  默认为crit, 生产环境用error 
        #crit 记录的日志最少,而debug记录的日志最多
        access_log  /usr/local/logs/web2/access.log main;
        error_log   /usr/local/logs/web2/error.log  crit;

    }


}


到这一步环境准备已完成,相关的配置也修改完成,下面我们来查看效果


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),可多设,每行一个
    }


到这一步环境准备已完成,相关的配置也修改完成,下面我们来查看效果


5.7 分别启动192.168.152.129的两个tomcat


5.8 分别启动192.168.152.130(keepalived主节点)和

192.168.152.129(keepalived从节点)的keepalived的

启动命令:


/usr/local/keepalived/sbin/keepalived  

可以看到keepalived和nginx都启动了

在浏览器输入www.dpspread.com域名访问

5.9 下面我们停掉主节点192.168.152.130的keepalived和nginx

可以看到从节点变为主节点了

在浏览器输入地址www.dpspread.com访问,可以看到访问正常

5.10 下面我们重新启动主节点192.168.152.130

可以看到主节点重新启动以后变为主节点了

之前变为主节点的从节点又变回从节点了

到此keepalived+nginx的高可用完美完成

译:h4d35

预估稿费:120RMB

投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿

前言


本篇文章主要介绍了在一次漏洞悬赏项目中如何利用配置错误挖到一个认证绕过漏洞。

从JS文件中发现认证绕过漏洞


本文内容源自一个私有漏洞赏金计划。在这个漏洞计划中,接受的漏洞范围限于目标网站少数几个公开的功能。基于前期发现的问题(当我被邀请进这个计划时,其他人一共提交了5个漏洞),似乎很难再挖到新的漏洞。同时,在赏金详情中提到了这样一句话:

如果你成功进入管理页面,请立即报告,请勿在/admin中进行进一步的测试。

然而,目标网站中存在一个仅限于未认证和未经授权的用户访问的管理页面。当我们访问/login或/admin时会跳转到https://bountysite.com/admin/dashboard?redirect=/。

对登录页面进行暴力破解也许是一个可行方案,但是我并不喜欢这种方式。看一下网页源码,没什么有用的内容。于是我开始查看目标网站的结构。似乎目标网站的JS文件都放在少数几个文件夹中,如/lib、/js、/application等。

有意思!

祭出神器BurpSuite,使用Intruder跑一下看能否在上述文件夹中找到任何可访问的JS文件。将攻击点设置为https://bountysite.com/admin/dashboard/js/*attack*.js。注意,不要忘记.js扩展名,这样如果文件能够访问则返回200响应。确实有意思!因为我找到了一些可访问的JS文件,其中一个文件是/login.js。

访问这个JS文件https://bountysite.com/admin/dashboard/js/login.js,请求被重定向至管理页面:) 。但是,我并没有查看该文件的权限,只能看到部分接口信息。

但是我并没有就此止步。这看起来很奇怪,为什么我访问一个.js文件却被作为HTML加载了呢?经过一番探查,终于发现,我能够访问管理页面的原因在于*login*。是的,只要在请求路径/dashboard/后的字符串中含有*login*(除了'login',这只会使我回到登录页面),请求就会跳转到这个管理接口,但是却没有正确的授权。

我继续对这个受限的管理接口进行了进一步的测试。再一次查看了页面源码,试着搞清楚网站结构。在这个管理接口中,有其他一些JS文件能够帮助我理解管理员是如何执行操作的。一些管理操作需要一个有效的令牌。我试着使用从一个JS文件中泄露的令牌执行相关管理操作,然并卵。请求还是被重定向到了登录页面。我发现另外一个真实存在的路径中也部署了一些内容,那就是/dashboard/controllers/*.php。

再一次祭出BurpSuite,使用Intruder检查一下是否存在可以从此处访问的其他任何路径。第二次Intruder的结果是,我发现几乎不存在其他无需授权即可访问的路径。这是基于服务器返回的500或者200响应得出的结论。

回到我在上一步侦察中了解到的网站结构中,我发现这些路径是在/controllers中定义的,通过/dashboard/*here*/进行访问。但是直接访问这些路径会跳转到登录页面,似乎网站对Session检查得还挺严格。此时我又累又困,几乎都打算放弃了,但是我想最后再试一把。如果我利用与访问管理页面相同的方法去执行这些管理操作会怎么样呢?很有趣,高潮来了:) 我能够做到这一点。

通过访问/dashboard/photography/loginx,请求跳转到了Admin Photography页面,并且拥有完整的权限!

从这里开始,我能够执行和访问/dashboard/*路径下的所有操作和目录,这些地方充满了诸如SQL注入、XSS、文件上传、公开重定向等漏洞。但是,我没有继续深入测试,因为这些都不在赏金计划之内,根据计划要求,一旦突破管理授权限制,应立即报告问题。此外,根据管理页面显示的调试错误信息可知,我之所以能够访问到管理页面,是因为应用程序在/dashboard/controllers/*文件中存在错误配置。期望达到的效果是:只要请求链接中出现*login*,就重定向至主登录页面,然而,实际情况并不如人所愿。

后记


总之,这是有趣的一天!我拿到了这个漏洞赏金计划最大金额的奖励。