ARTICLE DETAIL

资讯详情

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

JavaWeb登录注册手写实战:Servlet、Session与Filter全流程

JavaWeb登录注册手写实战:Servlet、Session与Filter全流程 简介在JavaWeb开发中用户登录注册是串联Servlet、JSP、Session、Cookie与Filter的典型场景也是理解Web会话管理和请求生命周期的最佳实践。通过手写登录注册可以清晰看到表单POST请求如何被Servlet接收、密码加盐哈希如何存储、Session如何维持登录状态、Filter如何控制页面访问权限。这套基础流程不仅是课程设计和毕业设计的常见需求更是后续学习Spring MVC、MyBatis等框架的底层铺垫。本文以ServletJSPTomcatMySQL为环境从项目搭建、数据库设计到会话闭环完整呈现一套可运行的登录注册实现并总结中文乱码、密码加密、Filter拦截等高频坑位与生产级优化建议。1. javaweb登录注册页面为什么值得自己写一遍登录注册是 JavaWeb 里少有的“一条线串到底”的场景用户在浏览器里填表单表单以 POST 请求发到 ServletServlet 查数据库、校验密码、把用户状态写进 Session再通过 Filter 控制后续页面的访问。每一步都发生在真实项目里每一步也都能单独拆出来讲清楚。我的经验是能把登录注册这条线走顺的人后面学 Spring MVC 或者 MyBatis 会明显更快因为框架只是把这套流程换了个更工程化的写法。这篇笔记从 IDEA 里跑通的最小配置出发先拆环境再给登录和注册两个 Servlet 的完整代码然后是 Session、Cookie、Filter 组成的会话闭环最后收在验证码、限流和验收清单上。新同学可以照着代码块逐行敲一遍已经写过一遍的可以直接跳去第 5 章和第 6 章看边界和检查项。2. 环境与项目骨架用 IDEA 跑通 javaweb 的最小配置2.1 选型Servlet JSP 还是 Spring Boot标题里挂着 javaweb我默认把它理解为传统 Servlet JSP 路线。Spring Boot 当然也能做登录注册而且启动更快但它的内嵌 Tomcat、自动配置和 starter 依赖把 Servlet 生命周期藏起来了新手很容易“跑起来但不知道发生了什么”。课程设计、毕业设计或者刚学完 Servlet 的阶段我一般建议用 Servlet 4.0 JSP Tomcat 9 MySQL 8 这套组合。另一个现实原因是很多学校的 JavaWeb 课程考核点就是手写 Servlet 和 web.xml用 Boot 反而没法交差。从过程来讲手写 Servlet 能看到请求对象和响应对象在 doGet/doPost 里怎么被塞进去能看到 Session 实际是挂在 Cookie 上的这些直观感受对后续理解框架帮助很大。下表是我平时给新人对比时的要点维度Servlet JSPSpring Boot请求入口web.xml / WebServletController RequestMappingSession 操作HttpSession 一套 APIHttpSession 或 Spring Session部署方式打 war 丢进 Tomcat内嵌 Tomcatjar 直接跑门槛低生命周期透明中自动配置黑盒适合场景课程设计 / 理解底层现代后端 / 快速开发如果你只是想把登录注册跑通Servlet 这套更合适如果你已经在用 Boot也可以用 Boot 重写一遍同样的逻辑对比一下两者对“请求生命周期”的暴露程度。2.2 项目结构与 Maven 依赖我建的是 maven-webapp 结构的 war 项目pom.xml 里只需要三个核心依赖servlet-api、jsp-api、mysql-connector-java。示意配置如下!-- pom.xml 关键部分 -- dependencies !-- Servlet API 由 Tomcat 提供scope 用 provided 不打包进 war -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency !-- 如果使用 JSP需要对应版本的 jsp-api -- dependency groupIdjavax.servlet.jsp/groupId artifactIdjavax.servlet.jsp-api/artifactId version2.3.3/version scopeprovided/scope /dependency !-- MySQL 驱动版本要和数据库对应 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency /dependencies参数说明scopeprovided 表示编译和测试时需要这份 jar但最终部署时由 Tomcat 容器提供这样能避免 servlet-api 和 Tomcat 自带版本冲突。mysql-connector-java 版本最好和数据库大版本匹配MySQL 5.7 与 8.0 的连接行为有差异JDBC 驱动的 8.0.x 版本也能连 5.7但反过来不一定行。还需要注意 Tomcat 版本和 Servlet API 版本的对应关系Tomcat 9 对应 javax.servlet 4.xTomcat 10 对应 jakarta.servlet 5.x包名完全不同。用 Tomcat 10 跑 javax 代码会直接报 NoClassDefFoundError这个问题我在第 5 章会展开讲。目录结构用最标准的 maven-webapp 布局src/main/java/com/example/demo/ servlet/ - RegisterServlet、LoginServlet filter/ - AuthFilter dao/ - UserDao util/ - DBUtil、HashUtil、SaltUtil src/main/webapp/ WEB-INF/web.xml index.jsp login.jsp register.jsp css/style.css这个结构的好处是请求处理、页面展示、数据库访问三层分开后面加 Filter 或者换连接池都不会影响已有 Servlet。2.3 数据库表设计用户名唯一是底线登录注册的表一般就一张但字段设计要有前瞻性。如果只做课程设计id、username、password 三个字段够跑如果要往真实项目靠密码加盐和创建时间必须有。用户可能重名所以 username 建唯一索引应用层做重名判断的同时数据库再兜底避免并发下的脏数据。CREATE DATABASE IF NOT EXISTS userdb DEFAULT CHARACTER SET utf8mb4; USE userdb; CREATE TABLE t_user ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL, password VARCHAR(100) NOT NULL, salt VARCHAR(32) DEFAULT , create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;参数说明utf8mb4 比 utf8 多支持 emoji 和部分生僻字注册时用户昵称里带个表情是很常见的场景用 utf8 可能直接报 Incorrect string value。password 字段设计成 100 长度是为了给加盐哈希后的字符串留空间SHA-256 十六进制串是 64 位BCrypt 是 60 字符VARCHAR(100) 足够如果只按 MD5 的 32 位设计将来换算法时又要改表结构。唯一索引是数据库层的兜底应用层查一次、数据库再挡一次双端检查才能挡住并发重名。2.4 数据库连接先写一个能跑的 DBUtil连接数据库最直接的方式是 DriverManager每次请求新建连接。课程设计无所谓但真实项目必须用连接池。为了让新手在最小代码量下跑通我在这套代码里用一个静态 DBUtil 封装连接和关闭// DBUtil.java —— 连接与关闭的统一封装 public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/userdb?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD 123456; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(MySQL驱动加载失败检查依赖是否引入); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { if (rs ! null) { try { rs.close(); } catch (SQLException ignored) {} } if (ps ! null) { try { ps.close(); } catch (SQLException ignored) {} } if (conn ! null) { try { conn.close(); } catch (SQLException ignored) {} } } }参数说明serverTimezoneAsia/Shanghai 是 MySQL 8 的硬要求不写会报时区异常useUnicodetruecharacterEncodingutf8 配合建表时的 utf8mb4是防止中文乱码的最关键一环。把用户名密码写死在代码里只适合开发阶段部署到公网前要改成配置文件加环境变量。课程设计里每次请求 new 一个连接也能跑但一定要在 finally 里关闭连接否则 MySQL 的 max_connections 很快被打满报 Too many connections。等你把这个项目跑通再把 DriverManager 换成 HikariCP能明显感受到连接池对并发请求的改善。3. 登录与注册的核心代码Servlet、密码哈希与表单提交3.1 注册 Servlet从表单字段到入库注册的流程是接收参数 → 基本校验 → 查重 → 密码加盐哈希 → 插入数据库 → 跳转登录页。完整代码贴在下面注意 doPost 里必须在一开始处理中文乱码// RegisterServlet.java WebServlet(/register) public class RegisterServlet extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 1. 请求与响应统一用 UTF-8必须在读取任何参数之前 req.setCharacterEncoding(UTF-8); resp.setContentType(text/html;charsetUTF-8); String username req.getParameter(username); String password req.getParameter(password); String confirm req.getParameter(confirm); // 2. 基础校验非空 两次密码一致 if (username null || username.trim().isEmpty() || password null || password.isEmpty()) { req.setAttribute(msg, 用户名和密码不能为空); req.getRequestDispatcher(/register.jsp).forward(req, resp); return; } if (!password.equals(confirm)) { req.setAttribute(msg, 两次密码输入不一致); req.getRequestDispatcher(/register.jsp).forward(req, resp); return; } // 3. 查重用户名被占用就回到注册页并提示 UserDao dao new UserDao(); if (dao.findByName(username) ! null) { req.setAttribute(msg, 用户名已被注册); req.getRequestDispatcher(/register.jsp).forward(req, resp); return; } // 4. 密码加盐哈希每个用户盐不同库里不存明文 String salt SaltUtil.generateSalt(); String hashed HashUtil.sha256(password salt); boolean ok dao.insertUser(username, hashed, salt); if (ok) { // 注册成功直接去登录页 resp.sendRedirect(req.getContextPath() /login.jsp); } else { req.setAttribute(msg, 注册失败请重试); req.getRequestDispatcher(/register.jsp).forward(req, resp); } } }逻辑说明setCharacterEncoding 必须在读取任何参数之前调用否则 Tomcat 默认按 ISO-8859-1 解码 POST 体中文名读出来就是乱码。forward 和 sendRedirect 的区别很容易被忽略forward 是服务器内部跳转地址栏不变request 对象还在sendRedirect 是浏览器重新发起新请求地址栏变化之前的 request 属性全部丢失。注册成功后用 sendRedirect 到 login.jsp 而不是 forward能避免用户按 F5 时重复提交同一个注册表单。参数说明username.trim().isEmpty() 挡掉只有空格的用户名password.isEmpty() 挡掉空密码。查重用 findByName 而不是 select all 再遍历SQL 用了占位符 ?防止拼字符串带来的 SQL 注入。salt 字段由 SaltUtil 生成一个随机串密码哈希用 HashUtil.sha256(password salt)这两段的实现我放在 3.3 里一起说。3.2 登录 Servlet比对密码并写入 Session登录流程接收参数 → 查用户 → 哈希比对 → 写入 Session → 跳转主页。核心代码// LoginServlet.java WebServlet(/login) public class LoginServlet extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); resp.setContentType(text/html;charsetUTF-8); String username req.getParameter(username); String password req.getParameter(password); String remember req.getParameter(remember); UserDao dao new UserDao(); User user dao.findByName(username); if (user ! null) { String hashed HashUtil.sha256(password user.getSalt()); if (hashed.equals(user.getPassword())) { HttpSession session req.getSession(true); session.setAttribute(loginUser, user); session.setMaxInactiveInterval(30 * 60); // 30分钟无操作失效 if (on.equals(remember)) { Cookie cookie new Cookie(rememberMe, user.getId() : user.getUsername()); cookie.setMaxAge(7 * 24 * 3600); // 7天 cookie.setPath(req.getContextPath()); resp.addCookie(cookie); } resp.sendRedirect(req.getContextPath() /index.jsp); return; } } // 统一返回“用户名或密码错误”不区分用户是否存在 req.setAttribute(error, 用户名或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); } }逻辑说明密码比对绝不是 SELECT * FROM t_user WHERE username? AND password?因为库里存的是加盐哈希后的串必须取出该用户的盐重新哈希再比对。这种设计还带来一个安全好处用户不存在和密码错误返回同一句话避免攻击者通过提示语暴破出有效用户名。参数说明session.setMaxInactiveInterval 的单位是秒这里设了 30 分钟无操作失效。getSession(true) 表示没有会话就新建显式写 true 是让意图更清楚。remember 的值来自 checkbox选中时表单会提交 on我把 user.getId() 和用户名拼进 Cookie 值这只是演示第 4 章会说这个做法的安全边界以及怎么改。3.3 前端页面与 DAO 实现把两端接起来JSP 表单的 action 要和 WebServlet 中的路径保持一致。下面是一个完整可用的 login.jsp%-- login.jsp --% % page contentTypetext/html;charsetUTF-8 languagejava % html head title登录/title /head body form action${pageContext.request.contextPath}/login methodpost div label用户名/label input typetext nameusername required/ /div div label密码/label input typepassword namepassword required/ /div div labelinput typecheckbox nameremember/ 记住我/label /div div stylecolor:red${requestScope.error}/div button typesubmit登录/button /form /body /html参数说明action 用 ${pageContext.request.contextPath} 拼接上下文路径这是避免今天部署在 / 明天部署在 /demo 时 404 的最稳写法。直接写 action/login 在 context path 为 /demo 时会打到域名根路径去。required 是 HTML5 级别的必填校验能挡掉大部分空表单请求但服务端必须再做一次校验前端校验只是体验优化不是安全边界。${requestScope.error} 取的是 LoginServlet 里 forward 时 setAttribute 的值如果登录成功然后用 forward 而不是 sendRedirect 到 index.jsp地址栏会停留在 /login刷新时会重复提交所以才说成功分支要用重定向。DAO 层是注册和登录共同依赖的。关键代码就两个方法用 PreparedStatement 防注入同时把密码和盐分别取出// UserDao.java —— 只列出两个核心方法 public class UserDao { private static final String SQL_FIND_BY_NAME SELECT id, username, password, salt FROM t_user WHERE username ?; private static final String SQL_INSERT INSERT INTO t_user(username, password, salt) VALUES(?, ?, ?); public User findByName(String name) { try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(SQL_FIND_BY_NAME)) { ps.setString(1, name); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { User u new User(); u.setId(rs.getInt(id)); u.setUsername(rs.getString(username)); u.setPassword(rs.getString(password)); u.setSalt(rs.getString(salt)); return u; } } } catch (SQLException e) { e.printStackTrace(); } return null; } public boolean insertUser(String name, String hashedPwd, String salt) { try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(SQL_INSERT)) { ps.setString(1, name); ps.setString(2, hashedPwd); ps.setString(3, salt); return ps.executeUpdate() 1; } catch (SQLException e) { e.printStackTrace(); return false; } } }逻辑说明这里用了 try-with-resources连接、PreparedStatement、ResultSet 都能自动关闭不用再写 finally。SQL 里的 ? 占位符是防注入的根本永远不要把用户输入直接拼进 SQL 字符串。HashUtil 和 SaltUtil 的算法不复杂SaltUtil 用 SecureRandom 生成 16 字节随机数组再转十六进制HashUtil 用 MessageDigest 对 password salt 做 SHA-256返回 64 位小写十六进制字符串。这两个类总共 40 行左右课程设计里完全够用。如果你想做成 AJAX 无刷新登录把 form 的提交事件拦下来用 fetch 把参数 POST 到 /loginServlet 端返回 {code:0, msg:ok} 这种 JSON前端根据 code 做分支。我实际做的时候更喜欢 JSON 方案因为页面不跳转错误提示可以内联显示。但要在 Servlet 里额外处理 JSON 的 MIME 类型和编码代码复杂度会高一点。4. 会话与拦截器让“未登录不能进页面”形成闭环4.1 用 Filter 做统一登录校验登录注册写完还有一个需求几乎必然出现主页需要登录才能看。最粗暴的做法是在每个 JSP 或 Servlet 里手动检查 Session但这样代码散得到处都是用一个 Filter 拦截所有请求是 JavaWeb 里标准的统一做法// AuthFilter.java —— 统一登录校验 Cookie 自动登录 WebFilter(/*) public class AuthFilter implements Filter { private static final String LOGIN_PAGE /login.jsp; private static final String REGISTER_PAGE /register.jsp; Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; // 1. 如果 Session 里没有用户尝试按 rememberMe Cookie 恢复登录 HttpSession session req.getSession(false); User user (session null) ? null : (User) session.getAttribute(loginUser); if (user null) { user autoLoginFromCookie(req); if (user ! null) { HttpSession s req.getSession(true); s.setAttribute(loginUser, user); } } // 2. 放行公共页面、静态资源和已登录用户 String path req.getRequestURI().substring(req.getContextPath().length()); boolean publicPath LOGIN_PAGE.equals(path) || REGISTER_PAGE.equals(path) || /login.equals(path) || /register.equals(path) || path.startsWith(/css/) || path.startsWith(/js/); if (publicPath || user ! null) { chain.doFilter(request, response); } else { resp.sendRedirect(req.getContextPath() LOGIN_PAGE); } } }逻辑说明req.getSession(false) 的关键在于 false没有已存在的 Session 时返回 null 而不是新创建一个。如果这里写 getSession(true)过滤器链上每个请求都会强制生成一个无意义 Session浏览器会拿到一堆多余的 Set-Cookie白白增加服务端内存。放行的白名单要明确写出静态资源路径否则登录页的 CSS 也会被拦页面样式全丢。注意放行的是 /login 这个 Servlet 路径因为 POST 登录请求也要经过 Filter不放行的话用户永远无法登录。参数说明autoLoginFromCookie 是我抽出来的私有方法逻辑在下一小节展开。整个 Filter 拦截的是所有请求所以路径匹配用 requestURI 减去 contextPath 之后来判断。publicPath 里用 equals 判断页面路径是因为登录页只允许 /login.jsp 这个精确路径不要用 startsWith(/login)否则 /login 和 /login.jsp 都会放行未登录攻击者也就能访问某些命名相似的页面。4.2 Cookie 记住我自动恢复登录与安全边界上一小节里存了 rememberMe Cookie这里要做的是Session 为空但 Cookie 有效时自动恢复用户状态。简化版做到“按 id 找回用户并重建 Session”// AuthFilter 中的私有方法 —— Cookie 自动登录 private User autoLoginFromCookie(HttpServletRequest req) { Cookie[] cookies req.getCookies(); if (cookies null) return null; for (Cookie c : cookies) { if (rememberMe.equals(c.getName())) { // 演示写法Cookie 里存的是 id:username String[] parts c.getValue().split(:); if (parts.length 2) { try { int id Integer.parseInt(parts[0]); return new UserDao().findById(id); } catch (NumberFormatException ignored) { return null; } } break; } } return null; }逻辑说明这段代码把用户 id 取出来重新查一次数据库相当于用一条身份凭证恢复登录。安全边界必须说清楚把 id 明文放进 Cookie等于给攻击者一张永久通行证只要能抓包改 Cookie 就能伪装任意用户。生产环境不能这么干正确做法是签发一个随机 token 字符串把 token 和用户绑定存到 Redis 或数据库有效期 7 天服务端可以随时吊销。参数说明findById 需要在 UserDao 里补一个方法SQL 是 SELECT ... WHERE id?逻辑和 findByName 完全一样。Cookie 的 path 必须和写入时一致否则浏览器不会带回这个 Cookie自动登录会静默失效。我在 LoginServlet 里写入 cookie.setPath(req.getContextPath())所以恢复的时候同样要保证应用上下文一致。4.3 退出登录Session 和 Cookie 双清退出动作看起来简单但有个常见的半途而废坑只 invalidate Session没删 Cookie结果下次进来又被自动登录捞回来。所以退出 Servlet 要同时处理两件事// LogoutServlet.java WebServlet(/logout) public class LogoutServlet extends HttpServlet { Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { HttpSession session req.getSession(false); if (session ! null) { session.invalidate(); } Cookie cookie new Cookie(rememberMe, ); cookie.setMaxAge(0); cookie.setPath(req.getContextPath()); resp.addCookie(cookie); resp.sendRedirect(req.getContextPath() /login.jsp); } }参数说明Cookie 的删除方式是覆盖同名 Cookie 并把 Max-Age 设为 0路径必须和之前设置时完全一致包括 context path否则浏览器不认。invalidate() 之后不能再往 Session 里写属性否则抛 IllegalStateException所以退出处理要在同一请求里销毁并重定向。退出用 doGet 而不是 doPost因为退出本身不修改数据用户点个链接就能触发不需要表单包装。到这里登录注册的会话闭环就完整了注册入库 → 登录写 Session → Filter 拦截未登录 → 退出清 Session 和 Cookie。5. javaweb登录注册的常见问题排查我踩过的五个坑5.1 中文用户名存进去变成问号现象注册表单里输入“张三”数据库里看到的是两个问号登录时也永远匹配不上。原因分层出现了断层。数据库是 utf8mb4、页面是 UTF-8但 JDBC 连接串里少了 characterEncodingutf8或者 Servlet 端没有在读取参数前调用 req.setCharacterEncoding(UTF-8)。Tomcat 默认按 ISO-8859-1 解码 POST 体中文自然变成问号。解决三处必须一致——建库建表用 utf8mb4、JDBC URL 带 useUnicodetruecharacterEncodingutf8、Servlet 在 doPost 一开始就 setCharacterEncoding(UTF-8)。另外 IDEA 里 JSP 文件本身的 file encoding 也要设成 UTF-8否则源码里的中文在编译前就已经乱码了。可以在 IDEA 的 Settings → File Encodings 里把 Global Encoding、Project Encoding 和 Default encoding for properties files 统一设成 UTF-8。5.2 密码加密做了个寂寞库里存的是可逆的“伪密文”现象跟着教程写了个 MD5(password) 存库自我感觉良好。结果用在线 MD5 一查123456 的 MD5 是 e10adc密文和明文其实一一对应。原因MD5 本身不是不能用而是不能仅对原始密码做一次哈希。相同密码在同一算法下产物相同攻击者直接拿彩虹表、字典表批量比对就能还原明文。没有盐的单次哈希本质上只是把密码换了个写法。解决必须加盐盐要随机且每个用户不同。注册时生成随机盐存 password 哈希(密码 盐)登录时取该用户的盐重新哈希再比对。我给课程设计的方案是 SecureRandom 生成 16 字节盐 SHA-256生产环境建议直接用 BCrypt它自带了随机盐和可调的计算代价库都不用自己写。记住一条密码字段里绝不能出现能反推出原文的东西哈希算法本身是单向的但你需要给算法配上随机盐才算真正落地。5.3 Filter 写好后静态资源和登录页全被拦截现象加上 WebFilter(/*) 之后浏览器打开登录页一片空白控制台一堆 CSS 404连验证码图片都出不来。原因/* 匹配了所有请求Filter 里只判断了 Session没放行静态资源。CSS、JS、图片这些文件的请求路径是 /css/style.css 或 /captcha.jpg它们没有 Session于是被统一重定向到登录页登录页自己引用的 CSS 也跟着 404。解决白名单里放行静态资源前缀。上面第 4 章的代码里我已经写了 path.startsWith(/css/)实际项目还要按需放行 /images/、/fonts/、/js/。注意后缀如果你的静态资源不带统一前缀可以用正则匹配以 .css、.js、.png 结尾的路径。更细的做法是把动态请求统一放到 /api 或 /action 前缀下Filter 只拦页面静态资源天然不冲突。排查时先在 Filter 第一行打印 path看看到底拦的是哪个 URI比猜快得多。5.4 登录成功后被自己写的 Filter 弹回登录页现象登录页提交成功地址栏跳到 /index.jsp但随后立刻被弹回 /login.jsp循环往复主页永远进不去。原因三个可能一是 AuthFilter 的白名单里没有放行 /login 这个 Servlet 路径登录请求本身被拦截二是判断已登录用户的条件写错比如从 Session 里取属性时 key 和 LoginServlet 写入时的 key 不一致三是路径匹配用错了方法比如用了 startsWith(/index)实际请求 /index.jsp 时得到的是 /index.jsp配了查询参数也不影响但大小写、多余的斜杠会造成问题。解决统一 key比如都用 loginUser在 Filter 里加一行 System.out.println(user user , path path)眼睛看一遍实际值。另外重定向回登录页时可以带一个参数比如 /login.jsp?redirectindex.jsp登录成功后跳回原目标页这个体验细节可以自己加。最容易翻车的是把 Filter 的放行条件写成 user ! null publicPath条件顺序反过来未登录访问登录页也会被拦自己却毫无察觉。5.5 用 Tomcat 10 跑旧代码控制台报 NoClassDefFoundError现象同样的代码在别人电脑上跑得欢自己装了个最新版 Tomcat运行时报 java.lang.NoClassDefFoundError: javax/servlet/ServletException。原因Tomcat 10 开始把包名从 javax.servlet 迁移到了 jakarta.servlet。旧代码的 import javax.servlet.* 在编译期可能通过因为 IDE 里带了 servlet-api 依赖但 Tomcat 10 容器里只有 jakarta 包运行期就找不到类。这是 JavaWeb 项目里的经典版本翻车点。解决课程设计和老项目强烈建议用 Tomcat 9 JDK 8/11。如果非要上 Tomcat 10把源码中所有 javax.servlet 批量替换为 jakarta.servlet并且 pom.xml 里的依赖也换成 jakarta.servlet-api 5.x。另外注意 servlet-api 的 scope 必须是 provided否则打包进 war 后和容器自带版本冲突可能出现方法签名不一致的 AbstractMethodError。6. 让登录注册在生产可用验证码、限流与验收清单6.1 加一道图形验证码图形验证码是防暴力破解的第一道闸Java 2D 三十行左右就能画不引第三方库。生成时把随机字符串画进 BufferedImage同时把值存进 Session提交时比 Session 里的值不区分大小写比对完立即 removeAttribute。关键点是生成和校验必须在同一个 Session 里页面刷新时不要顺手把 Session 里的验证码清掉。6.2 登录失败次数限制比验证码更粗粒度的是失败次数限制。可以在 Session 里维护 failCount连续失败五次锁定十五分钟。但 Session 锁不住攻击者清 Cookie生产上要用 Redis 以用户名为 key设 TTL 过期。代码逻辑不复杂失败时计数加一超过阈值返回“尝试次数过多”成功或超时时删除计数。它挡的是慢速枚举不是分布式撞库别当银弹用。6.3 验收清单提交前过一遍这七项检查项预期结果注册成功跳转登录页地址栏是 /login.jsp重复用户名注册提示已被注册不报 SQL 异常未登录访问主页跳回登录页而不是 404退出后再访问主页被重新拦截中文用户名密码不乱码、能正常登录密码存储数据库里不是明文连续输错五次15 分钟内被锁定这七项我每次交项目前都会过一遍重点是第二行和第六行——用户名唯一和密码不存明文这两条是最容易被验收老师追问的点。最后提一条个人习惯改完接口先用 curl 或抓包工具看一眼响应头和 Set-Cookie别只盯着浏览器页面。登录注册这类项目的问题大多藏在请求生命周期里把抓包变成调试习惯后面接触任何带登录的框架都会顺很多。希望帮到你。本文还有配套的精品资源点击获取
返回列表