JavaEE #
Servlet 技术 #
整体理解 #
Servlet 是运行在 Web 服务器或应用服务器上的程序,它是作为来自 Web 浏览器或其他 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层。使用 Servlet 可以收集来自网页表单的用户输入,呈现来自数据库或者其他源的记录,还可以动态创建网页。
在纯 Servlet 项目中一般会用到 Tomcat。这里我们和 PHP 的经典架构 LAMP 进行一个类比。
在 LAMP 中,当 Apache 接收到请求后会自己处理静态内容,如 HTML、CSS 等,然后把这些内容直接返回给客户端,而动态请求交给后续的 PHP 处理,比如查询数据库等。
到 Java Web 架构中,也可以使用 Apache/Nginx 来处理静态资源,然后动态请求就会发到 Tomcat 处理,再由 Tomcat 交给 Servlet 处理并生成对应内容并返回客户端。当然也可以不使用 Apache/Nginx 这类应用,因为 Tomcat 本身具备一个简易的 Web Server,也可以处理 HTTP 请求,然后交给内部的 Servlet 处理请求。
Tomcat 与 Servlet #
Tomcat 是 Servlet 容器。Servlet 类写好之后是没有 main 方法的,无法自行启动。我们必须把 Servlet 放到 Tomcat 中 Tomcat 才能创建 Servlet 实例,并在合适的时机调用它的方法。同时 Tomcat 管理了 Servlet 的完整生命周期。当启动或第一次请求时,Tomcat 调用init()方法,让 Servlet 初始化;每一次处理请求时,Tomcat 开线程,调用service()方法(进而调用我们的doGet/doPost方法);服务器关闭或应用卸载时,Tomcat 调用destroy()方法,让 Servlet 释放资源。
同时,Tomcat 可以接收原始的 TCP 字节流并解析为 HTTP 请求,然后把各种请求的参数封装到HttpServletRequest对象中,我们可以直接调用request.getParameter("xxx")来获取参数。在处理 HTTP 响应时我们把结果写到HttpServletResponse对象中,Tomcat 会自动把它组装成合法的 HTTP 响应报文发送到客户端。当多个用户同时访问时,Tomcat 会自动分配多个线程来调用 Servlet,并保证每个请求之间互不干扰。而向长连接管理、SSL/TLS 加密、会话管理等都是由 Tomcat 提供的。
样例代码 #
package com.example.servlet;
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;
//一个简单的servlet样例
public class IndexServlet extends HttpServlet {
@Override
public void init() throws ServletException {
System.out.println("init servlet");
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String name = req.getParameter("name");
resp.setContentType("text/html");
PrintWriter out = resp.getWriter();
out.println(name);
System.out.println("doGet servlet");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("doPost servlet");
}
@Override
public void destroy() {
System.out.println("destroy servlet");
}
}
同时需要给这个文件配置路由,如果不配置路由则无法调用这个文件的相关方法。
第一种配置路由的方法是在 web.xml 种添加相关配置,比如添加:
<servlet>
<servlet-name>index</servlet-name>
<servlet-class>com.example.servlet.IndexServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>index</servlet-name>
<url-pattern>/index</url-pattern>
</servlet-mapping>
第二种方法更加简单,直接在这个类的上面加上一行注解即可:
@WebServlet(name="index",value="/index")
Filter 过滤器 #

通过这一张图我们可以清楚地看到客户端访问时的数据走向。
Filter被称为过滤器,过滤器实际上就是对Web资源进行拦截,做一些处理后再交给下一个过滤器或Servlet处理,通常都是用来拦截request进行处理的,也可以对返回的 response进行拦截处理。开发人员利用filter技术,可以实现对所有Web资源的管理,例如实现权限访问控制、过滤敏感词汇、压缩响应信息等一些高级功能。
样例代码 #
package com.example.servlet.Filter;
import javax.servlet.*;
import java.io.IOException;
public class XssFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("XssFilter init");
}
@Override
public void destroy() {
System.out.println("XssFilter destroy");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("XssFilter doFilter");
chain.doFilter(request,response); //数据放行
}
}
然后在 web.xml 里配置一下相关路由:
<filter>
<filter-name>xss</filter-name>
<filter-class>com.example.servlet.Filter.XssFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>xss</filter-name>
<url-pattern>/index</url-pattern>
</filter-mapping>
或者配置注解:
@WebFilter(filterName = "xss",value="/index")
然后运行代码,会发现我们还没有访问/index这个路由时,已经输出了XssFilter init。然后我们访问/index路由,先输出init servlet,再输出XssFilter doFilter。然后停止程序运行,先输出destroy servlet,再输出XssFilter destroy。
一个简单的 XSS 过滤:
package com.example.servlet.Filter;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
public class XssFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("XssFilter init");
}
@Override
public void destroy() {
System.out.println("XssFilter destroy");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
System.out.println("XssFilter doFilter");
HttpServletRequest request = (HttpServletRequest) servletRequest;
String name = request.getParameter("name");
if(!name.contains("script")){
chain.doFilter(servletRequest,servletResponse);
}else {
System.out.println("XSS attack");
}
}
}
过滤器就是数据在到达核心程序之前进行一次过滤。后续内存马技术会涉及到过滤器。
Listener 监听器 #
监听器是 Servlet 规范中定义的一种特殊类:
- 用来监听 ServletContext、HttpSession 和 ServletRequest 等域对象的创建和销毁事件
- 用来监听域对象的属性发生修改的事件
- 可以在事件发生前后做一些必要的处理
Servlet规范中定义了9个监听器接口,可以用来监听ServletContext、HttpSession 和 ServletRequest 对象的生命周期 和属性变化事件。
监听器按照监听的事件可以分成3类:
- 监听对象创建和销毁的监听器
- 监听对象属性变更的监听器
- 监听 HttpSession 中的对象状态改变的监听器
关于监听器的更多了解参考Web应用监听器实战:在线人数统计与事件处理-CSDN博客
样例代码 #
下面是一个简单的 Session 监听器:
package com.example.servlet.Listen;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
@WebListener("/admin")
public class SessionListen implements HttpSessionListener {
@Override
public void sessionCreated(HttpSessionEvent se) {
System.out.println("Listen Session created");
}
@Override
public void sessionDestroyed(HttpSessionEvent se) {
System.out.println("Listen Session destroyed");
}
}
package com.example.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet(name="admin",value="/admin")
public class AdminServlet extends HttpServlet {
@Override
public void init() throws ServletException {
System.out.println("AdminServlet init");
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("AdminServlet doGet");
req.getSession().invalidate();
}
@Override
public void destroy() {
}
}
ORM 框架 #
Java Web 中的 ORM 框架就是一个把 Java 对象和数据库表记录之间自动相互转换的工具。
原生 JDBC #
package com.example.jdbc.servlet;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.sql.*;
@WebServlet(name="jdbc",value="/sql")
public class JdbcServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String id=req.getParameter("id");
String sql="select * from users where user_id="+id;
String url="jdbc:mysql://localhost:3306/dvwa?useUnicode=true&characterEncoding=utf-8";
try {
//加载驱动
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection(url, "root", "123456"); //连接数据库
//创建SQL语句执行器
Statement statement = connection.createStatement();
//执行查询语句
ResultSet resultSet = statement.executeQuery(sql);
while (resultSet.next()){
//按照列名查找数据并转换为字符串类型,输出到浏览器上
resp.getWriter().println(resultSet.getString("user_id"));
resp.getWriter().println(resultSet.getString("first_name"));
resp.getWriter().println(resultSet.getString("last_name"));
resp.getWriter().println(resultSet.getString("user"));
}
} catch (ClassNotFoundException | SQLException e) {
throw new RuntimeException(e);
}
}
}
以上是一个简单的原生 JDBC 代码,存在 SQL 注入漏洞。这么写既危险又繁琐,所以出现了其他 ORM 框架,既能简化代码,又能提高安全性。
JDBC 中的 SQL 预编译 #
为了提高原生 JDBC 的安全性、防止存在 SQL 注入,JDBC 中也有预编译机制。
package com.example.jdbc.servlet;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.sql.*;
@WebServlet(name="jdbc",value="/sql")
public class JdbcServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String id=req.getParameter("id");
//不安全写法
//String sql="select * from users where user_id="+id;
//预编译
String sql="select * from users where user_id=?";
String url="jdbc:mysql://localhost:3306/dvwa?useUnicode=true&characterEncoding=utf-8";
try {
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection(url, "root", "123456");
//Statement statement = connection.createStatement();
//ResultSet resultSet = statement.executeQuery(sql);
//预编译
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1,id);
ResultSet resultSet = preparedStatement.executeQuery();
while (resultSet.next()){
resp.getWriter().println(resultSet.getString("user_id"));
resp.getWriter().println(resultSet.getString("first_name"));
resp.getWriter().println(resultSet.getString("last_name"));
resp.getWriter().println(resultSet.getString("user"));
}
} catch (ClassNotFoundException | SQLException e) {
throw new RuntimeException(e);
}
}
}
这就是原生 JDBC 中简单的 SQL 预编译代码,可以防御 SQL 注入漏洞。
MyBatis #
MyBatis 的配置相对复杂,这里也不记录如何配置了,直接放样例代码。
mybatis-config.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/dvwa?serverTimezone=UTC"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="UserMapper.xml"/>
</mappers>
</configuration>
这个配置主要负责连接数据库。
UserMapper.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mybatis.mapper.UserMapper">
<select id="findUserByName" parameterType="String" resultType="com.example.mybatis.model.User">
select * from users where user = '${name}'
</select>
</mapper>
这个配置就是负责对数据库进行查询,同时 SQL 注入点也在这里。${name}是不安全的写法,会导致 SQL 注入,而#{name}就是预编译写法,更加安全。
UserMapper.java:
package com.example.mybatis.mapper;
import com.example.mybatis.model.User;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface UserMapper {
List<User> findUserByName(@Param("name") String name);
}
这个接口来接收参数,并且会填充到 SQL 语句中。
package com.example.mybatis.model;
public class User {
private int user_id;
private String user;
private String password;
@Override
public String toString() {
return "User{" +
"user_id=" + user_id +
", user='" + user + '\'' +
", password='" + password + '\'' +
'}';
}
public int getUser_id() {
return user_id;
}
public void setUser_id(int user_id) {
this.user_id = user_id;
}
public String getUser() {
return user;
}
public void setUser(String user) {
this.user = user;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
这个类接收查询到的数据。
package com.example.mybatis.servlet;
import com.example.mybatis.mapper.UserMapper;
import com.example.mybatis.model.User;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
@WebServlet(name="user",value="/user")
public class UserServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String name = req.getParameter("name");
if(name==null||name.isEmpty()){
resp.getWriter().write("username can't be empty");
return;
}
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 获取 SqlSession
try (SqlSession session = sqlSessionFactory.openSession()) {
// 获取 Mapper 接口
UserMapper mapper = session.getMapper(UserMapper.class);
// 执行查询(注意:${name} 为拼接注入点,返回 List)
List<User> users = mapper.findUserByName(name);
// 输出结果
if (users != null && !users.isEmpty()) {
for (User user : users) {
resp.getWriter().println("ID: " + user.getUser_id());
resp.getWriter().println("username: " + user.getUser());
resp.getWriter().println("password: " + user.getPassword());
resp.getWriter().println("--------------------");
}
} else {
resp.getWriter().write("111111no");
}
} catch (Exception e) {
// 渗透测试场景:把报错信息回显出来,方便报错注入/看列名
e.printStackTrace();
resp.getWriter().write("failed: " + e.getMessage());
}
}
}
这段代码负责处理用户输入和数据库查询。
Hibernate #
样例代码:
先创建一个 hibernate.cfg.xml:
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<!-- 数据库连接配置 -->
<property name="hibernate.connection.driver_class">com.mysql.cj.jdbc.Driver</property>
<property name="hibernate.connection.url">jdbc:mysql://localhost:3306/phpstudy?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai</property>
<property name="hibernate.connection.username">root</property>
<property name="hibernate.connection.password">123456</property>
<!-- 数据库方言 -->
<property name="hibernate.dialect">org.hibernate.dialect.MySQL8Dialect</property>
<!-- 显示 SQL 语句 -->
<property name="hibernate.show_sql">true</property>
<!-- 自动更新数据库表结构 -->
<property name="hibernate.hbm2ddl.auto">update</property>
<!-- 映射实体类 -->
<mapping class="com.example.entity.User"/>
</session-factory>
</hibernate-configuration>
User.java 映射实体类:
package com.example.entity;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name="users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String username;
private String email;
public User() {}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
@Override
public String toString() {
return "User{id=" + id + ", username='" + username + "', email='" + email + "'}";
}
}
工具类:
package com.example.util;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
public class HibernateUtil {
private static final SessionFactory sessionFactory;
static {
try {
Configuration configuration = new Configuration().configure();
sessionFactory = configuration.buildSessionFactory();
} catch (Throwable ex) {
throw new ExceptionInInitializerError(ex);
}
}
public static SessionFactory getSessionFactory() {
return sessionFactory;
}
}
Servlet 类部分:
package com.example;
import com.example.entity.User;
import com.example.util.HibernateUtil;
import org.hibernate.Session;
import org.hibernate.query.Query;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
@WebServlet("/user")
public class UserQueryServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
// 获取参数(这里假设根据用户名查询)
String username = request.getParameter("username");
// 打开 Hibernate Session
Session session = HibernateUtil.getSessionFactory().openSession();
try {
// 修改后的 HQL 语句,直接拼接字符串
String hql = "FROM User WHERE username = '" + username + "'";
//String hql = "FROM User WHERE username=:username";
// 创建查询对象
Query<User> query = session.createQuery(hql, User.class);
// 执行查询
List<User> users = query.getResultList();
// 输出查询结果
out.println("<html><body>");
if (users.isEmpty()) {
out.println("<p>未找到匹配的用户。</p>");
} else {
for (User user : users) {
out.println("<p>" + user + "</p>");
}
}
out.println("</body></html>");
} catch (Exception e) {
e.printStackTrace();
out.println("<html><body><p>查询出错,请稍后重试。</p></body></html>");
} finally {
// 关闭 Session
session.close();
}
}
}
这里的代码是存在 SQL 注入的。在 Hibernate 中用预编译来写就是String hql = "FROM User WHERE username=:username"。
Fastjson #
简介 #
Fastjson 是阿里巴巴的开源库,用于对 JSON 格式的数据进行解析和打包。其实简单的来说就是处理 json 格式的数据。例如将 JSON 转换成一个类,或者是将一个类转换成一段 json 数据。Fastjson 是一个 Java 库,提供了 Java 对象与 JSON 相互转换。
序列化与反序列化 #
- 序列化方法
JSON.toJsonString(),返回字符串
JSON.toJSONBytes(),返回 byte 数组
- 反序列化方法
JSON.parseObject(),返回JsonObject
JSON.parse(),返回Object
JSON.parseArray(),返回JSONArray
将 JSON 对象转换为 java 对象:JSON.toJavaObject()
将 JSON 对象写入 write 流:JSON.writeJSONString()
- 常用
JSON.toJSONString()、JSON.parse()、JSON.parseObject()
代码demo #
序列化实现 #
首先导入 Fastjson 的依赖,这里使用 1.2.24 版本的:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
先定义一个 Student 类:
public class Student {
private String name;
private int age;
public Student() {
System.out.println("构造函数");
}
public String getName() {
System.out.println("getName");
return name;
}
public void setName(String name) {
System.out.println("setName");
this.name = name;
}
public int getAge() {
System.out.println("getAge");
return age;
}
public void setAge(int age) {
System.out.println("setAge");
this.age = age;
}
}
然后用JSON.toJSONString()来序列化:
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
public class StudentSerialize {
public static void main(String[] args) {
Student student = new Student();
student.setName("solowalker");
String jsonString = JSON.toJSONString(student, SerializerFeature.WriteClassName);
System.out.println(jsonString);
}
}
输出:
构造函数
setName
getAge
getName
{"@type":"com.example.Student","age":0,"name":"solowalker"}
很显然String jsonString = JSON.toJSONString(student, SerializerFeature.WriteClassName);这一句是非常关键的。
我们关注一下这里的参数:
第一个参数是 student,就是我们要序列化的对象。第二个参数是SerializerFeature.WriteClassName,是JSON.toJSONString()中的一个设置属性值,设置之后在序列化时会多写入一个@type,即会写上被序列化的类名,type 可以指定反序列化的类,并且反序列化时调用其getter/setter/is方法。
- Fastjson 接受的 JSON 可以通过@type字段来指定该JSON应当还原成何种类型的对象,在反序列化的时候方便操作。
// 设置了SerializerFeature.WriteClassName
构造函数
setName
setAge
getAge
getName
{"@type":"com.example.Student","age":6,"name":"John"}
// 未设置SerializerFeature.WriteClassName
构造函数
setName
setAge
getAge
getName
{"age":6,"name":"John"}
反序列化实现 #
调用JSON.parseObject():
package com.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
public class StudentUnserialize {
public static void main(String[] args) {
String jsonString="{\"@type\":\"com.example.Student\",\"age\":0,\"name\":\"solowalker\"}";
Student student = JSON.parseObject(jsonString, Student.class, Feature.SupportNonPublicField);
System.out.println(student);
System.out.println(student.getClass().getName());
}
}
输出:
构造函数
setAge
setName
com.example.Student@5b37e0d2
com.example.Student
总结一下反序列化时 getter 和 setter 的调用情况:
- 当序列化时有
SerializerFeature.WriteClassName参数,即 json 中有@type时,反序列化时parse就会调用setter,parseObject就是调用getter和setter - 当反序列化时指定
Class,parseObject(text, Class)主要会调用setter,特定情况下会调用getter
JNDI #
简介 #
JNDI 全称 Java Naming and Directory Interflace(Java 命名和目录接口),也就是一个名字对应一个 Java 对象,一个字符串对应一个对象。
JNDI 是一组应用程序接口,为开发人员查找和访问各种资源提供了统一的通用接口,可以用来定义用户、网络、机器、对象和服务等各种资源。JNDI 支持的服务主要有:DNS、LDAP、CORBA、RMI 等。
关于 JNDI 的攻击方式后续会在 Java 安全专题里更新。
Log4j #
简介 #
Log4j 是 Java 编程中最流行的一个日志记录工具。简单来说就是开发人员在代码里“埋点”,让程序在运行过程中把各种状态、错误、信息打印出来(比如输出到控制台或文件),方便开发人员排查问题和监控系统。
在大型企业级应用中,日志系统是必备的。Log4j 因为性能好、功能强、配置灵活,几乎成了 Java 日志的事实标准,被全球数以百万计的应用使用。
样例代码 #
package com.example.log4jdemo;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class Log4jTest {
private static final Logger log = LogManager.getLogger(Log4jTest.class);
public static void main(String[] args) {
log.error("hello");
String code="${java:os}";
log.error("{}",code);
}
}
这段代码简单利用了 Log4j 的漏洞打印了系统的基本信息。
XStream #
XStream 是一个 Java 库,核心功能是把 Java 对象转换为 XML、把 XML 转换成 Java 对象,就是序列化和反序列化的过程。
样例代码 #
package com.example.xstreamdemo;
import java.io.ObjectInputStream;
import java.io.Serializable;
public class Car implements Serializable {
private String name;
private int price;
public Car(String name, int price) {
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
private void readObject(ObjectInputStream s) throws Exception{
s.defaultReadObject();
System.out.println("print car");
Runtime.getRuntime().exec("calc");
}
}
package com.example.xstreamdemo;
import com.thoughtworks.xstream.XStream;
public class XStreamTest {
public static void main(String[] args) {
Car ferrari = new Car("Ferrari", 4000000);
XStream xStream = new XStream();
String xml = xStream.toXML(ferrari);
System.out.println(xml);
}
}
输出:
<com.example.xstreamdemo.Car serialization="custom">
<com.example.xstreamdemo.Car>
<default>
<price>4000000</price>
<name>Ferrari</name>
</default>
</com.example.xstreamdemo.Car>
</com.example.xstreamdemo.Car>
反序列化:
package com.example.xstreamdemo;
import com.thoughtworks.xstream.XStream;
public class XStreamSer {
public static void main(String[] args) {
XStream xStream = new XStream();
String payload="<com.example.xstreamdemo.Car serialization=\"custom\">\n" +
" <com.example.xstreamdemo.Car>\n" +
" <default>\n" +
" <price>4000000</price>\n" +
" <name>Ferrari</name>\n" +
" </default>\n" +
" </com.example.xstreamdemo.Car>\n" +
"</com.example.xstreamdemo.Car>";
xStream.fromXML(payload);
}
}
运行之后会弹出计算器,说明 XStream 在反序列化时会调用readObject()方法。
shiro #
Apache Shiro 是一个强大且易用的 Java 安全框架,它的目标是让应用程序的安全控制变的简单直观。它关注的是应用安全的四大基石,即认证、授权、会话管理和加密。
Shiro 可以轻松实现以下功能:
- 认证 (Authentication):也就是“登录”,验证用户的身份。
- 授权 (Authorization):也就是“访问控制”,判断用户是否有权限做某件事。
- 会话管理 (Session Management):即使在非 Web 环境(如命令行程序)中,也能管理用户会话。
- 加密 (Cryptography):提供简单的 API 来使用加密算法保护数据。
- 其他功能:它还支持“记住我”、缓存、多线程并发等特性。
SpringBoot #
SpringBoot 是 Java 生态中最流行的应用开发框架(基于 Spring 框架),核心目标是简化 Spring 应用的初始搭建和开发过程。
在 Spring Boot 出现之前,用 Spring 开发一个 Web 项目,你需要:
- 配置大量的 XML 文件
- 手动管理各种 Jar 包的版本依赖
- 部署到外置的 Tomcat 服务器
- 配置数据源、事务管理器等基础设施
这些工作繁琐且容易出错。Spring Boot 的核心理念是**“约定优于配置”**——它给你一套默认的配置,只要你遵循约定,几乎不需要额外配置就能直接运行。
模板引擎 #
Java 中用到的模板引擎主要是 Thymeleaf、Freemarker、Velocity。Java 的模板引擎同样存在 SSTI 漏洞。
关于这些模板引擎的漏洞后续专题更新。
Actuator #
Spring Boot Actuator 是 Spring Boot 框架中一个非常重要的模块,它为应用提供了一系列生产就绪的功能,比如监控、管理和查看应用内部状态 。你可以把它看作是应用运行时的“体检中心”和“控制面板”,让你能轻松了解应用的健康状况、性能指标和配置信息 。
简单来说,Actuator 通过暴露一系列 HTTP 端点(或 JMX MBean)来提供功能。这些端点就像一个个“接口”,通过访问它们就能获取或操作应用的内部信息 。
| 端点分类 | 常用端点示例 | 功能介绍 |
|---|---|---|
| 应用健康与状态 | /health, /info | 检查应用是否存活、就绪,以及获取应用的自定义信息,是 Kubernetes 探针的基础 。 |
| 性能与度量指标 | /metrics, /threaddump, /heapdump | 获取 JVM 内存使用、CPU 负载、线程状态、堆转储等关键指标,用于性能分析 。 |
| 应用配置与环境 | /env, /configprops, /beans | 查看应用所有的环境变量、配置属性以及 Spring 容器中的所有 Bean,方便排查配置问题 。 |
| 操作与控制 | /shutdown, /loggers | 允许远程关闭应用(需谨慎开启),或在运行时动态调整日志级别 。 |
Swagger #
Swagger 是当下比较流行的实时接口文档生成工具。接口文档是当前前后端分离项目中必不可少的工具,在前后端开发之前,后端要先出接口文档,前端根据接口文档来进行项目的开发,双方开发结束后再进行联调测试。
在前后端分离的开发模式中,它能解决以下痛点:
- 消除信息孤岛:后端修改了接口(如改变了参数名),Swagger文档会实时刷新,前端能立刻看到最新定义,无需口头沟通或等待文档更新。
- 降低测试成本:Swagger-UI页面提供“Try it out”按钮,开发者无需使用Postman,在浏览器中就能直接填充参数、发送请求、查看JSON返回结果。
- 规范接口设计:通过强类型约束和注解,倒逼开发者设计出更清晰的入参和出参结构。
JWT 令牌 #
JWT(JSON Web Token)是由服务端用加密算法对信息签名来保证其完整性和不可伪造;Token 中可以包含所有必要信息,这样服务端就不需要保存任何关于用户或会话的信息;JWT 用于身份认证、会话维持等,由三部分组成:header、payload 与 signature。
JWT 的安全问题一般就两点:
- 目标使用的空加密
- 爆破密钥,但是复杂的密钥几乎无法爆破
Spring Security #
Spring Security 是 Spring 生态中为应用提供**身份认证(你是谁)和授权(你能干什么)**能力的基石。
Spring Security 的核心本质,就是一套过滤器链(Filter Chain)。一个请求进来,需要经过层层过滤器的检查。其中最重要的两个核心概念是:
- 认证(Authentication):验证你是谁。常见方式是用户名+密码、手机号验证码、JWT(JSON Web Token)令牌等。
- 授权(Authorization):验证你能做什么。即认证通过后,判断你是否有权限访问某个资源或执行某个操作(比如普通用户不能调用管理员的删除接口)。
这两个概念在代码中分别对应两个核心接口:
Authentication:代表当前用户的身份凭证。GrantedAuthority:代表用户拥有的权限(通常表现为角色或权限字符串)。
SnakeYaml #
在Java生态里,SnakeYAML 是最主流、最成熟的 YAML 解析库。它几乎成了“在Java里处理YAML”的代名词,尤其是在 Spring Boot 这类广泛使用 YAML 做配置文件的框架中,它的身影随处可见。
简单来说,SnakeYAML 是一个 Java 库,专门用来在 YAML 格式和 Java 对象之间进行转换。YAML 是一种对人极其友好的数据序列化格式,常被用来写配置文件,它比 XML 和 Properties 文件都更清晰易读。
序列化时会调用对象的构造方法、getter和setter;反序列化时会调用构造方法和setter。
SnakeYaml 有一个 SPI 机制,解决了 JNDI 注入无法绕过的情况,存在高危利用。