news 2026/10/20 18:08:57

JavaWeb用户管理模块:Servlet+JDBC企业级权限实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JavaWeb用户管理模块:Servlet+JDBC企业级权限实战

1. 项目概述:一个被反复打磨的用户管理模块,到底在练什么基本功?

“JavaWeb-15-SMBMS(以用户管理模块为例)”——这个标题乍看平平无奇,像极了某高校实验课第十五次作业的命名风格。但如果你在真实企业级Java Web开发一线干过三年以上,看到它会下意识坐直身体:这不是练习题,这是照妖镜。SMBMS全称是Small & Medium Business Management System,中文常译作“中小型企业管理系统”,而“用户管理模块”四个字背后,藏着一套完整的企业级权限治理骨架。它不炫技、不堆新框架,却把Servlet、JSP、MySQL、JDBC、MVC分层、过滤器、监听器、事务控制、SQL注入防护、密码加盐、角色权限映射这些Java Web最核心的硬功夫,全拧在了一起。

我带过几届实习生,第一周就让他们跑通这个模块。不是为了交差,而是因为——它能立刻暴露一个人对Web开发本质的理解深度。比如,有人写完登录功能就以为完成了,但真正卡住他的,是“管理员A修改了用户B的密码,为什么用户B下次登录时提示‘密码错误’,而A自己却能正常登录?”这种问题表面是逻辑bug,实则是没理清session生命周期与数据库状态同步的关系;再比如,“新增用户时前端传了空字符串,后端没判空,结果数据库里存了一堆‘’,后续用where username = ''查不到数据”,这暴露的是对SQL语义和Java字符串对象特性的双重盲区。

这个模块之所以被反复选为教学案例,并非因为它多先进,恰恰因为它足够“土”。它不用Spring Boot自动装配,不用MyBatis动态SQL,甚至不依赖Maven中央仓库——所有jar包都手动放进WEB-INF/lib。这种“返祖式”设计,逼着你亲手写Connection获取、PreparedStatement参数绑定、ResultSet遍历、finally块里的资源释放。它不让你跳过“为什么需要try-catch-finally”,而是让你在连接池未配置、数据库宕机的瞬间,亲眼看见Connection对象如何在内存里泄漏,直到Tomcat日志刷出“Too many connections”。

关键词“SMBMS”“用户管理模块”“JavaWeb”不是标签,而是坐标系。它锚定了技术栈边界(Java EE规范而非Spring全家桶)、业务复杂度边界(RBAC雏形而非OAuth2.0)、工程实践边界(手工部署war包而非Docker一键启停)。如果你正从培训班走向真实岗位,或刚从Spring Boot的舒适区回望Servlet原生开发,这个模块就是你的“渡劫台”:渡过去,你开始理解框架封装了什么;渡不过,你永远在调API,而不是写系统。

2. 整体架构与设计思路:为什么坚持用“老派”三层结构?

2.1 不用框架的底层逻辑:剥离抽象,直面本质

很多人看到“JavaWeb-15-SMBMS”第一反应是:“都2024年了还手写Servlet?是不是过时了?”这个问题问得极好,但答案恰恰相反——正因为它“过时”,才不可替代。Spring MVC的@RequestBody自动反序列化、MyBatis的resultMap自动映射、Spring Security的注解式权限控制,这些高级能力像一层厚厚的保温棉,裹住了开发者与HTTP协议、JDBC驱动、SQL执行引擎的真实触感。而SMBMS强制你撕开这层棉。

举个具体例子:用户登录验证。Spring Boot里可能就三行代码:

@PostMapping("/login") public Result login(@RequestBody LoginDTO dto) { return userService.login(dto.getUsername(), dto.getPassword()); }

但在SMBMS中,你需要亲手处理:

  • HTTP请求体解析:request.getParameter("username")获取表单字段,而非JSON解析;
  • 密码安全处理:用MessageDigest.getInstance("SHA-256")对明文密码加盐哈希,盐值必须随机生成并存入数据库,而非调用BCryptPasswordEncoder.encode();
  • Session状态管理:HttpSession session = request.getSession(true); session.setAttribute("user", user);而不是SecurityContextHolder.getContext().setAuthentication(...);
  • 登录失败计数:在数据库user表中增加login_fail_count字段,每次失败+1,连续5次锁定账户,这需要手动写UPDATE SQL并处理事务回滚。

这种“笨办法”的价值,在于它把每个环节的契约关系暴露无遗。比如request.getParameter()为什么只能取到GET/POST表单数据,而取不到JSON?因为Servlet规范定义的getParameter()方法只解析application/x-www-form-urlencoded和multipart/form-data两种Content-Type,JSON属于application/json,必须用request.getInputStream()读取原始字节流再解析——这个知识点,90%的Spring Boot初学者根本没机会接触。

2.2 MVC分层的物理实现:包结构即架构图

SMBMS的目录结构是教科书级的MVC具象化:

src/ ├── com.smbms.dao/ // 数据访问层:纯SQL操作,无业务逻辑 │ ├── UserDao.java // 接口定义CRUD方法 │ └── UserDaoImpl.java // 实现类,含JDBC连接、预编译、结果集映射 ├── com.smbms.service/ // 业务逻辑层:协调DAO,处理事务,校验规则 │ ├── UserService.java // 接口 │ └── UserServiceImpl.java// 实现类,含try-catch包裹DAO调用,手动commit/rollback ├── com.smbms.servlet/ // 控制层:接收请求,调用Service,转发响应 │ └── UserServlet.java // 根据action参数分发doLogin/doAdd/doUpdate等方法 └── com.smbms.entity/ // 实体层:POJO,与数据库表字段严格对应 └── User.java

关键点在于:每一层都有明确的“不可越界”契约。UserServlet里绝对不能出现new UserDaoImpl(),必须通过UserService userService = new UserServiceImpl()获取;UserServiceImpl里不能直接写System.out.println(),所有日志必须走统一Logger;UserDaoImpl里不能包含if (user.getAge() < 18)这类业务判断,年龄校验必须放在Service层。这种物理隔离,强迫你思考“数据该在哪里校验”“事务边界划在哪”“异常该由谁捕获”。

我曾见过一个真实案例:某公司用Spring Boot重构老系统,开发人员把DAO层的SQL异常直接抛到Controller,导致前端显示java.sql.SQLException: Lock wait timeout exceeded。根源就是没理解MVC的职责分离——DAO只负责数据操作,Service负责兜底异常转换,Controller只负责HTTP状态码返回。而SMBMS的手动分层,让这种错误在编码阶段就被语法约束扼杀。

2.3 用户模块的核心难点:RBAC权限模型的轻量落地

SMBMS的用户管理绝非增删改查那么简单,它嵌入了一个精简但完整的RBAC(基于角色的访问控制)模型。数据库设计上,它包含四张核心表:

  • user:用户基本信息(id, username, password, phone...)
  • role:角色定义(id, roleCode, roleName)
  • user_role:用户-角色关联表(userId, roleId)
  • permission:权限定义(id, code, name, url)
  • role_permission:角色-权限关联表(roleId, permissionId)

这个设计看似复杂,实则解决了三个致命问题:

  1. 避免硬编码权限:传统做法是在Servlet里写if (user.getRole().equals("admin")) { ... },一旦角色增多,if-else爆炸。而RBAC通过数据库关联,新增角色只需插入role表+配置role_permission,代码零修改;
  2. 支持细粒度控制:url字段存储/user/list.do、/user/add.do等具体路径,过滤器可拦截所有请求,根据当前用户拥有的权限URL列表决定是否放行;
  3. 解耦角色与权限:运营人员要给“财务专员”角色增加“导出报表”权限,DBA只需在role_permission表插入一条记录,无需重启应用或修改代码。

实操中最大的坑在于权限缓存一致性。很多初学者会把用户权限列表查出来存进Session,但当管理员后台修改了某角色的权限,已登录用户的Session不会自动更新。解决方案有两个:一是每次请求都重新查库(性能差),二是用监听器监听role_permission表变更,主动使对应Session失效。SMBMS选择后者,用ServletContextListener监听应用启动,注册自定义事件——这正是企业级系统必须面对的缓存穿透与雪崩问题的微缩版。

3. 核心细节解析与实操要点:从登录到权限校验的全流程拆解

3.1 登录流程的七步深水区

用户管理模块的登录功能,表面是输入账号密码点击提交,背后是七个必须亲手踩过的深水区:

第一步:表单提交的Content-Type陷阱
HTML表单默认enctype="application/x-www-form-urlencoded",此时request.getParameter("username")能正确获取值。但如果误设为enctype="multipart/form-data"(常用于文件上传),getParameter()将返回null。解决方案:检查form标签属性,或统一用request.getReader().readLine()读取原始请求体再解析。

第二步:密码加盐哈希的盐值存储
不能用固定盐值(如"abc123"),否则彩虹表可批量破解。SMBMS要求:

  • 盐值用SecureRandom生成32位随机字符串;
  • 盐值与哈希密码一起存入数据库user表的salt和password字段;
  • 验证时先查出用户盐值,再用相同算法哈希输入密码比对。

提示:SecureRandom比Random更安全,因后者是伪随机,可被预测。

第三步:Session ID的安全传输
Tomcat默认通过Cookie传递JSESSIONID,但若前端禁用Cookie,需启用URL重写。在web.xml中配置:

<session-config> <tracking-mode>COOKIE</tracking-mode> <!-- 或添加 URL --> <tracking-mode>URL</tracking-mode> </session-config>

实测发现,某些老旧内网浏览器禁用Cookie后,不配URL重写会导致每次请求都是新Session。

第四步:登录失败锁定机制
数据库user表需增加fail_count和lock_time字段。逻辑为:

  • 每次失败,fail_count++,若≥5则lock_time = now() + 30*60*1000(锁定30分钟);
  • 登录前先查lock_time > now(),若为真则拒绝并提示“账户已被锁定”;
  • 成功登录后重置fail_count=0,lock_time=null。

第五步:防止暴力破解的验证码
SMBMS通常集成简单数字验证码。关键点:验证码图片的<img src="code.jsp">每次刷新必须带时间戳参数(如?t=123456789),否则浏览器缓存图片导致验证码不更新。后端code.jsp需设置响应头:response.setHeader("Cache-Control", "no-store");。

第六步:登出时的Session彻底销毁
session.invalidate()只是标记Session失效,但客户端Cookie中的JSESSIONID仍存在。必须配合:

  • 前端登出后重定向到登录页;
  • 后端登出方法中调用session.removeAttribute("user")再session.invalidate();
  • 在web.xml中配置<session-config><session-timeout>30</session-timeout></session-config>作为兜底。

第七步:跨域登录的Cookie共享
若前端Vue项目部署在http://localhost:8080,后端JavaWeb在http://localhost:8081,浏览器默认阻止跨域Cookie。解决方案:后端响应头添加response.setHeader("Access-Control-Allow-Credentials", "true"),前端axios配置withCredentials: true,且Access-Control-Allow-Origin不能为*,必须指定精确域名。

3.2 用户列表分页的性能生死线

用户列表页常面临“查10万用户卡死”的问题。SMBMS的分页实现直击要害:

数据库层:避免OFFSET大偏移
传统SELECT * FROM user LIMIT 0,10在数据量大时效率骤降。SMBMS采用“游标分页”思想:

  • 第一页查SELECT * FROM user ORDER BY id ASC LIMIT 10,记下最后一条记录的id(如id=100);
  • 第二页查SELECT * FROM user WHERE id > 100 ORDER BY id ASC LIMIT 10;
  • 这样无论翻到第100页,都是基于索引的范围查询,而非全表扫描跳过前990条。

Java层:PageBean实体的精准设计
PageBean不能只存list和total,必须包含:

  • currentPage:当前页码;
  • pageSize:每页条数;
  • totalPage:总页数(total % pageSize == 0 ? total/pageSize : total/pageSize + 1);
  • startRow:SQL的起始行((currentPage-1)*pageSize);
  • hasPrevious/hasNext:布尔值指示是否有上下页。

注意:startRow计算必须用整型运算,避免浮点误差导致负数。

前端层:分页控件的防抖处理
用户狂点“下一页”按钮时,若不加限制,会触发多个AJAX请求。SMBMS的JSP页面用JavaScript实现:

let isLoading = false; function loadPage(page) { if (isLoading) return; isLoading = true; $.get("userServlet?action=list&page="+page, function(data){ // 渲染列表 isLoading = false; }); }

3.3 权限过滤器的双刃剑效应

SMBMS用Filter实现全局权限控制,但极易陷入两个误区:

误区一:过滤器链顺序错乱
web.xml中Filter声明顺序决定执行顺序。若EncodingFilter(处理中文乱码)在PermissionFilter之后,可能导致request.getParameter()获取的中文参数乱码,权限校验失败。正确顺序:

  1. EncodingFilter(最先,确保字符编码);
  2. LoginFilter(检查是否已登录);
  3. PermissionFilter(检查是否有权限);
  4. LogFilter(记录访问日志)。

误区二:忽略静态资源放行
PermissionFilter若拦截*.js、*.css、*.png,会导致页面样式丢失。必须在doFilter()中添加白名单:

String uri = request.getRequestURI(); if (uri.endsWith(".js") || uri.endsWith(".css") || uri.endsWith(".png")) { chain.doFilter(request, response); return; }

实战技巧:权限URL的动态匹配
permission表的url字段存/user/*表示匹配所有用户模块路径。过滤器需实现通配符匹配:

private boolean matchUrl(String requestUrl, String pattern) { if (pattern.endsWith("/*")) { String prefix = pattern.substring(0, pattern.length()-2); return requestUrl.startsWith(prefix); } return requestUrl.equals(pattern); }

这样/user/list.do能匹配/user/*,而/order/list.do不能。

4. 实操过程与核心环节实现:从零搭建用户管理模块

4.1 环境准备与项目初始化

SMBMS对环境要求极简,但细节决定成败:

JDK版本选择
必须使用JDK 8u202或更高版本。低版本存在SecureRandom熵池阻塞问题:在Linux服务器上,SecureRandom.getInstanceStrong()可能卡住30秒以上。解决方案:在$JAVA_HOME/jre/lib/security/java.security中修改:

securerandom.source=file:/dev/./urandom

(注意/dev/./urandom是绕过/dev/random阻塞的 trick)

Tomcat配置要点

  • 修改conf/server.xml,在<Connector>标签中添加URIEncoding="UTF-8",解决GET请求中文参数乱码;
  • conf/web.xml中取消<servlet-mapping>下<url-pattern>/</url-pattern>的注释,使默认Servlet能处理静态资源;
  • bin/catalina.sh中增加JAVA_OPTS="-Xms512m -Xmx1024m -XX:MetaspaceSize=256m",避免内存溢出。

数据库建表脚本关键字段
MySQL建表必须指定字符集和排序规则,否则中文存取异常:

CREATE TABLE `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `username` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户名', `password` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '密码(SHA256加盐)', `salt` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '盐值', `phone` varchar(11) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '手机号', `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:0-禁用,1-启用', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

注意:utf8mb4_unicode_ci支持emoji,USING BTREE显式指定索引类型,避免InnoDB默认的B+Tree被误解。

4.2 DAO层实现:JDBC模板的极简封装

SMBMS不引入DBUtils等工具,而是手写JDBC模板,核心在于BaseDao抽象类:

public abstract class BaseDao { private static final String DRIVER = "com.mysql.cj.jdbc.Driver"; private static final String URL = "jdbc:mysql://localhost:3306/smbms?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai"; private static final String USER = "root"; private static final String PASSWORD = "123456"; protected Connection getConnection() throws SQLException { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { throw new RuntimeException("驱动加载失败", e); } return DriverManager.getConnection(URL, USER, PASSWORD); } protected void closeResource(Connection conn, PreparedStatement ps, ResultSet rs) { if (rs != null) try { rs.close(); } catch (SQLException e) {} if (ps != null) try { ps.close(); } catch (SQLException e) {} if (conn != null) try { conn.close(); } catch (SQLException e) {} } }

UserDaoImpl的关键实现

public class UserDaoImpl extends BaseDao implements UserDao { @Override public User login(String username, String password) { Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; User user = null; try { conn = getConnection(); String sql = "SELECT id, username, password, salt, phone, status FROM user WHERE username = ? AND status = 1"; ps = conn.prepareStatement(sql); ps.setString(1, username); rs = ps.executeQuery(); if (rs.next()) { user = new User(); user.setId(rs.getLong("id")); user.setUsername(rs.getString("username")); user.setPassword(rs.getString("password")); // 存的是哈希值 user.setSalt(rs.getString("salt")); user.setPhone(rs.getString("phone")); user.setStatus(rs.getInt("status")); } } catch (SQLException e) { e.printStackTrace(); } finally { closeResource(conn, ps, rs); } return user; } }

关键细节:ps.setString(1, username)使用占位符防止SQL注入;closeResource()在finally中确保资源释放;status = 1过滤禁用账户,体现业务逻辑前置。

4.3 Service层事务控制:手动提交的生死时速

UserService的addUser()方法必须保证“插入用户+关联角色”原子性,否则出现用户存在但无角色的脏数据:

public class UserServiceImpl implements UserService { private UserDao userDao = new UserDaoImpl(); private UserRoleDao userRoleDao = new UserRoleDaoImpl(); @Override public boolean addUser(User user, Long roleId) { Connection conn = null; try { conn = BaseDao.getConnection(); conn.setAutoCommit(false); // 关闭自动提交 // 插入用户 long userId = userDao.insert(user, conn); if (userId <= 0) { throw new RuntimeException("用户插入失败"); } // 关联角色 boolean bindSuccess = userRoleDao.bindUserRole(userId, roleId, conn); if (!bindSuccess) { throw new RuntimeException("角色绑定失败"); } conn.commit(); // 全部成功才提交 return true; } catch (Exception e) { if (conn != null) { try { conn.rollback(); // 任一环节失败则回滚 } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); return false; } finally { if (conn != null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } } }

核心原理:conn.setAutoCommit(false)开启事务,conn.commit()手动提交,conn.rollback()手动回滚。必须将Connection作为参数传入各DAO方法,确保所有操作在同一数据库连接上执行。

4.4 Servlet层分发逻辑:Action模式的精妙平衡

UserServlet采用经典Action模式,用action参数分发请求:

public class UserServlet extends HttpServlet { private UserService userService = new UserServiceImpl(); @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String action = request.getParameter("action"); if ("login".equals(action)) { doLogin(request, response); } else if ("list".equals(action)) { doList(request, response); } else if ("add".equals(action)) { doAdd(request, response); } else if ("get".equals(action)) { doGetById(request, response); } } private void doLogin(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username"); String password = request.getParameter("password"); User user = userService.login(username, password); if (user != null) { // 登录成功,存入Session HttpSession session = request.getSession(); session.setAttribute("user", user); // 查询用户权限URL列表 List<String> urls = userService.getPermissionUrls(user.getId()); session.setAttribute("permissionUrls", urls); response.sendRedirect("jsp/frame.jsp"); // 跳转主框架页 } else { request.setAttribute("error", "用户名或密码错误"); request.getRequestDispatcher("login.jsp").forward(request, response); } } }

设计哲学:doGet()/doPost()只做路由,具体业务逻辑全部下沉到私有方法。这样既保持Servlet轻量,又便于单元测试(可直接调用doLogin()方法)。

4.5 前端JSP页面:EL表达式与JSTL的黄金组合

用户列表页userlist.jsp体现JSP最佳实践:

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <head> <title>用户列表</title> <link rel="stylesheet" href="${pageContext.request.contextPath}/css/common.css"> </head> <body> <div class="container"> <h2>用户管理</h2> <div class="search-box"> <form action="userServlet" method="get"> <input type="hidden" name="action" value="list"> <input type="text" name="queryUsername" placeholder="请输入用户名" value="${param.queryUsername}"> <button type="submit">搜索</button> </form> </div> <table class="data-table"> <thead> <tr> <th>序号</th> <th>用户名</th> <th>手机号</th> <th>状态</th> <th>操作</th> </tr> </thead> <tbody> <c:forEach items="${page.list}" var="user" varStatus="status"> <tr> <td>${status.count}</td> <td>${user.username}</td> <td>${user.phone}</td> <td> <c:if test="${user.status == 1}">启用</c:if> <c:if test="${user.status == 0}">禁用</c:if> </td> <td> <a href="userServlet?action=get&id=${user.id}">编辑</a> <a href="javascript:void(0)" onclick="delUser(${user.id})">删除</a> </td> </tr> </c:forEach> </tbody> </table> <!-- 分页控件 --> <div class="pagination"> <c:if test="${page.hasPrevious}"> <a href="userServlet?action=list&page=${page.currentPage-1}&queryUsername=${param.queryUsername}">上一页</a> </c:if> <span>第${page.currentPage}页,共${page.totalPage}页</span> <c:if test="${page.hasNext}"> <a href="userServlet?action=list&page=${page.currentPage+1}&queryUsername=${param.queryUsername}">下一页</a> </c:if> </div> </div> <script> function delUser(id) { if (confirm("确定删除该用户?")) { window.location.href = "userServlet?action=delete&id=" + id; } } </script> </body> </html>

关键技巧:${pageContext.request.contextPath}获取应用上下文路径,避免硬编码;<c:forEach>遍历集合,varStatus="status"提供循环索引;<c:if>条件渲染,替代Java脚本片段;onclick="delUser(${user.id})"中${user.id}在服务端渲染,避免XSS(因id为数字,无风险)。

5. 常见问题与排查技巧实录:那些只有踩过才懂的坑

5.1 中文乱码的七种死法与解法

中文乱码是SMBMS新手最高频问题,本质是字符编码在HTTP请求、数据库、JVM、IDE四层未对齐:

场景现象根本原因解决方案
GET请求中文参数request.getParameter("name")返回????Tomcat默认ISO-8859-1解码在web.xml中配置<connector port="8080" URIEncoding="UTF-8"/>
POST请求中文参数表单提交后getParameter()乱码请求体未指定编码在Servlet开头加request.setCharacterEncoding("UTF-8")
MySQL存储乱码数据库中显示李四MySQL服务端、数据库、表、列未设UTF8MB4执行ALTER DATABASE smbms CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
JSP页面乱码浏览器显示方框JSP文件本身编码非UTF-8IDE中右键JSP文件→Properties→Text file encoding→UTF-8
控制台日志乱码System.out.println("中文")输出??JVM默认编码非UTF-8启动Tomcat时加-Dfile.encoding=UTF-8参数
IDEA控制台乱码IDEA Terminal中mvn compile输出乱码IDEA终端编码设置错误File→Settings→Editor→File Encodings→Global Encoding设为UTF-8
Response输出乱码response.getWriter().print("中文")乱码响应未设ContentTyperesponse.setContentType("text/html;charset=UTF-8")

实战经验:遇到乱码,按“请求→响应→存储”链条逐层排查。先用浏览器开发者工具Network面板看Request Headers的Content-Type和Response Headers的Content-Type,确认编码声明;再查数据库SHOW VARIABLES LIKE 'character_set%';最后看IDE编码设置。切忌盲目改一处。

5.2 Session失效的三大幻觉

Session失效常被误判为“Bug”,实则是开发者对Session机制理解偏差:

幻觉一:“我明明没关浏览器,Session怎么没了?”
真相:Session默认超时30分钟(web.xml配置),且超时时间从最后一次请求开始计算。用户打开列表页后去喝咖啡,30分钟未操作,Session自动失效。解决方案:前端用Ajax定时发送心跳请求(如每25分钟$.get("keepAlive")),重置超时计时器。

幻觉二:“我在A浏览器登录,B浏览器也能看到用户信息!”
真相:Session ID通过Cookie传输,若两浏览器共用同一Chrome用户配置(如同步登录),Cookie可能共享。更常见的是开发时用localhost,而生产用域名,localhost的Cookie作用域不包含127.0.0.1,导致切换地址时Session丢失。解决方案:开发时统一用127.0.0.1,或配置Cookie Domain为.local。

幻觉三:“我调用了session.invalidate(),但getAttribute("user")还能取到值!”
真相:invalidate()只是标记Session失效,当前请求的Session对象仍可用,直到请求结束。getAttribute()在invalidate()后仍返回值,但下一次请求将获得新Session。验证方法:在invalidate()后立即session.getAttribute()能取到,但重定向后新请求中getSession(false)返回null。

5.3 SQL注入的隐蔽入口与防御实录

SMBMS虽用PreparedStatement,但仍有注入风险点:

风险点1:ORDER BY子句
String sql = "SELECT * FROM user ORDER BY " + request.getParameter("sortField");
即使sortField为username,攻击者传username, (SELECT password FROM user WHERE id=1)即可注入。
解法:白名单校验

String sortField = request.getParameter("sortField"); if (!"username".equals(sortField) && !"phone".equals(sortField)) { sortField = "id"; // 默认安全字段 }

风险点2:IN子句的动态参数
String sql = "SELECT * FROM user WHERE id IN (" + ids + ")";
攻击者传ids="1,2,3); DROP TABLE user; --"。
解法:用String.join(",", idList)拼接,且idList必须是Long类型列表,toString()后自然过滤非数字。

风险点3:LIKE模糊查询
String sql = "SELECT * FROM user WHERE username LIKE '%" + keyword + "%'";
攻击者传keyword="%'; DROP TABLE user; --"。
解法:PreparedStatement支持LIKE,但需转义通配符:

String keyword = request.getParameter("keyword").replace("%", "\\%").replace("_", "\\_"); ps.setString(1, "%" + keyword + "%");

5.4 过滤器失效的五种场景

PermissionFilter是权限守门员,但常因配置失误形同虚设:

失效场景排查方法修复方案
Filter未在web.xml注册查看web.xml是否有<filter>和<filter-mapping>补全配置,注意<url-pattern>必须覆盖所有受保护路径,如/user/*、/role/*
Filter映射顺序错误在web.xml中检查<filter-mapping>声明顺序将PermissionFilter放在LoginFilter之后,确保已登录用户才校验权限
静态资源未放行浏览器F12看Network,发现jquery.js返回403在Filter的doFilter()中添加`if (uri.endsWith(".js")
AJAX请求被拦截F12看XHR请求,Response为空检查request.getRequestURI()是否包含/api/等前缀,将其加入白名单
HTTPS重定向导致Cookie丢失生产环境HTTP跳转HTTPS后Session丢失在web.xml中配置<session-config><cookie-secure>true</cookie-secure></session-config>,并确保反向代理(如Nginx)透传X-Forwarded-Proto头

终极排查技巧:在Filter的doFilter()开头加System.out.println("Filter intercept: " + request.getRequestURI());,启动Tomcat看控制台输出,确认是否被拦截。若无输出,说明Filter根本未生效。

5.5 数据库连接池的隐形杀手

SMBMS虽未用Druid/HikariCP,但手动管理Connection时,以下问题必现:

问题1:Connection未关闭导致“Too many connections”
现象:运行一段时间后,数据库报错Cannot create PoolableConnectionFactory。
根因:closeResource()未在finally块中执行,或finally中conn.close()抛出异常导致后续ps.close()未执行。
解法:用Java 7+的try-with-resources:

try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { // 处理结果集 } catch (SQLException e) { e.printStackTrace(); } // 自动关闭所有资源

问题2:事务未回滚导致数据不一致
现象:新增用户失败,但数据库中已存在部分数据(如用户记录插入成功,角色关联失败)。
根因:catch块中只打印异常,未调用`

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 18:08:08

PreScan-ROS-Simulink三端协同的自动驾驶横向控制验证方法

简介&#xff1a;本资源是一套面向计算机、电子信息工程及数学等专业学习者的自动驾驶控制算法实践方案&#xff0c;聚焦横向控制核心方法——Stanley与MPC算法的联合实现&#xff0c;依托PreScan仿真环境、ROS通信框架与Simulink建模平台完成闭环验证。资源共1636个文件&#…

作者头像 李华
网站建设 2026/10/20 18:06:08

RV1126开发板选型评估:从需求拆解到场景适配的硬核指南

1. 写在选型之前&#xff1a;为什么那么多项目栽在"以为够用"上这两年陆陆续续有朋友拿着需求来找我聊硬件选型&#xff0c;问得最多的之一就是RV1126。有人想拿它做智能门禁&#xff0c;有人想用在做工业视觉检测&#xff0c;还有人打算把它塞进农业采摘机器人里。问…

作者头像 李华
网站建设 2026/10/20 18:05:22

Playwright 实战:一套代码统一 Web、移动端与 API 自动化测试

做自动化测试这些年&#xff0c;我见过太多同学在 Web、移动端、API 自动化测试之间来回切换&#xff0c;最终维护三套脚本、三套报告和三条流水线&#xff0c;整个人被重复劳动拖垮。后来认真啃下 Playwright&#xff0c;我才意识到&#xff0c;这三件事其实是可以被同一套代码…

作者头像 李华
网站建设 2026/10/20 18:04:52

大模型算法岗面试复盘:KV Cache、MLA与RoPE外推深度拆解

刚面完某头部互联网公司的大模型算法岗&#xff0c;我花了一整天复盘面试记录。说实话&#xff0c;挂得心服口服。整场面试几乎没有问什么“你熟不熟悉Transformer”这类送分题&#xff0c;全程围绕一个核心逻辑展开&#xff1a;从KV Cache的显存账本一路追问到DeepSeek MLA的结…

作者头像 李华
网站建设 2026/10/20 18:04:32

Opus 5.5不生成视频?真相:用代码让浏览器逐帧画出动画

自从看到某个新模型号称"生成视频"之后&#xff0c;我一直想写点东西聊聊背后的真相。标题里那句"Opus 5.5 不生成视频&#xff0c;是写代码让浏览器逐帧画出来"&#xff0c;我当时看到也愣了一下&#xff0c;但实际动手跑过几个场景之后&#xff0c;发现这…

作者头像 李华
网站建设 2026/10/20 18:03:55

AI时代隐私工具选型指南:按任务分级匹配工具

1. 为什么“按任务选工具”是 AI 时代的第一课1.1 一个真实踩坑场景&#xff1a;把敏感数据喂给了通用大模型去年帮一个做跨境电商的朋友处理售后数据&#xff0c;他图省事&#xff0c;直接把包含客户姓名、电话、收货地址的 Excel 表格拖进了一个公开的在线 AI 对话工具&#…

作者头像 李华