JavaWeb基础

文章发布时间:

最后更新时间:

文章总字数:
4.5k

预计阅读时间:
18 分钟

Java Web基础

前言

我们前面所学的所有基于标准JDK的开发都是JavaSE,而开始研究JavaWeb之后,使用到的都是JavaEE,JavaEE实际上完全基于JavaSE,只是多了一大堆服务器相关的库以及API接口,方便开发者开发web应用。

JavaEE最核心的组件就是基于Servlet标准的Web服务器,开发者编写的应用程序是基于Servlet API并运行在Web服务器内部的。

Web开发基础

今天我们访问网站,使用App时,都是基于Web这种Browser/Server模式,简称BS架构,它的特点是,客户端只需要浏览器,应用程序的逻辑和数据都存储在服务器端。浏览器只需要请求服务器,获取Web页面,并把Web页面展示给用户即可。

之前我们分析过的URLConnection是使用java来请求服务器,也就是使用java来编写了客户端,今天主要学习如何使用java的socket来编写一个服务端,即能对请求发出响应(由于我们现在是自己在实现API,所以IDEA创建普通项目,使用JavaSE即可)。

一般套路如下:

  • 创建ServerSocket对象,绑定监听端口
  • 通过accept()方法监听客户端请求
  • 连接建立后,通过输入流读取客户端发送的请求信息
  • 通过输出流向客户端发送响应信息
  • 关闭相关资源

一个HTTP Server本质上是一个TCP服务器,我们先用TCP编程一个socket多线程实现的服务器端框架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;


class Handler extends Thread{
Socket sock;
public Handler(Socket sock){
this.sock = sock;
}
public void run() {
try(InputStream input = this.sock.getInputStream()){
try(OutputStream output = this.sock.getOutputStream()){
handle(input,output);
}//run的时候获取输入和输出流,传给handle处理
}catch(Exception e){
}finally {
try{
this.sock.close();//连接结束
}catch(IOException ioe){
}
System.out.println("Client disconnected");
}
}
private void handle(InputStream input,OutputStream output) throws IOException{
var reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
var writer = new BufferedWriter(new OutputStreamWriter(output,StandardCharsets.UTF_8));

}//处理输入输出的服务器主要逻辑

}

public class Server {
public static void main(String[] args) throws Exception{
ServerSocket ss = new ServerSocket(8082);//监听8082端口
System.out.println("Server is running in 127.0.0.1:8082");
for(;;){
Socket sock = ss.accept(); //接收socket
System.out.println("Connected from "+sock.getRemoteSocketAddress());
Thread t = new Handler(sock);//多线程异步

t.start();
}

}
}

有了框架之后我们需要具体实现服务器的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
private void handle(InputStream input,OutputStream output) throws IOException{
var reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
var writer = new BufferedWriter(new OutputStreamWriter(output,StandardCharsets.UTF_8));
boolean requestOK = false;
String first = reader.readLine();
if(first.startsWith("GET / HTTP/1.")){
requestOK = true;
}//接收GET请求
for(;;){
String header = reader.readLine();
if (header.isEmpty()){
break;
}//读到空行的时候代表请求头已经读完
System.out.println(header);//输出请求头
}
System.out.println(requestOK?"Request OK":"Request Error");
if(!requestOK){//根据request是否OK使用write返回信息给客户端
writer.write("HTTP/1.0 404 Not Found\r\n");
writer.write("Content-Length:0\r\n");
writer.write("\r\n");
writer.flush();
}else{
String data = "<html><body><h1>Hello,world!</h1></body></html>";
int len = data.getBytes(StandardCharsets.UTF_8).length;
writer.write("HTTP/1.0 200 OK\r\n");
writer.write("Connection: close\r\n");
writer.write("Content-Type: text/html\r\n");
writer.write("\r\n");
writer.write(data);
writer.flush();
}//data经过浏览器的渲染呈现在客户面前
}

运行即可访问127.0.0.1:8082,可以看到helloworld的响应,同时服务器端输出访问者信息:

image-20230905101906859

Servlet开发基础

刚刚我们手写了一个socket服务器,可以发现编写HTTP服务器是非常简单的,只需要先编写基于多线程的TCP服务,然后在一个TCP连接中读取HTTP请求,发送HTTP响应即可。

但是,如果要完善服务器端的功能,我们还需要写上千行的基础代码,非常麻烦。因此,在JavaEE平台上,处理TCP连接,解析HTTP协议这些底层工作统统扔给现成的Web服务器去做,我们只需要把自己的应用程序跑在Web服务器上。为了实现这一目的,JavaEE提供了Servlet API,我们使用Servlet API编写自己的Servlet来处理HTTP请求,Web服务器实现Servlet API接口,实现底层功能。

image-20230905102507422

这里的jakartaEE9.1需要我们的jdk版本比较高,于是通过环境变量换成jdk17:

image-20230905154101875

记住换完之后需要重启,重启完java -version就显示17了。

创建完项目之后可以发现已经自带了一些文件:

image-20230905154314743

这个webapp下的index.jsp就是我们可以访问到首页,里面的代码是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
<title>JSP - Hello World</title>
</head>
<body>
<h1><%= "Hello World!" %>
</h1>
<br/>
<a href="hello-servlet">Hello Servlet</a>
</body>
</html>

可以看到他输出了helloworld,并且设置了一个点击跳转,可以跳转到hello-servlet,看到HelloServlet里的代码(这是IDEA帮你自动写好的一个简单的Servlet模板)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.example.myservlet;

import java.io.*;

import jakarta.servlet.http.*;
import jakarta.servlet.annotation.*;

@WebServlet(name = "helloServlet", value = "/hello-servlet")
//这里的注解表示这是一个Servlet应用,同时路径为/hello-servlet
public class HelloServlet extends HttpServlet {//继承HttpServlet
private String message;

public void init() {
message = "Hello World!";
}

public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setContentType("text/html");

// Hello
PrintWriter out = response.getWriter();
out.write("<h1>" + message + "</h1>");
out.flush();
}//重写doGet方法来处理get请求

public void destroy() {
}
}

通过这个模板我们也可以学习学习Servlet应用是怎么写的:

一个Servlet总是继承自HttpServlet,然后覆写doGet()doPost()方法。注意到doGet()方法传入了HttpServletRequestHttpServletResponse两个对象,分别代表HTTP请求和响应。我们使用Servlet API时,并不直接与底层TCP交互,也不需要解析HTTP协议,因为HttpServletRequestHttpServletResponse就已经封装好了请求和响应。以发送响应为例,我们只需要设置正确的响应类型,然后获取PrintWriter,写入响应即可。

运行web应用

那写是写好了,怎么运行web应用程序呢?普通的Java程序是通过启动JVM,然后执行main方法开始运行,但是web应用程序有所不同,我们必须先启动Web服务器,再由服务器来加载我们编写的Servlet,这样就可以让我们的Servlet处理浏览器发送的请求。

因此,我们需要使用支持Servlet API的Web服务器,tomcat就行。

但是需要注意一个小问题,tomcat的版本问题,由于Servlet版本分为<=4.0和>=5.0两种,所以,要根据使用的Servlet版本选择正确的Tomcat版本。

  • 使用Servlet<=4.0时,选择Tomcat 9.x或更低版本;
  • 使用Servlet>=5.0时,选择Tomcat 10.x或更高版本。

我们这里使用的Servlet版本是大于5的,一定要使用tomcat10.x,不然就会出现无法跳转,显示资源不可访问的问题。(本人一开始用tomcat8就出现了这个问题)

换好10之后,部署好工件,tomcat,启动!

可以发现能够正常访问正常跳转到我们写的HelloServlet页面,实验成功。(话说之前Structs2-001实验环境部署之后无法跳转是不是也是因为tomcat版本问题。。。看来想学好安全确实应该先学开发)

在Servlet容器中运行的Servlet具有如下特点:

  • 无法在代码中直接通过new创建Servlet实例,必须由Servlet容器自动创建Servlet实例;
  • Servlet容器只会给每个Servlet类创建唯一实例;
  • Servlet容器会使用多线程执行doGet()doPost()方法。

Request & Response

从刚刚的doGet函数的参数中可以看出,Java EE将B/S架构中最重要的浏览器和服务器端交互封装为请求和响应对象,即 request(HttpServletRequest)response(HttpServletResponse)

HttpServletRequest常用方法

方法 说明
getParameter(String name) 获取请求中的参数,该参数是由name指定的
getParameterValues(String name) 返回请求中的参数值,该参数值是由name指定的
getRealPath(String path) 获取Web资源目录
getAttribute(String name) 返回name指定的属性值
getAttributeNames() 返回当前请求的所有属性的名字集合
getCookies() 返回客户端发送的Cookie
getSession() 获取session回话对象
getInputStream() 获取请求主题的输入流
getReader() 获取请求主体的数据流
getMethod() 获取发送请求的方式,如GET、POST
getParameterNames() 获取请求中所有参数的名称
getRemoteAddr() 获取客户端的IP地址
getRemoteHost() 获取客户端名称
getServerPath() 获取请求的文件的路径

HttpServletResponse常用方法

方法 说明
getWriter() 获取响应打印流对象
getOutputStream() 获取响应流对象
addCookie(Cookie cookie) 将指定的Cookie加入到当前的响应中
addHeader(String name,String value) 将指定的名字和值加入到响应的头信息中
sendError(int sc) 使用指定状态码发送一个错误到客户端
sendRedirect(String location) 发送一个临时的响应到客户端
setDateHeader(String name,long date) 将给出的名字和日期设置响应的头部
setHeader(String name,String value) 将给出的名字和值设置响应的头部
setStatus(int sc) 给当前响应设置状态码
setContentType(String ContentType) 设置响应的MIME类型

使用这些方法我们就可以在servlet里面实现更多的功能。

JSP开发基础

从前面的分析可以知道,Servlet就是一个可以处理http请求,发送http响应的小程序,发送响应就是获取PrintWriter,然后输出HTML。

不过使用Servlet输出HTML有点痛苦,需要一行一行write,而且还要插入各种变量。为了更简单输出HTML,就有了JSP(与PHP,ASP等类似的脚本语言)。

JSP的语法与HTML基本一致,不同点在以下几点:

  • 包含在<%----%>之间的是JSP的注释,它们会被完全忽略

  • 包含在<%%>之间的是Java代码,可以编写任意Java代码

  • 如果使用<%= xxx %>则可以快捷输出一个变量的值

  • 可以在<%@ %>中引入java类,或者加多一个include file=引入另一个JSP文件

  • JSP页面内置了几个变量,可以直接使用:

    变量名 类型 作用
    pageContext PageContext 当前页面共享数据,还可以获取其他8个内置对象
    request HttpServletRequest 客户端请求对象,包含了所有客户端请求信息
    session HttpSession 请求会话
    application ServletContext 全局对象,所有用户间共享数据
    response HttpServletResponse 响应对象,主要用于服务器端设置响应信息
    page Object 当前Servlet对象,this
    out JspWriter 输出对象,数据输出到页面上
    config ServletConfig Servlet的配置对象
    exception Throwable 异常对象

从本质上说JSP其实就是一个Servlet,在服务器启动的时候,服务器会将其自动转换成Servlet(Java class)。

在tomcat的临时目录下,可以找到hello_jsp.java这个文件,这个就是我们的hello.jsp经过tomcat的jasper转换后的Servlet,看看代码:

image-20230906102437841

可以看到,我们使用的jsp页面内置变量在try前面进行了声明和赋初始值,而下面try里面,首先通过getPageContext,获得共享对象,从而获得了我们另外八个变量具体的值,然后将我们写的jsp转换为Servlet的具体代码,可以看到一行一行用write写了。

MVC开发基础

从前面两个章节的分析也可以看出,其实使用Servlet开发和使用JSP开发各有千秋:

  • Servlet适合编写Java代码,实现各种复杂的业务逻辑,但不适合输出复杂的HTML;
  • JSP适合编写HTML,并在其中插入动态内容,但不适合编写复杂的Java代码(只适合写个小小的后门)

那能否把这两者结合在一起呢,我们可以使用MVC开发模式,先手敲一个例子实践实践再具体解释什么是MVC开发模式:

先写一个java类testMVC:

1
2
3
4
5
6
7
8
9
10
11
package com.example.myservlet;

public class testMVC {
public String name;
public long id;
public testMVC(String name,long id){
this.name = name;
this.id = id;
}

}

然后在Servlet的doGet里面模拟在数据库寻找数据并发送给WEB-INF下的testMVC.jsp:

1
2
3
4
5
6
7
8
9
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
response.setContentType("text/html");
testMVC test = new testMVC("Ming",17);
//这里模拟Java中使用JDBC与数据库进行交互,取出数据
request.setAttribute("testMVC",test);
//将类放进request的参数中
request.getRequestDispatcher("/WEB-INF/testMVC.jsp").forward(request,response);
//通过getRequestDispatcher和forward向WEB-INF下的testMVC.jsp传输request和response
}

为什么要把testMVC.jsp放在WEB-INF下呢,因为WEB-INF是一个特殊目录,Web Server会阻止浏览器对WEB-INF目录下任何资源的访问,这样就防止用户通过/testMVC.jsp路径直接访问到JSP页面,我们现在只想让用户访问到Servlet。

testMVC.jsp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<%@ page import="com.example.myservlet.testMVC" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
testMVC test= (testMVC) request.getAttribute("testMVC");
//这里通过从request中获得Attribute并进行类型强制转换获得该数据对象
%>
<html>
<head>
<title>Title</title>
</head>
<body>
<h1>Hello <%= test.name //然后使用数据对象中的数据即可%>!</h1>
<p>Your Name:
<span style="color:red">
<%= test.name %>
</span>
</p>
<p>Your ID:
<span style="color:red">
<%= test.id %>
</span>
</p>
</body>
</html>

启动服务器,当我们访问HelloServlet时,就会先处理我们的请求,然后将数据通过testMVC.jsp渲染并展示在浏览器页面。

image-20230906112458873

我们把HelloServlet看作业务逻辑处理,把testMVC类看作模型,把testMVC.jsp看作渲染,这种设计模式通常被称为MVC:Model-View-Controller,即HelloServlet作为控制器(Controller),testMVC作为模型(Model),testMVC.jsp作为视图(View)

使用MVC模式的好处是,Controller专注于业务处理,它的处理结果就是Model。Model可以是一个JavaBean,也可以是一个包含多个对象的Map,Controller只负责把Model传递给View,View只负责把Model给“渲染”出来,这样,三者职责明确,且开发更简单,因为开发Controller时无需关注页面,开发View时无需关心如何创建Model。

MVC模式广泛地应用在Web页面和传统的桌面程序中,我们在这里通过Servlet和JSP实现了一个简单的MVC模型,但它还不够简洁和灵活,以后会学到更简单的Spring MVC开发。

Servlet&Filter

为了把一些公用逻辑从各个Servlet中抽离出来,JavaEE的Servlet规范还提供了一种Filter组件,即过滤器,它的作用是,在HTTP请求到达Servlet之前,可以被一个或多个Filter预处理,类似打印日志、登录检查等逻辑,完全可以放到Filter中。

编写Filter时,必须实现Filter接口,并重写doFilter()方法,在这个方法内部,如果要继续处理请求(意思就是放行),需要使用filterchain.doFilter()。另外需要使用注解@WebFilter标注需要过滤的URL。

写一个简单的LogFilter,逻辑是通过session检查是否登录,未登录就回到登录页面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@WebFilter("/user/*")
public class AuthFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
System.out.println("AuthFilter: check authentication");
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
if (req.getSession().getAttribute("user") == null) {
// 未登录,自动跳转到登录页:
System.out.println("AuthFilter: not signin!");
resp.sendRedirect("/signin");
} else {
// 已登录,继续处理:
chain.doFilter(request, response);
}
}
}

总之,Filter是一种对HTTP请求进行预处理的组件,它可以构成一个处理链,使得公共处理代码能集中到一起;Filter适用于日志、登录检查、全局设置等;设计合理的URL映射可以让Filter链更清晰。

安全方面的总结

对于基于Filter和Servlet实现的简单架构项目,代码审计的重心在于先找出所有的Filter规则,然后判断是否有全局的安全过滤,是否对敏感URL地址做权限校验并尝试绕过。第二步则是找出所有的Servlet,分析其业务是否存在安全问题。

千万不要看到jsp和servlet里的安全问题就妄下定论,因为他们前面很有可能有Filter进行了安全过滤。

FilterServlet都是Java Web提供的API,简单的总结了下有如下共同点。

  1. FilterServlet都需要在web.xml注解(@WebFilter@WebServlet)中配置,而且配置方式是非常的相似的。
  2. FilterServlet都可以处理来自Http请求的请求,两者都有requestresponse对象。
  3. FilterServlet基础概念不一样,Servlet定义是容器端小程序,用于直接处理后端业务逻辑,而Filter的思想则是实现对Java Web请求资源的拦截过滤。
  4. FilterServlet虽然概念上不太一样,但都可以处理Http请求,都可以用来实现MVC控制器(Struts2Spring框架分别基于FilterServlet技术实现的)。
  5. 一般来说Filter通常配置在MVCServletJSP请求前面,常用于后端权限控制、统一的Http请求参数过滤(统一的XSSSQL注入Struts2命令执行等攻击检测处理)处理,其核心主要体现在请求过滤上,而Servlet更多的是用来处理后端业务请求上。

好了,有这样的Java web基础就可以去研究java web的常见漏洞了,Spring框架以后会开一个专题来分析。

参考链接:

https://www.liaoxuefeng.com/wiki/1252599548343744/1255945497738400