JavaWeb登录注册项目实战:Servlet、Session、JDBC与密码安全

JavaWeb登录注册项目实战:Servlet、Session、JDBC与密码安全 简介基于JavaWeb的登录注册案例面向正在学习JSP、Servlet与MySQL整合开发的Java初学者可用于课程设计或毕业设计中的用户认证环节。压缩包共34个文件约3.56MB包含7个Java源文件与对应class字节码、3个JSP页面、1份SQL建表脚本、1个依赖JAR包以及XML和properties配置、界面图片、Eclipse工程配置等目录结构完整便于直接导入运行也方便对照前端页面、后端逻辑与数据库脚本的映射关系。已有3047人学习浏览。资源实现了完整的注册与登录流程前端JSP表单采集数据后端Servlet通过doPost与doGet处理请求再经JDBC操作MySQL数据库完成用户查询与插入同时覆盖登录失败的错误返回、注册时用户名重复校验、密码加密存储、参数化查询防SQL注入、登录后Session会话跟踪等关键细节。读者可在此基础上扩展邮件确认、用户管理等功能用于深入理解Web全栈开发。 一个看似基础却几乎能串联起整个JavaWeb知识体系的项目就是登录和注册。说实话做了这么多年Java开发带过不少新人也看过很多教学项目登录注册永远是被问得最多、也最容易写崩的东西。很多人觉得自己会写SQL、会写Servlet就能把登录做好结果一上线全是漏洞密码明文存储、Session根本没用对、乱码问题能把人逼疯、稍微改个密码就登录失败。这个项目看似只有两个页面、几十行代码实际上它涵盖了JavaWeb开发中最核心的环节请求与响应、表单处理、数据库交互、会话机制、参数校验、异常排查。把它彻底吃透后面学SSM、SpringBoot会轻松很多因为你已经理解了Web应用最本质的那套运行逻辑。这篇文章我不打算只给你贴一堆代码而是把我实际做这个项目时的完整思路、每一步的考量、以及踩过的坑都拆开来讲。不管你是刚学完JavaSE、准备接触JavaWeb的在校生还是需要快速上手Web开发的转行者这份内容都能让你少走不少弯路。1. 登录注册项目的整体设计与思路拆解1.1 这个项目到底在解决什么问题登录注册的核心业务逻辑只有一句话确认“你是谁”然后让系统记住你。但落实到你实际要写的代码里就会拆出几个非常明确的需求点注册用户提交账号密码系统校验合法性确认账号没被注册过然后把数据安全落库。登录用户提交账号密码系统跟数据库里存的信息做比对通过之后建立会话Session保持登录状态。访问控制未登录的用户不能访问需要认证的页面登录之后能访问并能识别出是哪个用户。这三个点讲起来简单却覆盖了JavaWeb开发中最核心的技能表单数据的获取、Servlet的请求分发、JDBC操作MySQL、密码的安全处理、Session的生命周期管理、Filter做统一的登录校验。我见过不少新手一上来就纠结用什么框架。其实做这类项目最合适的就是最传统的Servlet JSP JDBC这套组合原因有两点第一它是JavaWeb最基础的运行模型能让你看清底层机制——请求怎么进来的、数据怎么流转的、响应怎么返回的这些如果被框架封装掉了你后期排查问题会非常痛苦第二这个项目体量不大用重量级框架会很臃肿纯粹是为了用技术而用技术。1.2 技术选型与目录结构规划先说版本和环境这是很多新手容易卡住的地方。我使用的环境是JDK 1.8IDEA 2023版本社区版和专业版均可Tomcat 8.5或9.0MySQL 5.7或8.0Servlet 4.0 JSP 2.3Maven用于依赖管理为什么推荐用Maven而不是直接在WEB-INF/lib下扔jar包因为Servlet和JSP的依赖如果在IDEA里手动导经常会遇到版本冲突、依赖缺失的问题Maven可以统一管理版本后面想加JDBC驱动、连接池之类的依赖一行坐标就搞定。目录结构上我按照Maven的标准Web工程来建核心结构如下src/main/java ├── com.example.servlet -- Servlet接口层处理请求分发 ├── com.example.service -- 业务逻辑层 ├── com.example.dao -- 数据访问层JDBC操作 ├── com.example.entity -- 实体类 ├── com.example.util -- 工具类DB连接、MD5加密等 └── com.example.filter -- 过滤器登录状态校验 src/main/webapp ├── WEB-INF -- 受保护目录放JSP页面 │ ├── login.jsp │ ├── register.jsp │ └── index.jsp这样分层的逻辑很直白Servlet只负责接收请求和跳转页面不写业务SQLService管业务规则比如密码校验DAO管数据库操作。新手最容易犯的错误就是所有代码全塞在Servlet里——你别笑我见过很多人写的Servlet里既有SQL语句又有页面拼接逻辑后期改个需求头都大了。分层不是为了装架构而是为了让你改代码的时候只动该动的地方。2. 核心细节解析会话机制、密码存储与数据校验2.1 Session到底该怎么用才算用对了登录成功之后怎么让系统认识你这是Session存在的意义。Session保存在服务器端每个客户端通过Cookie里存的JSESSIONID来关联自己的会话数据。我不止一次在代码评审里看到有人这样写“登录成功把用户名存到request作用域里”。这样写百分之百是错的因为request是一次请求内有效请求一结束数据就没了。正确的做法是存到Session里// 登录成功后 HttpSession session request.getSession(); session.setAttribute(user, user);取的时候也一样HttpSession session request.getSession(); User user (User) session.getAttribute(user); if (user null) { // 未登录跳回登录页 response.sendRedirect(request.getContextPath() /login.jsp); return; }这里有一个容易被忽略的细节getSession()和getSession(true)的底层行为。如果当前没有SessiongetSession()默认会新建一个。在某些校验场景下比如判断是否登录如果你调用了getSession()本来未登录的用户反而会被服务器创建了一个新的Session对象白白占用内存。所以更严谨的写法是先判断HttpSession session request.getSession(false); // 没有就不创建 if (session null || session.getAttribute(user) null) { // 未登录 }这个细节看起来不起眼但它会直接影响服务端的内存占用和逻辑正确性。面试的时候被问到“Session和Cookie的区别”如果你能把这层细节讲清楚绝对比单纯背概念加分不少。2.2 密码存储千万不要明文往数据库里放做登录注册项目最不能妥协的就是密码存储。明文存密码的后果就是数据库一旦泄露所有用户的密码直接暴露即使是开发者本人也不该能看到用户的真实密码。正确的基础做法是先哈希再加盐。单纯的MD5已经不够安全了因为彩虹表太成熟。比较合适的方案是加盐的SHA-256或BCrypt。这里我给一个基于JDK自带工具类的MD5加盐实现——注意这只适用于练手项目生产级推荐BCryptpublic static String md5WithSalt(String password, String salt) { String combined password salt; MessageDigest md; try { md MessageDigest.getInstance(MD5); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } byte[] digest md.digest(combined.getBytes(StandardCharsets.UTF_8)); StringBuilder sb new StringBuilder(); for (byte b : digest) { String hex Integer.toHexString(b 0xff); if (hex.length() 1) { sb.append(0); } sb.append(hex); } return sb.toString(); }盐怎么生成最简单可靠的方式是用UUID的随机部分String salt UUID.randomUUID().toString().replace(-, );最终数据库里存的是两个字段盐salt和密文password。校验的时候拿用户输入的密码加上库里对应的盐再做一次相同哈希比对是否一致。这样做的好处是即便两个用户的密码完全相同因为盐不同最后存的密文也不同极大提高了安全性。2.3 表单校验前端校验是体验后端校验才是安全很多新手只会做前端校验——用JS检查一下空值就完事。但直接拿HTTP请求工具比如Postman就能绕开前端、直接向服务端发送非法请求。所以后端校验永远不能省略。后端校验的核心逻辑在注册流程中包括用户名不能为空、长度限制比如4-16个字符用户名不能重复查一次数据库密码长度至少6位确认密码要和密码一致邮箱格式是否正确如果有这些逻辑建议抽成一个独立的方法public MapString, String validateRegister(String username, String password, String confirmPassword) { MapString, String errors new HashMap(); if (username null || username.trim().length() 4 || username.trim().length() 16) { errors.put(username, 用户名长度必须在4到16个字符之间); } if (password null || password.length() 6) { errors.put(password, 密码长度不能少于6位); } if (!password.equals(confirmPassword)) { errors.put(confirmPassword, 两次输入的密码不一致); } return errors; }前端校验可以快速提示用户少填了东西但后端校验才是最终的防线。项目里前后端校验都要有一个是体验一个是安全。3. 实操过程从建表到跑通完整登录注册流程3.1 第一步创建JavaWeb项目骨架我直接用IDEA 2023来演示这是目前大多数人在用的版本。新建项目的步骤如下打开IDEA选择File - New - Project。左侧选择Maven不勾选Create from archetype手动补齐Web结构。设置GroupId为com.exampleArtifactId为login-demo。项目创建后右键src/main/java选择New - Directory命名为webapp然后在资源根目录下手动创建WEB-INF/web.xml。然后需要在pom.xml里声明打包方式和依赖。这里的坑在于如果用非Maven的普通方式很多IDEA版本尤其是2023默认不会帮你生成web.xml需要手动创建并且在pom.xml里声明packagingwar/packaging否则Tomcat配置的时候识别不到Web应用结构。packagingwar/packaging dependencies dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdjavax.servlet/groupId artifactIdjsp-api/artifactId version2.0/version scopeprovided/scope /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency /dependenciesscope设置为provided是因为Tomcat本身已经带了Servlet和JSP的实现如果编译时不去掉会导致冲突。这一步没配好启动Tomcat时就会经常报ClassNotFoundException或者NoClassDefFoundError别问我怎么知道的。3.2 第二步设计数据库表结构注册登录项目一张表就够了但字段设计有讲究。我在实际中用的是CREATE TABLE t_user ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(16) NOT NULL, password VARCHAR(64) NOT NULL COMMENT 加盐后的密码密文, salt VARCHAR(32) NOT NULL COMMENT 随机盐值, email VARCHAR(64) DEFAULT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有两个关键点必须强调字符集必须用utf8mb4而不是utf8。因为MySQL的utf8无法存储emoji以及部分生僻汉字你注册一个“张三”这种带表情的用户名直接报Incorrect string value错误。username必须建立唯一索引。去重不能靠代码里的SELECT COUNT(*)先查一遍因为并发情况下可能会有两个请求同时查到“用户名可用”然后同时插入造成脏数据。唯一索引才是最后的兜底防线。3.3 第三步数据库连接工具类的编写JDBC连接数据库的样板代码没必要在每个DAO里重复写抽一个工具类出来。我习惯用简单的DriverManager方式练手项目足够生产级应用建议换成Druid或者HikariCP连接池。public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/login_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USER root; private static final String PASSWORD 你的数据库密码; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { try { if (rs ! null) rs.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (ps ! null) ps.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (conn ! null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }连接串中的几个参数都踩过坑专门说一下。serverTimezoneAsia/Shanghai不配的话MySQL 8.0大概率会报Server time zone的异常characterEncodingutf8不配的话中文数据交互容易乱码useSSLfalse去掉也可以但在本机开发时配置上能避免一堆SSL警告刷屏。3.4 第四步注册流程的完整实现注册的Servlet核心逻辑分四步接收参数、校验合法性、校验用户名唯一性、插入数据。这里我把Service和DAO的划分也一并展示了方便你照着搭层次。先是DAO层public class UserDao { public User findByUsername(String username) { String sql SELECT id, username, password, salt, email FROM t_user WHERE username ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { return mapToUser(rs); } } } catch (SQLException e) { e.printStackTrace(); } return null; } public int insertUser(String username, String password, String salt, String email) { String sql INSERT INTO t_user (username, password, salt, email) VALUES (?, ?, ?, ?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); ps.setString(3, salt); ps.setString(4, email); return ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } return 0; } }注意这里用了PreparedStatement强烈建议执行任何SQL都用它而不是Statement最大的原因是防SQL注入。比如登录时如果有人输入 or 11这种万能密码用字符串拼接SQL的话直接就能绕过密码验证登录进去。然后是Service层处理业务规则public class UserService { private UserDao userDao new UserDao(); // 注册返回成功或者失败原因 public String register(String username, String rawPassword, String confirmPassword, String email) { if (username null || username.trim().length() 4 || username.trim().length() 16) { return 用户名长度必须在4到16个字符之间; } if (rawPassword null || rawPassword.length() 6) { return 密码长度不能少于6位; } if (!rawPassword.equals(confirmPassword)) { return 两次输入的密码不一致; } if (userDao.findByUsername(username) ! null) { return 用户名已被注册; } String salt UUID.randomUUID().toString().replace(-, ); String encodedPassword PasswordUtil.md5WithSalt(rawPassword, salt); int rows userDao.insertUser(username, encodedPassword, salt, email); return rows 0 ? null : 注册失败请稍后重试; } }最后是Servlet层WebServlet(/register) public class RegisterServlet extends HttpServlet { private UserService userService new UserService(); Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String username request.getParameter(username); String password request.getParameter(password); String confirmPassword request.getParameter(confirmPassword); String email request.getParameter(email); String error userService.register(username, password, confirmPassword, email); if (error ! null) { request.setAttribute(error, error); request.getRequestDispatcher(/register.jsp).forward(request, response); } else { response.sendRedirect(request.getContextPath() /login.jsp); } } }request.setCharacterEncoding(UTF-8)这行必须放在getParameter之前否则POST请求的中文参数就会乱码。这个细节掉过无数次坑了一会儿在问题排查那节细讲。3.5 第五步登录流程与Session的配合登录接口和注册的最大区别在于成功后要写Session并且要把密码的处理换成“取盐-加盐-哈希-比对”。直接在Service里写public User login(String username, String rawPassword) { User user userDao.findByUsername(username); if (user null) { return null; } String encodedPassword PasswordUtil.md5WithSalt(rawPassword, user.getSalt()); if (!encodedPassword.equals(user.getPassword())) { return null; } return user; }这里有个细节判断逻辑里findByUsername和密码比对是分开的两步这样在登录时不方便统一提示“用户名不存在”还是“密码错误”。从安全角度来说统一提示“用户名或密码错误”更合理防止攻击者通过登录提示来枚举已注册的用户名。但对于练手项目分开也问题不大你可以自己决定。Servlet里的处理方式WebServlet(/login) public class LoginServlet extends HttpServlet { private UserService userService new UserService(); Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String username request.getParameter(username); String password request.getParameter(password); User user userService.login(username, password); if (user ! null) { HttpSession session request.getSession(); session.setAttribute(user, user); response.sendRedirect(request.getContextPath() /index.jsp); } else { request.setAttribute(error, 用户名或密码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); } } }登录成功之后用sendRedirect而不是forward这里也是刻意的。forward是服务器内部转发地址栏不会变用户刷新页面会导致重复提交表单sendRedirect是重定向地址栏变成目标地址刷新不会重复提交。表单提交类的请求成功之后统一用重定向这是Web开发的基本素养。3.6 用Filter统一做登录状态校验有了Session还不够你得强制每个受保护页面都校验登录状态。新手如果每个页面都手动写一遍校验逻辑代码冗余不说还容易漏。标准的解法是写一个LoginFilter利用Servlet的Filter机制统一拦截请求。WebFilter(/*) public class LoginFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; String uri req.getRequestURI(); // 放行静态资源和登录注册相关页面 if (uri.indexOf(/login.jsp) -1 || uri.indexOf(/register.jsp) -1 || uri.indexOf(/login) -1 || uri.indexOf(/register) -1 || uri.endsWith(.css) || uri.endsWith(.js) || uri.endsWith(.jpg)) { chain.doFilter(request, response); return; } HttpSession session req.getSession(false); if (session ! null session.getAttribute(user) ! null) { chain.doFilter(request, response); } else { resp.sendRedirect(req.getContextPath() /login.jsp); } } }如果你是用注解方式配Filter注意URL匹配规则是/*而不是/写反了会导致Filter不生效。另外req.getSession(false)这个写法刚才讲过原因——避免给未登录的用户白白创建Session。4. 常见问题与排查技巧实录4.1 登录失败明明数据库有这条数据却登录不了这是我被问得最多的问题。常规排查路径是这样的第一步确认用户是否能被查到。在Service里加一行日志输出看看findByUsername返回的User对象是不是null。如果是null说明没查到这时候去数据库执行一下同样的SQL看返回结果。查不到的可能原因表里的数据字符集和代码里的编码不一致——用户名明明是admin代码传进去的是admin但后面多了个不可见字符比如从网页复制过来的空格。第二步确认密码比对是否通过。如果User对象不为null但是登录还是失败问题多数出在密码的哈希算法上。检查数据库里存的salt和password是不是同一次注册生成的检查你取的密码是不是表单提交的那个原始值检查编码一致性。第三步确认Session是否写入成功。在LoginServlet里输出session.getId()和session.getAttribute(user)看看重定向后的页面能不能取到。如果取不到说明Tomcat的Cookie可能被禁用了或者是浏览器安全级别设置过高。4.2 IDEA 2023创建JavaWeb项目的那些坑IDEA 2023版本跟老版本相比创建Web项目的入口变化很大。很多人找不到Java Enterprise选项或者建出来的项目没有webapp目录。最稳妥的做法就是你直接用Maven骨架建一个普通Java项目然后手动补Web目录和依赖。不要觉得手动补麻烦这个过程会让你对Web应用的结构有更清晰的认识。另外一个高频问题配置Tomcat时Deployment标签页里看不到Artifact。这是因为项目还没被打成war包。在IDEA右侧的Maven面板里先执行clean package让项目打包成功然后打开Project Structure - Artifacts - - Web Application: Exploded手动添加。4.3 乱码问题的根源与统一解法乱码问题贯穿JavaWeb开发的始终核心就是三个环节的编码要保持一致页面编码、请求编码、响应编码。页面层面JSP文件头部要有这样一行% page contentTypetext/html;charsetUTF-8 languagejava %请求层面POST请求在读取参数前设置编码也就是Servlet里的request.setCharacterEncoding(UTF-8)GET请求的编码在Tomcat 8之后默认就是UTF-8基本不用处理。响应层面可以显式设置response.setContentType(text/html;charsetUTF-8);数据库层面连接串的characterEncodingutf8和表的utf8mb4字符集缺一不可。最后还有一个容易忽略的idea本身的问题IDEA的全局编码和项目编码。在Settings - Editor - File Encodings里把Global Encoding、Project Encoding、Properties Files全部设为UTF-8。这一步不设置哪怕代码里全写对了文件本身存的是GBK一样乱码。4.4 “failed to start login server”这类启动报错的排查Tomcat启动失败这个问题热词搜索里也出现了。这类报错的本质是端口被占用或者权限不足。Windows下最常见的原因是端口被占用修改Tomcat的端口conf/server.xml里的Connector port8080换成8081、8082即可。还有一种情况是“以一种访问权限不允许的方式做了一个访问套接字尝试”这多半是防火墙或者杀毒软件拦截了端口的监听把项目目录加入信任区就能解决。5. 从练手到生产这个项目还能怎么扩展做完基础的登录注册之后别急着丢到一边。我建议你按下面几个方向继续改每个方向都能学到新东西把数据库密码加密从MD5换成BCrypt。MD5加盐虽然比明文强很多但MD5本身已经被证明碰撞成本很低生产环境推荐jBCrypt或Spring Security Crypto里的BCryptPasswordEncoder每次生成的哈希都不同安全性直接拉满。加上“记住我”功能。本质是Cookie里存一个加密后的token关闭浏览器后重新打开无需登录。这里你能学到Cookie的属性设置有效期、HttpOnly、Secure和Token的生成设计。引入连接池把DriverManager换成Druid或HikariCP。连接池能显著提升高并发下的性能你能顺便理解DataSource和Connection的区别。日志记录登录日志。登录成功、失败、IP、时间、用户代理都打下来后续可以做成“异地登录提醒”“失败次数锁定”等安全功能。我个人在实际项目中的体会是登录注册是那种“写一遍觉得简单写十遍才能真正理解”的东西。每重写一次你对会话管理、参数传递、异常处理、安全防护的理解都会更深一层。不要觉得它基础就不重视恰恰是这些地基一样的东西决定了你后面能盖多高的楼。最后再分享一个小技巧调试登录问题时别在Servlet里到处打印日志先把Tomcat的日志打开conf/logging.properties配合浏览器的开发者工具F12 - Network看请求的Status Code和Response。是302重定向、还是500服务器错误、还是200成功返回这一步就能帮你排除一大半问题。把这些排查动作变成肌肉记忆你处理任何Web项目的速度都会快很多。本文还有配套的精品资源点击获取