资讯详情

资讯详情

Struts2登录注册与用户信息管理:框架原理、拦截器与避坑实践

简介这是一套基于Struts2开发的完整登录注册与用户信息管理系统工程包适合Java Web学习者、毕业设计参考者及想快速上手MVC架构的开发者。系统覆盖用户登录、注册、信息修改、退出等核心模块以Action与Result为核心配合拦截器、struts.xml配置和SQL Server数据库文件完整呈现从JSP页面提交、业务逻辑处理、数据库读写到结果返回的典型流程。压缩包共74个文件含13个Java源文件、13个class编译文件、6个JSP页面、9个XML配置、6个jar依赖库另附数据库的mdf/ldf文件、项目图片素材和一份图解文档整包仅4.27MB目录结构清晰便于按模块对照学习。资源还包含Eclipse工程配置文件可直接导入IDE运行调试目前已有843人学习下载。通过这套工程读者能掌握Struts2的表单验证、用户认证与授权、session跟踪、全局异常处理等关键机制同时理解如何配置拦截器栈、组织Action类并与SQL Server交互对独立开发类似管理系统有很强的参考价值。1. Struts2登录注册与用户信息管理先把框架的脾气摸透再动手做Java Web课程设计老师点名要求用Struts2写一个登录注册及用户信息管理系统看起来就是最基础的表单增删改查。可真到跑起来那天404、ClassNotFoundException、NoSuchMethodError、校验不生效、中文乱码毕业生在这类项目上翻车的不在少数。问题往往不在业务逻辑而在Struts2整套框架的版本搭配、配置细节和拦截器机制。这篇内容把一个Struts2实现的登录注册及用户信息管理项目拆开从框架原理讲到Action怎么写、校验怎么配、CRUD和分页怎么落地最后把S2系列漏洞和jar包冲突这类坑标出来。适合三类人赶课设的学生、接手老Struts2项目的维护者以及想补全Java Web请求链路的新手。2. Struts2框架原理与工程准备请求从哪进来又从哪出去2.1 从Servlet到Struts2三个关键节点先回顾Servlet时代。一个登录功能要写一个LoginServlet在web.xml里注册映射代码里手动getParameter、手动setAttribute、手动forward。业务一多Servlet类迅速膨胀跳转关系散落在各个doGet、doPost里没有人能一眼看清这个系统的请求脉络。Struts1尝试用ActionServlet做统一入口但和Servlet API还是绑得太紧。到了Struts2设计思路完全变了Action类不需要继承任何Servlet API类就是一个带getter/setter的普通POJO。请求处理被拆成一条拦截器链真正干活的Action只是链上的一环。我到现在仍然觉得Struts2最值得学习的不是标签库而是拦截器这个思想——后来Spring MVC的HandlerInterceptor、Shiro的过滤器链思路都同源。这里有个关键点要记牢Struts2不是Struts1的升级版它的内核是WebWork项目的XWork。所以你会看到struts2-core、xwork-core、ognl这几个核心包始终同时出现版本必须相互匹配。很多课设项目跑不起来第一步就死在jar包版本错配上这个坑后面单独说。2.2 一次请求的生命周期从HTTP到Action再回HTTP理解Struts2的请求链路排错就成功了一半。一次完整请求是这样走的浏览器发请求web.xml里配置的StrutsPrepareAndExecuteFilter过滤器拦下ActionMapper判断这个URL是否需要Struts处理需要则创建ActionProxyActionProxy按namespace和action名查找配置执行拦截器栈命中Action方法方法返回resultCode字符串框架按这个字符串在struts.xml里找对应ResultResult执行转发或重定向拦截器链的after逻辑收尾响应写回浏览器。这条链路解释了Struts2项目里一个经典现象你改了Action方法但请求还是404或者跳到错误页面原因往往不在Action而在struts.xml里的result配置或拦截器栈的执行顺序。排错要自顶而下先确认URL没被过滤器放掉再确认action名和namespace拼写最后看result跳转类型。有三个配置常量在这个阶段特别重要。struts.action.extension决定URL后缀一般配action也能配空值让URL更干净struts.i18n.encoding决定请求和响应编码统一配UTF-8struts.enable.DynamicMethodInvocation是动态方法调用开关新版本默认关闭大量老教程里的!method写法失效就是因为它。2.3 环境与jar包先列一份能跑的通关组合我建议的起步组合是JDK 1.8加Tomcat 8Struts2选2.3.16.x或2.5系列数据库用MySQL 5.7开发工具IDEA或Eclipse都行。核心jar包按这份清单来配组件版本建议作用struts2-core2.3.16.x框架核心xwork-core跟随核心版本拦截器与Action代理ognl3.x表达式求值javassist跟随依赖字节码增强freemarker跟随依赖模板渲染commons-lang33.x工具类commons-fileupload / commons-io对应版本上传与IOlog4j-api / log4j-core对应版本日志输出jar包这块我有一句忠告别自己脑补版本。Struts2每次发版依赖清单是连同Maven pom一起发布的各包之间有严格对应关系。我见过有人把struts2-core升到2.5系而xwork-core还留在2.3一启动直接NoSuchMethodError。手工导jar的话建议从对应版本的官方发布包lib目录里拷贝不要在网上随便打包下载。等Tomcat能正常启动、日志刷出框架初始化信息再开始写Action顺序不要反。3. 登录注册从零落地Action、双层校验与三层乱码3.1 注册功能落地struts.xml与Action代码先看struts.xml里的关键配置片段?xml version1.0 encodingUTF-8? !DOCTYPE struts PUBLIC -//Apache Software Foundation//DTD Struts Configuration 2.3//EN http://struts.apache.org/dtds/struts-2.3.dtd struts constant namestruts.i18n.encoding valueUTF-8/ package namedefault namespace/ extendsstruts-default action nameregister classcom.demo.action.UserAction methodregister result namesuccess/register-success.jsp/result result nameinput/register.jsp/result /action /package /strutsstruts.i18n.encodingUTF-8是第一道编码闸门放在全局最稳妥。nameregister是外部URL的访问名class指向Action类method指向类里处理注册的方法。success和input是Struts2的约定结果名业务正常走success校验失败统一回input也就是回到原表单页展示错误信息。这里不要自己发明用error来表示校验失败后面会解释原因。UserAction核心代码用属性驱动方式写public class UserAction extends ActionSupport { private String username; private String password; private String email; private UserDao userDao new UserDao(); public String register() throws Exception { if (userDao.findByUsername(username) ! null) { addFieldError(username, 用户名已存在); return INPUT; } User user new User(); user.setUsername(username); user.setPassword(password); user.setEmail(email); userDao.save(user); return SUCCESS; } // getter/setter 省略 }register()方法里的逻辑很直白先查重再组装User对象最后落库。属性驱动模式下表单字段会按同名规则自动赋值到Action属性上这是Struts2的params拦截器在方法执行前完成的。如果你觉得这种写法字段一多就很啰嗦可以实现ModelDrivenUser接口用getModel()返回User对象JSP里表单name直接写username、password不需要user.前缀。两种方式我都用过字段少用属性驱动直白字段多用ModelDriven省事但要注意ModelDriven会把模型压到值栈栈顶后续标签取值都围绕栈顶模型展开改动时牵连面更大。3.2 表单校验XML校验管格式validate()管业务Struts2支持两层校验叠加。第一层是XML声明式校验文件名必须是UserAction-register-validation.xml表示只对UserAction的register方法生效。如果写成UserAction-validation.xml则对UserAction所有方法生效。这个细节很多人写错导致校验死活不触发。?xml version1.0 encodingUTF-8? !DOCTYPE validators PUBLIC -//Apache Struts//XWork Validator 1.0.3//EN http://struts.apache.org/dtds/xwork-validator-1.0.3.dtd validators field nameusername field-validator typerequiredstring param nametrimtrue/param message用户名不能为空/message /field-validator field-validator typestringlength param nameminLength3/param param namemaxLength20/param message用户名长度必须在3到20之间/message /field-validator /field field nameemail field-validator typeemail message邮箱格式不正确/message /field-validator /field /validatorsrequiredstring会自动trim首尾空格stringlength限定长度email校验格式这些都是XWork内置校验器。校验失败时框架把错误信息放入fieldErrors结果流转向input。第二层在Action里重写validate()方法做业务校验Override public void validate() { if (userDao.findByUsername(username) ! null) { addFieldError(username, 用户名已被注册); } }XML校验管格式validate()管业务规则两层都通过才会执行真正的register()。这里有个实际教训有的同学校验失败时返回ERROR而不是input导致表单错误信息永远看不到用户还得从头重新填写。正确做法是始终返回input表单数据保留在值栈里配合JSP里的s:fielderror标签能在原表单上方展示错误用户不用从头再来。3.3 中文乱码按这三层排查中文乱码在Struts2项目里几乎是必考题。按顺序排查三层就能定位。第一层JSP页面编码。每个JSP头部统一写pageEncodingUTF-8和contentTypetext/html; charsetUTF-8不要混合GBK公共头尾和错误页也要检查。第二层请求过滤器。struts.i18n.encodingUTF-8只对Struts2自己处理的请求生效要保证它在web.xml的过滤器里排在前面。更稳妥的做法是额外配置一个CharacterEncodingFilterencoding设为UTF-8forceEncoding设为true并让它先于其他过滤器执行。第三层数据库连接串。MySQL连接串必须带useUnicodetrue和characterEncodingUTF-8否则入库照样乱码。三层统一后再看一下数据表字段的collation是不是utf8_general_ci。曾经有个项目页面显示正常查数据库全是问号最后定位到表结构collation是latin1_swedish_ci导致的。提示碰到乱码不要一次改十处改一处重启一次。乱码的根往往只有一个改多了反而不知道哪个生效。4. 用户列表、分页与增删改后端取值与结果跳转的细节4.1 用户列表与值栈Action的属性为什么JSP直接能取到登录注册做完就是用户信息管理。列表页是最先要面对的。Action侧写一个list()方法public String list() throws Exception { userList userDao.findAll(); return SUCCESS; }这里没有把userList塞进request或session而是靠getter让值栈自然暴露只要提供对应getUserList()方法JSP页面就能直接取到。这是很多新手困惑的地方以为必须setAttribute才能用实际上Struts2的值栈机制把Action本身当作数据容器。JSP页面用Struts2标签遍历% taglib prefixs uri/struts-tags % table trthID/thth用户名/thth邮箱/thth操作/th/tr s:iterator valueuserList tr tds:property valueid//td tds:property valueusername//td tds:property valueemail//td td a hrefeditUser.action?ids:property valueid/修改/a a hrefdeleteUser.action?ids:property valueid/ onclickreturn confirm(确定删除)删除/a /td /tr /s:iterator /table解释一下值栈的查找顺序s:iterator的valueuserList会在值栈里从上往下找getUserList()找到后把集合元素压入栈顶子标签property valueid直接取当前元素的getId()。取不到时再依次向上找request、session、application。这个顺序决定了JSP里哪种写法有效。遇到取值空白先确认有没有对应getter再看是不是被其他标签压栈把栈顶挤掉了。这是Struts2页面调试的高频套路。4.2 分页封装PageBean设计与SQL配合用户量一多不分页页面就卡。我习惯把分页参数封装成一个PageBeanpublic class PageBean { private int pageNo; // 当前页码从1开始 private int pageSize; // 每页条数 private int totalCount; // 总记录数 private int pageCount; // 总页数由totalCount和pageSize计算 private ListUser list; // 当前页数据 public PageBean(int pageNo, int pageSize, int totalCount) { this.pageNo pageNo; this.pageSize pageSize; this.totalCount totalCount; this.pageCount (totalCount pageSize - 1) / pageSize; } }DAO侧配合LIMIT分页public ListUser findPage(int pageNo, int pageSize) { int offset (pageNo - 1) * pageSize; String sql SELECT * FROM t_user ORDER BY id LIMIT ?, ?; // 使用PreparedStatement设置offset和pageSize }对应的Action处理逻辑public String list() { String pn ServletActionContext.getRequest().getParameter(pageNo); int pageNo 1; try { pageNo Integer.parseInt(pn); } catch (Exception e) { pageNo 1; } int pageSize 10; int totalCount userDao.count(); PageBean pageBean new PageBean(pageNo, pageSize, totalCount); if (pageNo pageBean.getPageCount()) pageNo pageBean.getPageCount(); if (pageNo 1) pageNo 1; pageBean.setList(userDao.findPage(pageNo, pageSize)); return SUCCESS; }这里两个关键点必须要说。第一pageNo从request参数来必须做非法值兜底用户手改URL是常态第二LIMIT的offset是(pageNo-1)pageSize很多初学写成pageNopageSize第二页开始就少一条错一条。JSP里上一页下一页的链接直接指向list.action?pageNo对应页码不要混用window.location跳转和form提交两种机制叠加容易出双份参数Action里取到的值会很奇怪。4.3 修改与删除路径传参和结果重定向修改操作分两步比价清晰editUser先按id查出对象回显到表单updateUser接收表单提交并更新。删除相对简单重点在result配置action namedeleteUser classcom.demo.action.UserAction methoddeleteUser result namesuccess typeredirectActionlist/result /action注意typeredirectAction。如果按默认的dispatcher转发到列表页浏览器地址栏还是deleteUser.action用户按一次F5就把同一条数据再删一次线上数据就是这么丢的。改成redirectAction让浏览器重新请求list.action刷新就是安全的。编辑保存后的跳转同理这是Struts2里转发和重定向的基本功但课设代码里十有八九用错。参数传递有一个细节要强调删除URL里的id是怎么进到UserAction的params拦截器会拿request参数匹配Action的setter所以UserAction里必须提供setId方法或者通过User模型接收否则id被静默丢弃。曾经遇到过删除操作删的总是同一行数据查了一晚上才发现是Action里压根没有id对应的setter。5. Struts2避坑指南jar包冲突、动态方法调用与S2-029漏洞5.1 jar包冲突ClassNotFound与NoSuchMethodError现象Tomcat启动时抛NoClassDefFoundError或者运行到某个拦截器时报NoSuchMethodError。原因struts2-core、xwork-core、ognl版本互不匹配或者classpath里同时存在同一个类的多个版本。Struts2的jar包依赖写死在官方pom里版本组合错一个就出问题。解决核对三件事。struts2-core和xwork-core是否来自同一次发版ognl版本是否和struts2-core对应的pom一致lib目录下有没有重复的旧jar残留。Maven项目用依赖树排查最直观mvn dependency:tree -Dincludesorg.apache.struts:*手工导jar的项目就把lib清空按官方发布包重新导入。Struts2老项目的启动异常一半以上是jar包问题这个比例不夸张。5.2 动态方法调用被禁!method语法失效的替代方案现象照老教程写/userAction!add.action能打开在自己的项目里请求直接404。原因struts.enable.DynamicMethodInvocation在某些版本和加固配置下默认关闭!method这种动态调用方式本来就不在官方推荐路径里。解决第一选择是在struts.xml里配置method属性action nameaddUser classcom.demo.action.UserAction methodadd result namesuccess/list.action/result /action第二选择是通配符简化name*User method{1}让addUser、editUser、deleteUser分别映射到add、edit、delete方法。但通配符要注意命名空间边界配宽了不该吞的请求也会被匹配进来排查起来更痛苦。5.3 中文乱码依旧检查这四处现象页面查出来全是问号或者提交到数据库变成乱码改了JSP的charset也没用。原因编码链路不止一处。JSP页面、Struts2编码常量、CharacterEncodingFilter、数据库连接串和表collation四层里任何一层断掉都会乱码。解决按顺序全部统一成UTF-8。JSP的pageEncoding、struts.i18n.encoding、CharacterEncodingFilter的encoding和forceEncoding、MySQL连接串里的useUnicode和characterEncoding、表字段collation一项一项过。附带提醒跳转链路里的中间JSP比如公共头尾、错误提示页如果漏了pageEncoding也会把中文链路截断。5.4 校验错误提示全是英文缺国际化资源现象输入非法字符时页面显示的是英文的Invalid field value而不是预期的中文。原因Struts2把内置校验器的默认文案放在它自己的国际化文件里要变成中文得提供自定义资源文件。解决在struts.xml里配置全局资源constant namestruts.custom.i18n.resources valueglobalMessages/然后建globalMessages_zh_CN.properties放格式化错误、类型转换错误的文案例如error.required该字段必填 error.range长度不合法XML校验的message里直接引用key就行。虽然现在的SSM项目很少再用这套资源机制但理解它之后接手老项目时能少走弯路。5.5 S2-029远程代码执行漏洞老框架为什么总被盯上现象项目部署到公网后安全扫描报出apache struts2远程代码执行漏洞编号S2-029。原因这是Struts2 2.0.0到2.3.20之间版本异常处理机制的缺陷攻击者可以在特定条件下构造恶意请求让服务器执行任意Java代码。官方在2.3.20版本里修复。解决把struts2-core升级到2.3.20或更高版本同时同步升级xwork-core再关闭动态方法调用做好应用的出站流量监控。这套组合动作执行完能挡掉一大部分常见攻击面。这里有个反直觉的结论要说明白不是版本越新越安全很多老课设项目用的2.3.16.3正好落在漏洞区间里。接手这种项目第一件事就是做版本体检不要相信原作者说的“我本地能跑”这种话。6. 从能用到好用登录拦截、全局异常与下一步整合项目跑通之后我会补三个加固动作。第一个是登录状态检查。现在很多Action直接裸奔访客输URL就能进后台页面。自定义一个拦截器把它挂到拦截器栈里public class LoginInterceptor extends AbstractInterceptor { Override public String intercept(ActionInvocation invocation) throws Exception { Object user ActionContext.getContext().getSession().get(loginUser); if (user null) { return login; } return invocation.invoke(); } }登录成功后把用户对象放进sessionkey用loginUser拦截器统一检查而不是每个Action里重复写判断。第二个是全局异常处理global-results result nameerror/error.jsp/result /global-results global-exception-mappings exception-mapping exceptionjava.lang.Exception resulterror/ /global-exception-mappingsSQL异常不该白屏更不该把异常栈直接甩给用户统一跳到友好提示页是基本体面。第三个是下一步方向如果还继续往深走就学Struts2加Spring整合把Action交给Spring容器管理持久层换成MyBatis这条链路学完你对Java Web的请求生命周期、拦截器思想和分层架构的体感会完全不一样。我后来接手任何Struts2老项目第一件事永远是做版本体检、关动态方法调用、检查拦截器栈从那以后再也没有在登录页面上翻过车。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →