Servlet处理前端数据踩坑大全:从乱码、404到SQL注入,我的避坑实战记录
Servlet开发避坑实战从乱码防御到SQL注入防护第一次用Servlet写登录功能时我对着浏览器里显示的???乱码和不断跳出的404页面差点崩溃。更可怕的是当我在搜索框输入 or 11 --时居然直接看到了所有用户数据——这让我意识到教科书式的Servlet示例代码在实际开发中藏着多少陷阱。本文将分享我在Servlet开发中踩过的典型深坑及解决方案这些经验大多来自线上事故后的深夜调试。1. 中文乱码从表象到本质的编码战争很多开发者遇到乱码的第一反应是机械地加上request.setCharacterEncoding(UTF-8)但当你的页面依然显示锟斤拷时问题往往比想象中复杂。真正的编码防御需要建立三层防护体系第一层请求编码拦截// 必须放在所有getParameter调用之前 request.setCharacterEncoding(UTF-8);这个经典方法对POST有效但对GET请求无能为力。我曾遇到一个案例GET参数经过Nginx转发后出现乱码最终发现需要在Tomcat的server.xml中配置Connector URIEncodingUTF-8 ... /第二层响应编码加固response.setContentType(text/html;charsetUTF-8); response.setCharacterEncoding(UTF-8);这里有个隐藏陷阱setContentType必须包含charset定义否则某些浏览器会忽略后续的编码设置。更稳妥的做法是用过滤器统一处理WebFilter(/*) public class EncodingFilter implements Filter { public void doFilter(...) { request.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8); chain.doFilter(request, response); } }第三层前端编码同步!-- 必须与后端编码一致 -- meta http-equivContent-Type contenttext/html; charsetUTF-8当使用AJAX时需要额外设置$.ajaxSetup({ contentType: application/x-www-form-urlencoded;charsetUTF-8 });典型乱码排查路线图确认浏览器开发者工具中请求头的Content-Type是否带charset检查Tomcat日志原始字节是否已损坏用Postman绕过前端直接测试接口在Linux环境用curl -v查看原始传输数据2. 404错误路径映射的黑暗森林新手最困惑的莫过于明明代码没问题为什么一直404。以下是我整理的路径问题检查清单问题类型典型症状解决方案Servlet未加载所有路径404检查web.xml或注解扫描路径上下文路径缺失控制台无错误完整路径应为/项目名/servlet路径注解冲突部分路径404避免同时使用web.xml和WebServlet默认Servlet覆盖静态资源404在web.xml中调整映射顺序最隐蔽的路径陷阱发生在重定向时response.sendRedirect(login); // 相对路径风险 response.sendRedirect(request.getContextPath() /login); // 正确写法我曾花费三小时追踪一个诡异现象开发环境正常但生产环境404最终发现是Eclipse没有把WebServlet编译到classes目录。解决方法是在Maven配置中明确指定build resources resource directorysrc/main/java/directory includes include**/*.class/include /includes /resource /resources /build3. 500错误服务器内部的完美风暴当看到控制台抛出NullPointerException时真正的挑战是定位问题根源。以下是几种常见模式连接池泄漏诊断// 错误示范每次请求都新建连接 Connection conn DriverManager.getConnection(url); // 正确做法使用连接池 DataSource ds (DataSource) new InitialContext().lookup(java:comp/env/jdbc/mydb);事务处理陷阱try { conn.setAutoCommit(false); // 业务操作1 // 业务操作2 conn.commit(); // 忘记提交 } catch (Exception e) { conn.rollback(); // 未处理回滚异常 } finally { conn.close(); // 可能关闭失败 }推荐使用模板方法封装public class JdbcTemplate { public static void execute(ConnectionCallback action) { Connection conn null; try { conn dataSource.getConnection(); action.doInConnection(conn); } finally { if(conn ! null) try {conn.close();} catch(SQLException ignored) {} } } }日志记录技巧不要简单打印e.printStackTrace()应该logger.error(用户{}登录异常: {}, username, ExceptionUtils.getStackTrace(e));配置log4j2.xml实现线程信息记录PatternLayout pattern%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n/4. SQL注入从漏洞到防御的进化之路当我第一次成功注入自己的系统时才真正理解预编译语句的价值。下面通过对比展示防御演进危险阶段字符串拼接String sql SELECT * FROM users WHERE username username AND password password ; Statement stmt conn.createStatement(); ResultSet rs stmt.executeQuery(sql); // 注入点基础防御参数化查询String sql SELECT * FROM users WHERE username? AND password?; PreparedStatement pstmt conn.prepareStatement(sql); pstmt.setString(1, username); pstmt.setString(2, password);进阶方案ORM框架Entity Table(nameusers) public class User { Id private String username; private String password; } public User login(String username, String password) { return em.createQuery( SELECT u FROM User u WHERE u.username:un AND u.password:pw, User.class) .setParameter(un, username) .setParameter(pw, password) .getSingleResult(); }终极防御深度防护策略最小权限原则数据库用户只赋予必要权限输入验证白名单过滤特殊字符if(!username.matches([a-zA-Z0-9_])) { throw new IllegalArgumentException(非法用户名); }密码加密永远不要存储明文密码String hashedPwd DigestUtils.sha256Hex(password salt);定期漏洞扫描使用SQLMap等工具测试5. 性能优化从功能实现到生产级代码当用户量突破1000时我的登录接口响应时间从200ms飙升到2s。通过JProfiler分析发现连接管理瓶颈// 反模式每次请求创建新连接 public Connection getConnection() throws SQLException { return DriverManager.getConnection(url, user, pwd); }解决方案配置HikariCP连接池# application.properties spring.datasource.hikari.maximum-pool-size20 spring.datasource.hikari.idle-timeout30000缓存策略优化// 使用Guava缓存验证码 LoadingCacheString, String codeCache CacheBuilder.newBuilder() .expireAfterWrite(5, TimeUnit.MINUTES) .build(new CacheLoaderString, String() { public String load(String key) { return generateRandomCode(); } });异步处理实践对于发送邮件等非核心操作WebServlet(/login) public class LoginServlet extends HttpServlet { private ExecutorService executor Executors.newFixedThreadPool(4); protected void doPost(...) { executor.submit(() - { sendWelcomeEmail(username); }); } }6. 安全加固超越基础防护在经历了一次撞库攻击后我完善了安全措施登录限流实现// 使用Redis记录尝试次数 String key login:attempts: request.getRemoteAddr(); long attempts redis.incr(key); redis.expire(key, 1, TimeUnit.HOURS); if(attempts 5) { response.sendError(429, 尝试次数过多); return; }CSRF防护方案// 生成令牌 String token UUID.randomUUID().toString(); session.setAttribute(csrfToken, token); request.setAttribute(csrfToken, token); // 验证令牌 if(!request.getParameter(csrfToken).equals(session.getAttribute(csrfToken))) { throw new SecurityException(CSRF验证失败); }HTTPS强制配置在web.xml中添加security-constraint web-resource-collection url-pattern/*/url-pattern /web-resource-collection user-data-constraint transport-guaranteeCONFIDENTIAL/transport-guarantee /user-data-constraint /security-constraint7. 测试策略保障稳定性的最后防线单元测试模板ExtendWith(MockitoExtension.class) class LoginServletTest { Mock HttpServletRequest request; Mock HttpServletResponse response; Mock RequestDispatcher dispatcher; Test void testLoginSuccess() throws Exception { when(request.getParameter(user)).thenReturn(admin); when(request.getParameter(pwd)).thenReturn(123456); when(request.getRequestDispatcher(/welcome.jsp)).thenReturn(dispatcher); new LoginServlet().doPost(request, response); verify(dispatcher).forward(request, response); } }集成测试方案使用TestContainers进行数据库测试Testcontainers class UserRepositoryIT { Container static MySQLContainer? mysql new MySQLContainer(mysql:8.0); BeforeAll static void setup() { System.setProperty(db.url, mysql.getJdbcUrl()); } }压力测试要点# 使用wrk进行基准测试 wrk -t4 -c100 -d30s --latency http://localhost:8080/login8. 现代化演进从Servlet到Spring Boot虽然现代项目多采用Spring框架但理解Servlet底层机制依然重要。对比传统与现代实现配置方式对比// 传统web.xml servlet servlet-namelogin/servlet-name servlet-classcom.example.LoginServlet/servlet-class /servlet // Spring Boot方式 RestController public class LoginController { PostMapping(/login) public ResponseEntityString login(RequestBody LoginForm form) { // ... } }依赖管理进化!-- 传统Servlet依赖 -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency !-- Spring Boot Starter -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency异常处理改进// Servlet时代 try { // 业务代码 } catch (Exception e) { response.sendError(500, e.getMessage()); } // Spring方式 ExceptionHandler(SQLException.class) public ResponseEntityErrorResponse handleSQLException(SQLException ex) { return ResponseEntity.status(500) .body(new ErrorResponse(数据库错误, ex.getMessage())); }