ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JavaEE核心组件详解:Servlet、Filter、Listener与安全开发实战

JavaEE核心组件详解:Servlet、Filter、Listener与安全开发实战 现在很多程序员学Java Web开发第一步就是Spring Boot一个注解加一个依赖就跑起来根本不知道背后发生了什么。但一旦开始做代码审计、接手老项目、或者排查线上一个莫名其妙的乱码和CSRF攻击时你才会发现Servlet、Filter、Listener、JSP这些JavaEE里看似“过时”的东西其实一直活在生产环境的底层。我最近正好跟着一套安全向的Java开发课程把day32~40这部分集中啃了一遍从VS Code配置JavaEE语言环境开始到完成一个带登录鉴权和图书管理功能的Web应用。这一篇就把这整个过程中的环境搭建、核心组件协作、分层代码组织以及开发时需要特别留神的几个安全问题一次性梳理清楚。如果你是刚接触JavaEE、又被各种视频课程里的术语劝退的话这篇文章可以当作一条相对平滑的路径我先解释清楚这块内容的定位再带你一步步配好VS Code环境然后通过一个可运行的登录图书管理项目把Servlet、Filter、Listener、MVC分层和安全编码串起来。看完之后你应该能独立把一个war包部署到Tomcat上也会明白为什么很多安全报告里反复出现那些Java Web漏洞。1. 一上来学JavaEE心态先纠正过来1.1 JavaEE从来不是一门语言而是一组“契约”很多人会把JavaEE当成一个像Python、Go那样的具体技能其实它是一整套企业级开发规范的集合。Servlet、JSP、EJB、JMS、JDBC、JTA这些都是JavaEE体系里的成员只不过后来Spring把其中大部分底层的活都封装完了导致很多新人在写Spring Boot时不觉得自己在用JavaEE。但底层逻辑没有变。Spring MVC的核心还是DispatcherServlet它本身就是一个ServletSpring Security的过滤器链本质上就是在一层又一层Filter上做文章Spring Boot内嵌的Tomcat、Jetty也依然是一个Servlet容器。也就是说不管外层包装得多花哨JavaWeb运行时还是离不开这些JavaEE规范。我用一个类比来理解这套东西Servlet像快递柜里的格子规定好每个包裹怎么存取Filter则是快递柜门口的检查员所有包裹进出都要过一遍Listener是柜子里的感应器开门、放东西、关门都会触发通知。三者的组合能覆盖一个Web请求从进来到最终离开的完整生命周期。1.2 为什么这套课程把day32到day40单独拎出来讲day32到day40是这套课程里非常特殊的一段。前面的内容大多在铺垫网络基础、HTTP协议、数据库操作到这里才开始真正把“请求从浏览器发出Java代码怎么接住并处理”这件事串起来。课程虽然顶着“安全”的名头但这个阶段的重点并不是攻击技巧而是让开发者知道自己写的JavaEE代码会引入哪些漏洞。比如一条SQL语句如果用的是字符串拼接那注入风险就摆在明面上如果没有统一的编码Filter中文乱码和潜在的编码绕过就会烦死人如果文件上传接口没有校验扩展名和路径那几乎等于给服务器开了一个后门。对于想做代码审计的人来说这一段的产出非常直接你得能看懂Servlet里一条SQL是怎么被拼出来的才理解安全报告里的“注入点在doPost方法第47行”到底是什么意思。我自己的目标也在这个阶段做了调整。之前我一直觉得能把Spring Boot项目跑起来就算会Java Web但遇到老项目的时候打开全是JSP和Servlet的代码连从哪下手都不知道。补完这一块之后再回头看那些框架源码明显顺畅了很多。1.3 我在这一阶段前已经具备的基础如果你是零基础直接切入我建议先准备三个前置点能用Java写普通的类和方法理解HTTP的GET和POST区别会看最基本的数据库表结构。不一定非要多熟练但至少要能读代码。这套内容本身讲JavaEE不太会回头教Java语法。我的实际节奏是每天一到两节课每节课后都不急着往下看而是先把示例代码手敲一遍改一改参数自己造几个错误来观察现象。刚开始很容易把doGet和doPost写反或者在web.xml里漏掉servlet-mapping导致404。这些问题看起来很基础但恰恰是理解请求路径映射的最好机会。如果你已经有一点Spring Boot基础那反而要刻意“忘掉”自动配置。用纯Servlet写接口的时候你会发现自己要多写很多行代码这其实是一件好事控制器的路由、参数的读取、响应的返回每一步都变得可见了。2. VS Code配置JavaEE语言环境从下载到第一个Servlet打印Hello World2.1 为什么最后选了VS Code而不是IDEA现在社区里主流的JavaEE开发工具还是IDEA这个没得争。但我个人选VS Code当主力原因很简单轻而且平时还得写脚本、看前端代码不想为了一个项目常年开着IDE。JavaEE开发在VS Code里完全可行尤其是配合Maven之后编译、打包、依赖管理都跟在IDEA里差不多。关键是把VS Code当成一个增强的编辑器来用别去追求图形化点按钮创建Servlet而是用命令行生成项目结构、用配置文件声明依赖。这样反而能让人更清楚工程的骨架是什么。如果你后面打算长期做Java后端IDEA还是有它的优势但如果你是学习阶段、或者需要快速验证一个Web应用VS Code这套方案足够。2.2 一步一步装齐JDK、Maven和Tomcat首先是JDK。我自己选了JDK 11因为兼容性比较好课程里涉及的Servlet 4.0、Tomcat 9都能稳定支持。如果你要跑的Tomcat版本在8.5以下建议还是用JDK 8。安装完之后在命令行执行java -version看到类似openjdk version 11.0.x的输出就行。然后装Maven。Maven的作用是管理依赖和构建这里提供一个可靠的下载源解压后配置环境变量到bin目录。如果下载依赖太慢记得改一下settings.xml里的镜像仓库。Windows下常见的坑是每次启动终端都要重新set MAVEN_HOME这个别嫌麻烦直接在系统环境变量里加好一劳永逸。接着是Tomcat。我用的是Tomcat 9它对应Servlet 4.0规范符合课程这一阶段的设定。解压到你习惯的目录后进入bin目录执行startup.bat启动一次试试浏览器打开http://localhost:8080能看到Tomcat首页就算成功。这里建议把CATALINA_HOME环境变量也配上后面VS Code里面识别会方便很多。2.3 装哪些VS Code扩展settings.json怎么设VS Code里面做JavaEE开发最少要装两套东西。一套是Java语言支持直接搜Extension Pack for Java红帽出的包含语言服务、调试器、Maven支持。另一套是Tomcat运行插件搜索Tomcat for Java装好之后就能在侧边栏管理本地Tomcat也可以直接把war包部署上去。装完扩展后我推荐手动配置一下settings.json避免后续环境变量识别出问题。下面这份是我一直在用的配置{ java.configuration.updateBuildConfiguration: automatic, java.configuration.runtimes: [ { name: JavaSE-11, path: C:\\Program Files\\Java\\jdk-11, default: true } ], maven.terminal.customEnv: [ { environmentVariable: JAVA_HOME, value: C:\\Program Files\\Java\\jdk-11 } ] }如果你的JDK装在其他路径把path和value换成自己的就行。有个小细节java.configuration.updateBuildConfiguration: automatic一定要开否则改了pom.xml之后扩展不会自动更新依赖代码里会一直提示找不到javax.servlet相关类。2.4 用Maven骨架创建Web工程并配置Tomcat运行打开VS Code的终端执行下面的命令mvn archetype:generate -DgroupIdcom.demo -DartifactIdbookmanager -DarchetypeArtifactIdmaven-archetype-webapp -DinteractiveModefalse这个命令会生成一个最简单的Maven Web工程目录结构长这样bookmanager |-- pom.xml -- src -- main |-- resources -- webapp |-- WEB-INF | -- web.xml -- index.jsp打开pom.xml加上Servlet和JSTL依赖。注意javax.servlet-api的scope要设为provided因为Servlet容器里已经有这些类了打包的时候不需要塞进war包。dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency接下来在src/main/java下面新建包com.demo.controller写第一个Servletpackage com.demo.controller; 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(/hello) public class HelloServlet extends HttpServlet { Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType(text/html;charsetUTF-8); resp.getWriter().write(h1Hello JavaEE/h1); } }如果想直接用VS Code的Tomcat插件运行可以右键项目名选择Run on Tomcat。插件会自动执行mvn package生成war包然后部署到Tomcat。启动之后浏览器访问http://localhost:8080/bookmanager/hello能看到页面上出现“Hello JavaEE”就算成功了。2.5 首次启动踩过的坑我在这里踩的第一个坑是URL路径不对。war包的名字是bookmanager所以访问路径必须带上这个项目上下文否则会404。Tomcat插件也会在侧边栏展开项目名那一层显示context path注意看一眼就行。第二个坑是中文乱码。Tomcat 9的控制台和代码里的UTF-8在Windows的默认编码下经常会打架。一个偏方是执行startup.bat前先在当前终端执行chcp 65001切到UTF-8代码页或者在启动脚本里加上JAVA_TOOL_OPTIONS-Dfile.encodingUTF-8。代码里则统一用request.setCharacterEncoding(UTF-8)后面写Filter的时候也可以统一处理。第三个坑是javax.servlet.http.HttpServlet在VS Code里标红通常是扩展还没从Tomcat安装路径里读取到Servlet API。检查一下是不是把javax.servlet-api依赖加上了以及scope是不是provided。如果加上了还标红就在VS Code命令面板里执行Java: Clean Java Language Server Workspace让语言服务重置一次。3. 三个“老伙计”的协作Servlet、Filter、Listener3.1 Servlet处理请求的最小可用代码Servlet是JavaEE Web应用里最核心的组件。它接收HTTP请求、读取参数、执行业务逻辑、返回响应。每一个WebServlet(/路径)注解或者web.xml里的servlet-mapping都是告诉容器“这个URL归谁管”。以登录请求为例一个简单的LoginServlet大概是这样的package com.demo.controller; 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(/login) public class LoginServlet extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username req.getParameter(username); String password req.getParameter(password); if (admin.equals(username) 123456.equals(password)) { req.getSession().setAttribute(admin, username); resp.sendRedirect(index.jsp); } else { req.setAttribute(error, 用户名或密码错误); req.getRequestDispatcher(login.jsp).forward(req, resp); } } }这段代码很好地说明了Servlet的职责从request里取数据做最简逻辑再决定是重定向还是转发。你可能会觉得这段代码很稚嫩但它是后面所有分层的起点。如果一个Servlet里写了大量SQL和HTML拼接那就是所谓的“面条代码”这也是安全审计里最让人头疼的东西。Servlet生命周期里有几个方法值得关注。init()只执行一次适合初始化连接池之类重资源service()会根据请求类型自动路由到doGet或者doPost所以一般不需要重写destroy()在Servlet从容器卸载时调用通常用来释放资源。我个人建议在调试阶段重写init()打一行日志能帮助你感知Servlet的创建时机。3.2 Filter链从字符编码到登录鉴权Filter是位于Servlet之前的拦截器。一个请求到了容器之后要先经过所有匹配的Filter最后才会进入Servlet。Filter最常见的用法有三种统一设置请求/响应的编码、做登录鉴权、记录日志。字符编码Filter几乎每个项目都该有package com.demo.filter; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import java.io.IOException; WebFilter(/*) public class CharacterEncodingFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { request.setCharacterEncoding(UTF-8); response.setCharacterEncoding(UTF-8); chain.doFilter(request, response); } }注意一定要在chain.doFilter(request, response)之前设置编码因为一旦请求参数被读取再修改编码就不生效了。登录鉴权Filter是另一个高频例子。假设管理后台的所有URL都在/admin/*下面可以这样写package com.demo.filter; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; WebFilter(/admin/*) public class AdminAuthFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpReq (HttpServletRequest) request; HttpServletResponse httpResp (HttpServletResponse) response; HttpSession session httpReq.getSession(false); if (session null || session.getAttribute(admin) null) { httpResp.sendRedirect(httpReq.getContextPath() /login.jsp); return; } chain.doFilter(request, response); } }这段代码的意思是如果当前会话里没有admin这个属性说明没登录直接跳回登录页否则继续放行。多个Filter同时存在时执行顺序是一个值得注意的点。如果用注解配置顺序不是严格按照类名字来的后面被加载的Filter排序不一定符合直觉。如果顺序敏感比如必须先编码Filter再鉴权Filter我建议直接把Filter定义在web.xml里因为那里的filter-mapping顺序是明确有效的。3.3 Listener应用启动和用户会话的“监听哨”Listener用来监听Web应用里各种事件。最常见的是ServletContextListener和HttpSessionListener。前者可以在应用启动时初始化东西后者可以统计在线人数。下面是一个简单的在线人数统计Listenerpackage com.demo.listener; import javax.servlet.ServletContext; import javax.servlet.annotation.WebListener; import javax.servlet.http.HttpSessionEvent; import javax.servlet.http.HttpSessionListener; WebListener public class OnlineCountListener implements HttpSessionListener { Override public void sessionCreated(HttpSessionEvent se) { ServletContext ctx se.getSession().getServletContext(); Object online ctx.getAttribute(online); int count online null ? 0 : (Integer) online; ctx.setAttribute(online, count 1); } Override public void sessionDestroyed(HttpSessionEvent se) { ServletContext ctx se.getSession().getServletContext(); Integer count (Integer) ctx.getAttribute(online); if (count ! null) { ctx.setAttribute(online, count - 1); } } }这里有一个比较容易忽视的细节应用启动时ServletContext里没有online这个属性所以取出来是null。写成Integer count (Integer) ctx.getAttribute(online)之后多了就加少了就减逻辑上不会报错但如果没做空判断第一次访问就把线上人数变成null页面显示就会异常。我在实际开发里对Listener的使用不算多主要是因为它解决的是某些特定场景的需求。但理解它的存在很重要一个Web应用的启动、停止、Session创建、销毁都是可以编程感知的。3.4 一次请求从浏览器到响应的完整流转把Servlet、Filter、Listener放在一起看一次请求的生命周期就清晰了。假设用户访问的是/admin/book/list链路大致是容器检查对应的Application Filter链。编码Filter先执行设置请求和响应编码。鉴权Filter判断Session里是否有登录信息没有则重定向。请求最终进入BookListServlet的doGet或doPost方法。Servlet读取参数调用Service和DAO完成数据查询。Servlet把数据放到request属性转发到JSP页面。JSP渲染结果经过响应Filter返回给浏览器。Filter能访问所有请求Servlet只处理自己映射的路径Listener则在整个生命周期里感知各种事件。三者分得清清楚楚这也是JavaEE设计上最经典的地方。组件职责典型场景Servlet接收请求调用业务代码控制视图跳转登录、列表、表单提交Filter请求前后统一处理字符编码、登录鉴权、日志记录Listener监听应用和Session事件启动初始化、在线人数统计4. 从Servlet乱堆到分层DAO、Service、Controller的实战重构4.1 最初图省事的写法为什么撑不到第二个功能刚开始写JavaEE的时候很容易把SQL直接写在Servlet里。一个方法动辄五六十行查完数据库再拼HTML输出到resp.getWriter()。这样写第一个接口很快第二个接口也能凑合但到第三个接口碰上复用时就崩了改一个数据库表结构所有相关Servlet都要动想加一个单元测试Servlet依赖的容器环境很难模拟。分层并不是什么高级理念它就是为了让代码“换起来不疼”。我最终的目标是三层结构Controller层Servlet负责接收请求和返回响应Service层处理业务逻辑DAO层专门访问数据库。JSP只负责展示不在里面写大段Java代码。4.2 用连接池管理数据库连接之前我只会在Servlet里DriverManager.getConnection每次请求都新建一个连接请求结束再关闭。连接创建的开销其实不小高并发下还会让数据库文件句柄迅速膨胀。连接池是这个场景的标准解法。我用的是Druid因为它有监控、性能也不错。做一个简单的工具类package com.demo.util; import com.alibaba.druid.pool.DruidDataSource; import javax.sql.DataSource; public class JdbcUtils { private static DataSource dataSource; static { try { DruidDataSource ds new DruidDataSource(); ds.setUrl(jdbc:mysql://localhost:3306/bookdb?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/Shanghai); ds.setUsername(root); ds.setPassword(your_password); ds.setInitialSize(5); ds.setMaxActive(20); ds.setMaxWait(60000); dataSource ds; } catch (Exception e) { throw new RuntimeException(初始化连接池失败, e); } } public static DataSource getDataSource() { return dataSource; } }要注意serverTimezoneAsia/Shanghai这个参数不设置的话新版JDBC驱动在连接MySQL时可能直接报时区错误。URL里的characterEncodingUTF-8也必须和之前编码Filter的配置保持一致否则中文又是一个坑。4.3 写一个BaseDAO和BookDAO有了连接池之后DAO层就很容易写了。我用Apache Commons DbUtils简化JDBC样板代码里面自带QueryRunner和结果集转Bean的功能。BaseDAO可以这样定义package com.demo.dao; import com.demo.util.JdbcUtils; import org.apache.commons.dbutils.QueryRunner; import org.apache.commons.dbutils.handlers.BeanListHandler; import java.sql.SQLException; import java.util.List; public class BaseDAOT { private ClassT clazz; public BaseDAO(ClassT clazz) { this.clazz clazz; } public ListT queryList(String sql, Object... params) throws SQLException { QueryRunner runner new QueryRunner(JdbcUtils.getDataSource()); return runner.query(sql, new BeanListHandler(clazz), params); } }BookDAO继承BaseDAO只要写上自己的查询方法package com.demo.dao; import com.demo.entity.Book; import java.sql.SQLException; import java.util.List; public class BookDAO extends BaseDAOBook { public BookDAO() { super(Book.class); } public ListBook findAll() throws SQLException { String sql SELECT id, name, author, price FROM book; return queryList(sql); } }这里点名一个规范SQL里不要拼接参数。SELECT id, name, author, price FROM book WHERE name ?这种占位符写法才是在DAO层防范SQL注入的基础。BaseDAO里Object... params拿到参数后QueryRunner会帮我们正确设置到PreparedStatement中。4.4 JSP页面里别写JavaEL和JSTL的正确用法JSP早期允许直接在页面里写% for(...) { %看起来效率很高但维护成本极低。我强烈建议把JSP当纯模板用数据由Servlet提前放到request或session里页面只负责取值和循环。Servlet端转发到JSP时把列表放进requestreq.setAttribute(books, bookService.findAll()); req.getRequestDispatcher(/WEB-INF/jsp/book-list.jsp).forward(req, resp);JSP端用EL表达式和JSTL来渲染% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html head title图书列表/title /head body table border1 tr th编号/th th书名/th th作者/th th价格/th /tr c:forEach items${books} varbook tr td${book.id}/td td${book.name}/td td${book.author}/td td${book.price}/td /tr /c:forEach /table /body /html注意JSP文件我放在了WEB-INF目录下面。因为直接放在webapp下的时候用户可以不经过Servlet直接访问JSP这样请求里的业务校验就失效了。放到WEB-INF后只能通过Servlet转发来访问这条路才是可控的。从DAO到Servlet再到JSP整个分层就串起来了。Controller变得很薄DAO只负责数据Service负责交易和业务规则JSP负责展示。后面想换成Spring MVC也方便Dao和Service几乎可以原封不动搬过去。5. 安全视角扫一遍JavaEE开发中最常见的五个漏洞点5.1 SQL注入唯一的解法是参数化查询写JavaEE接口时最危险的句子就是字符串拼接SQL。比如String sql SELECT * FROM user WHERE username username AND password password ;如果username被传成 OR 11整个条件就变成恒真等于绕过了登录。更严重的是有些数据库驱动支持多语句执行攻击者可以在参数里塞一条DELETE语句结果就不只是脱库而是直接删表。正确写法是PreparedStatement。哪怕你用原生JDBC也应当这样String sql SELECT * FROM user WHERE username ? AND password ?; PreparedStatement ps conn.prepareStatement(sql); ps.setString(1, username); ps.setString(2, password);?占位符会把参数当纯数据而不是SQL结构的一部分。这也是我在前面写BaseDAO时坚持用QueryRunner加可变参数的原因。代码审计里看到Statement.executeQuery(String sql)、Statement.execute(String sql)几乎可以直接标红。5.2 XSS不只是“过滤”而是输出编码XSS的本质是服务端把用户输入的数据当成HTML标签输出了。比如用户注册了一个昵称叫scriptalert(1)/script页面用${user.nickname}直接渲染这段脚本就会执行。防御的第一道防线是输出编码。JSTL的c:out默认会对HTML特殊字符转义应该优先使用c:out value${user.nickname}/而不是直接写${user.nickname}。如果业务上确实需要富文本你得用白名单过滤器比如OWASP Java HTML Sanitizer而不是自己写正则去replace。黑名单永远不可靠因为浏览器对实体编码、大小写、事件属性的解析方式太多样了。5.3 CSRF同步令牌在Filter里的落地CSRF利用的是浏览器自动携带Cookie特性用户登录了银行系统又访问了一个恶意站点恶意站点发起的请求会带上银行的Cookie银行后端不知道请求来源有问题照样执行转账操作。防御CSRF最简单有效的方式是同步令牌。用户登录后在Session里保存一个随机token每次表单提交时把token作为隐藏字段带上后端在Filter里校验这个token和Session里的是否一致。登录成功生成tokenString token UUID.randomUUID().toString(); req.getSession().setAttribute(csrf_token, token); req.setAttribute(csrf_token, token);表单里加隐藏字段input typehidden namecsrf_token value${csrf_token} /Filter里过滤所有POST请求if (POST.equals(req.getMethod())) { HttpSession session req.getSession(false); String sessionToken session null ? null : (String) session.getAttribute(csrf_token); String requestToken req.getParameter(csrf_token); if (sessionToken null || !sessionToken.equals(requestToken)) { resp.sendError(403); return; } }这样即使恶意站点头表单发出了请求也拿不到你Session里的随机token请求就会被拦下。5.4 文件上传和路径穿越文件上传接口如果只检查了文件的Content-Type很容易被绕过。比如测试工具把image/png改成application/octet-stream或者直接用一个jpg后缀的webshell伪装文件。真正要做的检查是扩展名白名单并且不止看文件名后缀还要验证文件内容头部是否匹配。另一个容易忽略的是路径穿越。很多人在Servlet里写String path /upload/ filename; FileOutputStream fos new FileOutputStream(path);如果filename带着../../文件就能被写到上层目录甚至写到启动脚本目录里。有效做法是上传目录放在webroot之外用UUID重命名文件再将文件扩展名白名单化。5.5 Session安全问题的小提醒Session是JavaEE里一个高频目标。登录成功后要调用一下旧Session失效然后创建一个新的防止Session Fixation攻击HttpSession oldSession req.getSession(false); if (oldSession ! null) { oldSession.invalidate(); } HttpSession newSession req.getSession(true); newSession.setAttribute(admin, username);Cookie属性也要设置HttpOnly和Secure。HttpOnly能防止JavaScript读取Cookie里的Session IDSecure让Cookie只在HTTPS下传输。Filter换Session的时候这些属性也得同步更新。Session超时时间不要设太长管理后台建议15到20分钟。超时时间越长Session被窃取后的有效攻击窗口就越大。漏洞类型产生位置关键防御写法SQL注入DAO层SQL拼接PreparedStatement / 占位符XSSJSP输出用户内容JSTL c:out / 白名单过滤CSRF表单提交同步令牌 Filter校验文件上传上传接口扩展名白名单 UUID重命名会话固定登录逻辑登录后invalidate旧Session6. 实战复盘从部署调试到收尾的个人体会6.1 热部署与日志别让每次重启消耗掉耐心纯Servlet项目改一行代码就要重启Tomcat很影响学习效率。开发阶段可以在Tomcat里把Context的reloadable设为true这样类文件变更后容器会自动重新加载。VS Code的Tomcat插件也提供了一个更新按钮相当于手动触发reload。但注意这个配置不能带到生产环境。生产环境的reloadable必须关掉否则任意一个小改动都可能触发大规模上下文重载请求全部超时。日志方面用System.out打印消息在控制台看看还行项目稍微大一点就很难定位。建议尽早使用SLF4J的Simple实现或者配置Log4j2。输出里最好带上请求路径和耗时比如在Filter里记录一条req.getRequestURI()加耗时日志排错会顺手很多。6.2 调优连接池参数和Tomcat线程池连接池参数不是越大越好。数据库连接过多反而会造成数据库线程切换和锁竞争加剧。我一般按并发量来估同时在线200用户连接池maxActive设为20就够了。initialSize设5让启动时有几个连接预热。maxWait设60000毫秒拿不到连接时等待一分钟再报错避免应用直接卡死。Tomcat自身的线程池也需要了解。默认情况下maxThreads是200如果请求量大可以结合CPU核数适当调整但别盲目设置成几千。线程多了之后上下文切换带来的开销比等待数据库返回还大。6.3 学完这段后我对“安全开发”的理解这段课程给我最大的收获不是记住了几个组件API而是建立了一个“请求链路思维”。以前看安全报告看到漏洞在/admin/upload我会觉得很抽象现在我能立刻想到这条请求经过哪些FilterServlet里是怎么读参数的DAO层是怎么处理SQL的文件到底落在了哪个目录。安全开发并不是要在写完代码后额外做一遍什么神秘测试而是在写每一行代码时就想清楚它会不会被恶意输入影响。JavaEE里这些“老组件”本身没什么问题问题往往出在开发者图省事字符串拼接SQL、不过滤直接渲染、上传文件不校验。当你把前面的步骤都做规范很多漏洞在源头就消失了。如果你也在学JavaEE我强烈建议不要只停留在跑通而是每学一个组件就故意写一个有漏洞的版本和一个安全版本对比一下差别。这个过程比看十篇博客都有用。day32~40这一段是我过去这段时间里收获最大的一次补课它让我真正看懂了从request到response之间那些“看不见的代码”。
返回列表