Struts2登录注册实战:从工程搭建到拦截器鉴权的完整指南
发布时间:2026/10/17 12:08:51 锦皓数字建站

简介这是一套基于Struts2完成的用户登录、注册与信息管理实战项目面向正在学习Java Web与MVC架构的高校学生、初级开发人员以及毕业设计使用者可作为课程设计或框架入门参考。项目覆盖Struts2框架的核心运用包括Action与Result跳转、拦截器配置、表单验证、异常处理、用户认证与授权等关键环节并基于SQL Server存储用户数据配套JSP页面和数据库文件便于运行后对照检查。压缩包共74个文件主要包含Java源码、JSP视图、XML配置文件、jar依赖库、class编译产物以及gif操作图解、doc说明文档和mdf/ldf数据库文件整体体积仅4.27MB结构紧凑、目录划分明确适合快速导入开发环境。已有843人学习下载读者可获得完整的项目目录结构、数据库表设计以及Struts2的配置思路尤其适合逐模块拆解登录认证、注册校验和信息管理流程加深对框架执行链路的理解。1. Struts2 登陆注册维护老项目先拆一套用户管理系统Struts2 登陆注册模块是接手旧系统时最先被要求动刀的地方。这框架在新项目里少见但在某公司内部账号系统、某高校的老选课平台里仍跑得好好的。第一次点开一堆.action请求时容易懵表单参数怎么封装、页面为什么跳错、用户为什么串号全靠黑匣子猜。这套“登录注册及用户信息管理系统”是我改过的典型样例适合维护老系统的开发者也适合还在学 SSH 的初学者。拆完你会发现表单封装、拦截器鉴权、结果转发、参数校验是一条完整链路换成其他 MVC 框架思路照样通用。2. 搭建工程骨架从依赖到 struts.xml 的核心配置2.1 依赖与目录一个能跑起来的 Struts2 工程长什么样很多老项目没有统一依赖管理WEB-INF/lib下塞了几十个 jar 包缺一个类根本查不出是哪来的。我接手这类项目时习惯先按 Maven 工程把依赖理清楚编译报错能定位到具体 jar排查效率立刻不一样。下面是这套系统常用的依赖组合版本号按你本地仓库实际解析到的稳定版为准dependencies !-- Struts2 核心包 -- dependency groupIdorg.apache.struts/groupId artifactIdstruts2-core/artifactId version2.5.30/version /dependency !-- 持久层我用的是 MyBatis老项目里 Hibernate 也常见二选一 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.16/version /dependency !-- MySQL 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency !-- Servlet API 由 Tomcat 提供编译期需要 -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency /dependencies参数说明struts2-core是必须的2.5.x 之后包内结构变化不小别拿 2.3 版本的 jar 硬套。MyBatis 和 MySQL 驱动按你实际数据库调整如果源项目用 Hibernate把mybatis依赖换成对应版本即可。provided表示 Servlet API 只在编译期参与运行时由 Tomcat 提供避免和服务器自带的类冲突。工程结构我习惯按 action、service、mapper、model 四层切src/main/java com.example.user.action UserAction.java com.example.user.service UserService.java com.example.user.service.impl UserServiceImpl.java com.example.user.mapper UserMapper.java com.example.user.model User.java src/main/resources struts.xml mybatis-config.xml mapper UserMapper.xml src/main/webapp WEB-INF web.xml login.jsp register.jsp user list.jsp这套划分的逻辑是action只做参数接收和跳转不写 SQLservice放业务规则比如用户名重复判断、密码散列mapper层负责和数据库交互。初学者容易把业务代码全塞进 Action短期跑得通一旦加需求比如注册时要同时初始化默认角色Action 会迅速膨胀后期维护很痛苦。目录结构确定后下一步就是核心配置文件。2.2 struts.xmlnamespace、Action 映射与 result 的跳转逻辑struts.xml 是整个 MVC 路由的中枢。请求进来后Struts2 根据namespace action name找到对应 Action 类和方法方法返回字符串再根据返回值找 result。看一个最简配置?xml version1.0 encodingUTF-8? !DOCTYPE struts PUBLIC -//Apache Software Foundation//DTD Struts Configuration 2.5//EN http://struts.apache.org/dtds/struts-2.5.dtd struts !-- 开发阶段开 devMode上线前务必关掉 -- constant namestruts.devMode valuetrue / constant namestruts.i18n.encoding valueUTF-8 / package nameuser namespace/user extendsstruts-default action namelogin classcom.example.user.action.UserAction methodlogin result namesuccess/user/list.jsp/result result nameinput/login.jsp/result result nameerror/login.jsp/result /action action nameregister classcom.example.user.action.UserAction methodregister result namesuccess typeredirectActionlist/result result nameinput/register.jsp/result /action action namelist classcom.example.user.action.UserAction methodlist result namesuccess/user/list.jsp/result /action /package /struts逻辑说明登录成功后跳/user/list.jsp失败则回到登录页并携带fieldError。注册成功用redirectAction而不是直接转发到 list这是为了避免用户按 F5 时重复提交注册请求。redirectAction会再发一次新的请求到list此时 session 里已经有登录用户不走注册逻辑。参数说明namespace/user意味着访问路径是/user/login.actionJSP 表单里的action要写成对应路径少一个斜杠都会 404。method指定调用 Action 里的哪个方法2.5 版本默认关闭了动态方法调用不要在代码里靠请求参数拼方法名规范做法是每个业务方法在 struts.xml 显式映射一个action。result name必须和 Action 返回值完全一致大小写敏感这是新人最容易踩的坑。2.3 web.xml 入口StrutsPrepareAndExecuteFilter 和旧版本的差异Struts2 通过过滤器接管所有请求web.xml 里要配置过滤器映射。2.5 版本必须用StrutsPrepareAndExecuteFilter老教程里的FilterDispatcher在新版已经被移除复制旧代码会出现ClassNotFoundException。filter filter-namestruts2/filter-name filter-classorg.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter/filter-class /filter filter-mapping filter-namestruts2/filter-name url-pattern/*/url-pattern /filter-mapping参数说明url-pattern必须写成/*不能写*.action。写成*.action时静态资源不受影响但如果你有直接转发到 JSP 的场景Struts2 过滤器不参与struts.xml里的拦截器栈也不会执行导致登录校验被绕过。这是个隐蔽问题开发时功能正常部署后某些页面却不需要登录就能访问。还有一点过滤器加上后JSP 路径默认受 Struts2 控制访问/login.jsp会直接放行因为 Struts2 对 JSP 文件本身不拦截真正拦的是.action请求链路。到这里工程骨架已经能启动了。配置上最容易出问题的就是 result 名和过滤器新旧类这两处处理完后面写业务逻辑会顺畅很多。3. 登录注册逻辑实现从 JSP 表单到数据库落库的完整链路3.1 用户表设计密码字段和状态字段的约定登录注册系统最核心的表就是用户表。字段设计直接影响后续业务扩展比如用户禁用、软删除、记住登录状态都需要提前留位置。建表语句如下CREATE TABLE t_user ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL COMMENT 登录名, password VARCHAR(128) NOT NULL COMMENT 加盐后的密文, salt VARCHAR(32) DEFAULT NULL COMMENT 每个用户独立的盐值, real_name VARCHAR(50) DEFAULT NULL COMMENT 姓名, status TINYINT NOT NULL DEFAULT 1 COMMENT 1启用 0禁用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字段说明password长度设成 128 而不是常见的 32 位因为老项目里经常从 MD5 升级到加盐 MD5再到 SHA-256字段长度不足会直接报数据截断。salt单独存每个用户随机生成不能所有账号共用同一个盐。status用于账号禁用比删除用户安全以后要恢复也方便。uk_username是数据库层面的唯一约束代码里查重只是第一道防线并发请求下只有唯一索引能兜底。密码存储这里我要多说一句网上很多 Struts2 老教程直接MD5(password)存库这个习惯要改。MD5 本身不可逆但彩虹表查得快必须加盐。常见做法是生成 16 位随机盐拼到明文密码后面再算摘要。// 生成盐值 String salt UUID.randomUUID().toString().replace(-, ).substring(0, 16); // 加盐后散列 String hashed DigestUtils.md5Hex(salt rawPassword); // 入库时同时保存 salt 和 hashed参数说明DigestUtils来自 commons-codec老项目里基本都有这个依赖。盐值长度 16 位足够UUID去掉横杠截断后随机性有保障。散列结果存password字段盐存salt字段登录校验时先按用户名查出salt再对输入密码做同样拼接和散列最后比对密文。3.2 登录逻辑参数封装、校验与 Session 写入Struts2 的 Action 类本身就是一个 POJO请求参数会自动封装到同名字段。比如登录表单里有username和passwordUserAction 里声明这两个字段并生成 getter/setter 就能拿到值。这种设计写起来省事但也要求 Action 字段不能用于跨请求存储。登录方法的核心代码public class UserAction extends ActionSupport { private UserService userService new UserService(); private String username; private String password; private User user; private ListUser userList; // getter/setter 省略 public String login() { // 提交参数先做一次非空校验避免 null 进入 SQL if (username null || username.trim().isEmpty() || password null || password.isEmpty()) { addFieldError(username, 用户名和密码不能为空); return INPUT; } User query userService.login(username, password); if (query null) { addFieldError(password, 用户名或密码错误); return INPUT; } if (query.getStatus() 0) { addFieldError(username, 账号已被禁用); return INPUT; } // 登录成功后把用户对象放进 session ServletActionContext.getRequest().getSession() .setAttribute(loginUser, query); return SUCCESS; } }逻辑说明addFieldError存入的是字段错误信息Struts2 会把错误带到inputresult 对应的页面JSP 里用s:fielderror/展示。返回INPUT后struts.xml里的loginaction 配置会自动跳回/login.jsp不需要手动 redirect。ServletActionContext.getRequest()是 Struts2 提供的获取原生 Request 的方式虽然不推荐在 Action 里直接用 Servlet API但写入 Session 时这种写法最直观老项目里也最普遍。参数说明UserService.login()内部先按 username 查出用户再比对散列密码。如果查不到用户和密码错误返回同一个提示避免暴露“用户名不存在”的信息。status 0单独判断禁用账号即使密码正确也不能登录这里返回INPUT会带出错误提示。Session 里存的是整个User对象后续拦截器判断键名loginUser是否存在就行这个键名前后端要统一。3.3 注册逻辑重名校验与数据落库注册比登录多一个“写入”步骤核心要处理两件事用户名不能重复、密码不能明文入库。代码结构如下public String register() { if (username null || username.trim().isEmpty() || password null || password.isEmpty()) { addFieldError(username, 用户名和密码不能为空); return INPUT; } if (userService.exists(username)) { addFieldError(username, 用户名已被占用); return INPUT; } userService.register(username, password, realName); return SUCCESS; }逻辑说明先做非空校验再做重名校验这是最基础的两层防线。exists方法对应SELECT COUNT(*) FROM t_user WHERE username ?比查出整个对象再判断省内存。注册成功后返回SUCCESSstruts.xml 里配置的是redirectAction会重新发请求到 list 页面。这样做的原因在第 2 章提过直接转发的话用户按 F5 会重复提交注册请求生成两条一样的账号数据。参数说明register方法里没有写散列逻辑实际散列放在userService.register()内部Action 只管参数和跳转。这样拆分的好处是未来把密码算法从 MD5 换成 BCrypt只改 Service 一个地方Action 和 JSP 都不用动。realName是可选字段允许为空但 getter/setter 必须存在否则 Struts2 封装参数时会静默跳过。3.4 用户信息管理列表、禁用与启用登录成功后的主界面通常是用户列表管理端最基础的操作是查看用户、禁用账号、重新启用。列表查询代码public String list() { userList userService.findAll(); return SUCCESS; }逻辑说明findAll返回全部用户数据量小时没问题。用户量超过几百条后要加分页常见做法是给listaction 增加pageNo和pageSize两个字段findAll(pageNo, pageSize)底层用LIMIT查询同时返回总记录数。禁用和启用一般做成一个方法根据 ID 和状态值更新public String toggleStatus() { userService.updateStatus(id, status); return SUCCESS; }参数说明status的值由页面提交0 表示禁用1 表示启用。这里要注意权限问题任何登录用户都能调toggleStatus接口的话普通用户可以直接把管理员账号禁掉。老项目里常靠页面隐藏按钮来限制这在技术上等于没有防护。正确做法是加角色字段或权限拦截器在 Action 方法调用前判断当前 session 用户的角色这个点放到第 5 章展开。4. 避坑与常见问题排查四个高频故障现场4.1 提交后 404 或 No result defined for action现象登录页填好账号密码点击登录地址栏变成/user/login.actionTomcat 返回 404控制台出现No result defined for action或There is no Action mapped for namespace / and action name login。原因最常见的是 result name 和 Action 返回值不匹配。比如 Action 里return inputstruts.xml 里写的是nameINPUT大小写不一致直接找不到 result。其次是表单的action路径写错比如写了login.action但 struts.xml 里 namespace 是/user完整路径应该是/user/login.action。解决先在 Action 方法里临时打印返回值比如System.out.println(result result)确定字符串内容再回 struts.xml 核对。我一般会在 package 里加一个全局兜底global-results result nameerror/error.jsp/result /global-results这样即使漏配了某个 result至少不会白屏控制台错误信息也会更明确。路径问题则用浏览器开发者工具看 Network 里的请求 URL对比 struts.xml 的 namespace 一眼就能查出来。4.2 中文乱码POST、GET 和数据库三层编码不一致现象注册时输入“张三”保存到数据库变成“???”或者登录页显示正常查询结果里的中文全是乱码。原因这是三处编码不一致叠加出来的。第一处JSP 页面没声明 UTF-8浏览器按默认编码提交表单第二处struts.xml 里struts.i18n.encoding没设置Struts2 对 POST 参数按默认编码解析第三处JDBC 连接串没带characterEncoding数据到 MySQL 后以错误编码存储。三处只要有一处不对显示就会乱。解决按顺序确认三个位置。JSP 头部% page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8 %struts.xml 里设置constant namestruts.i18n.encoding valueUTF-8 /JDBC 连接串jdbc:mysql://localhost:3306/userdb?useUnicodetruecharacterEncodingUTF-8注意struts.i18n.encoding只对 POST 请求生效GET 参数编码由 Tomcat 决定。Tomcat 8 及以上版本默认 GET 就是 UTF-8老版本 Tomcat 需要手动指定URIEncodingUTF-8。排查顺序建议是先看请求头里的 Content-Type再查数据库连接串最后看 JSP 声明大部分情况是 JSP 忘记加pageEncoding。4.3 用户串号A 登录后看到了 B 的数据现象A 登录后进入用户列表下拉选择“只看自己创建的数据”结果列表里包含 B 的数据或者 A 编辑自己的资料保存后 B 的资料被改了。原因这是经典的“共享可变字段”问题。Struts2 的 Action 本身是每次请求 new 一个实例Action 字段不会跨请求共享真正翻车点在 Service 或 DAO。有些老代码为了让查询条件在多个方法间传递把查询关键字写成 Service 的成员变量。Service 是单例A 请求把userId1001存进去B 请求还没等赋值就被 A 的覆盖查出来自然是别人的数据。解决请求级别的参数只能放 Action 字段或者作为方法参数一层层传下去不能存在 Service 实例字段里。检查代码时重点找 Service 类里有没有非静态成员变量比如private String currentUserId; // 这个字段出现在单例 Service 里就是隐患如果有全部改成方法参数。还有一个隐蔽情况static集合被多个 Action 共用同样会造成串号。改完以后用两个浏览器账号同时操作一个提交后立刻看另一个的页面数据不串就是修好了。4.4 数据库连接池耗尽系统跑一段时间后 Tomcat 卡死现象系统上线后每天下午开始变慢最终页面打不开日志里出现Connection is not available, request timed out重启 Tomcat 后恢复隔天又复发。原因连接没有被归还。老式 JDBC 代码里开发者关闭了ResultSet和Statement但忘记关闭Connection连接池里的连接被慢慢耗尽。MyBatis 场景下SqlSession没有关闭也会占用连接。这类问题前期无感流量上来后集中爆发排查时看数据库连接数会发现大量Sleep状态连接。解决手写 JDBC 时用finally块确保关闭所有资源MyBatis 优先用 try-with-resourcestry (SqlSession session sqlSessionFactory.openSession(true)) { UserMapper mapper session.getMapper(UserMapper.class); User query mapper.selectByUsername(username); // 其他操作 }openSession(true)表示自动提交资源会在 try 块结束时自动关闭。如果用的是连接池给 DBCP 或 HikariCP 配好最大连接数和空闲回收时间比如最大 20 个连接、空闲 5 分钟回收。排查时执行SHOW PROCESSLIST;看到大量Sleep状态的连接且时间很长基本可以断定是代码里漏了 close。从那次血泪经验后我每次写 DAO 层都会强制检查连接资源的关闭链路不过度依赖连接池的自动回收。5. 进阶用拦截器把登录鉴权和防重复提交做进框架5.1 登录拦截器拦截器栈怎么配才不被绕过登录注册系统里“未登录用户不能访问用户列表”这个约束应该由框架统一处理不是在每个 JSP 里判断 session。Struts2 的拦截器正好干这件事。自定义拦截器public class LoginInterceptor implements Interceptor { public void init() {} public void destroy() {} public String intercept(ActionInvocation invocation) throws Exception { MapString, Object session invocation.getInvocationContext().getSession(); Object loginUser session.get(loginUser); if (loginUser null) { return login; } return invocation.invoke(); } }逻辑说明取不到loginUser就返回login这个字符串需要在 struts.xml 里定义 result。invoke()放行后才会执行真正的 Action 方法所有受保护接口必须在放行后才处理避免业务代码做重复校验。配置拦截器栈注意登录 action 要跳过登录检查package nameuser namespace/user extendsstruts-default interceptors interceptor namelogin classcom.example.user.interceptor.LoginInterceptor / interceptor-stack nameuserStack interceptor-ref namedefaultStack / interceptor-ref namelogin / /interceptor-stack /interceptors default-interceptor-ref nameuserStack / action namelogin classcom.example.user.action.UserAction methodlogin interceptor-ref namedefaultStack / result namesuccess typeredirectActionlist/result result nameinput/login.jsp/result /action action namelist classcom.example.user.action.UserAction methodlist result namelogin/login.jsp/result result namesuccess/user/list.jsp/result /action /package参数说明default-interceptor-ref设置了包内所有 action 默认走userStack登录 action 自己重新指定defaultStack就是把自己排除在登录检查之外。result namelogin是拦截器返回的字符串对应跳转登录页。这个设计能把“是否登录”和“登录后做什么”完全分开后续新增的 action 默认受保护不需要每个方法手写判断。5.2 token 拦截器注册按钮连点两次的后悔药注册功能还有一类高频问题用户点击提交后页面没跳转又点了一次生成两条记录。Struts2 自带 token 拦截器专门治这个。注册表单里加标签s:token /struts.xml 里给注册 action 追加 token 拦截器action nameregister classcom.example.user.action.UserAction methodregister interceptor-ref namedefaultStack / interceptor-ref nametoken / result nameinvalid.token/register.jsp/result result namesuccess typeredirectActionlist/result result nameinput/register.jsp/result /action逻辑说明s:token/会在页面生成隐藏字段值存进 session。第二次提交时 token 值和 session 里的不一致拦截器直接返回invalid.token连 Action 方法都不执行。从那以后我接老系统第一件事不是读业务代码而是先画拦截器栈和 result 图谱确认哪些 action 受保护、哪些放行。这套 Struts2 项目拆完再碰到表单重复提交、用户串号、连接池耗尽基本能凭经验快速定位希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。