简介:Struts2登录注册及用户信息管理系统是一份适合Java Web初学者学习与参考的完整项目源码,聚焦于MVC架构下用户认证、授权和信息管理模块的实现。这份资源包为RAR压缩格式,压缩包大小约4.27MB,共74个文件,包含13个Java源码文件、6个JSP页面、9个XML配置文件、6个依赖jar库以及SQL Server数据库文件等,并附有操作演示gif和设计图解文档。目前已有843人下载学习。项目围绕Struts2核心特性展开,涵盖Action映射与结果跳转、拦截器实现登录验证与权限检查、表单数据校验、全局异常处理等关键机制,同时演示了基于session的用户会话跟踪与权限控制,以及登录注册模块中用户信息的管理流程。通过阅读和运行这份资源,可以直观理解登录注册模块从请求分发、业务处理到数据库交互的完整流程,为后续独立开发更复杂的Web系统打下基础。
1. Struts2做的登陆注册及用户信息管理系统:2025年为什么还要翻这套老代码
如果你接到一个维护任务,要在一套跑着Tomcat的“Struts2做的登陆注册及用户信息管理系统”上改需求,第一反应多半不是重写。无论是校内课设、毕业设计还是早期企业内部后台,这类题目的存量极大,模块也惊人地一致:注册、登录、用户信息列表、编辑删除,顶多加一个分页。它的价值在于,登录鉴权是所有Web系统的地基,用户管理又是后台管理的缩影,而Struts2在这一层里恰好把表单数据处理、拦截器校验和页面跳转串成了一条完整链路。这篇文章的目标读者很明确:接手老项目的人、要复现一个可运行SSH项目的学生、以及面试时被问到“Struts2和Spring MVC到底差在哪”的从业者。我会按一次登录请求怎么流转、最小工程怎么搭、CRUD怎么做、坑在哪里这个顺序来讲。
2. 从一次登陆请求看Struts2的任务链:值栈、OGNL和拦截器,为什么这套架构这么“绕”
2.1 Action是多实例的:这和Servlet的单例模型正好相反
刚到老代码里找登录逻辑时,大部分人的第一反应是去翻Servlet。但是Struts2里没有Servlet类,只有Action。它的核心设计是:每个请求进来,框架都会new一个Action实例,处理完就丢。这种多实例模型和Servlet的单例模型正好相反,也决定了你在Action里写成员变量的时候不用太担心并发串数据——每次请求都是新对象。
这一点在实际排错时非常重要。我见过有人用Spring管理Action时不小心把scope配置成了singleton,结果两个用户同时登录,A用户把用户名写进Action的user属性后,B用户也登录,A页面上显示的用户名变成了B的名字。查了很久才发现是Bean作用域配置错了。Struts2默认的Action实例是prototype,在struts-default.xml里定义好了,这个底线不能动。
表:Servlet与Struts2 Action的并发模型差异
| 对比项 | Servlet | Struts2 Action |
|---|---|---|
| 实例数量 | 每个Servlet单例 | 每个请求新建实例 |
| 成员变量 | 必须考虑线程安全 | 可安全持有请求级数据 |
| 生命周期 | 容器管理,常驻内存 | 请求结束即销毁 |
| 数据传递 | request / session | Action属性 + 值栈 + Session |
2.2 OGNL表达式:取值方便,攻击面也在这里
Struts2里取值用的不是EL,是OGNL。它比EL更强,体现在能直接调用Java方法、访问静态成员,甚至能操作值栈里的任意对象。这个“强”在正常业务里非常好用。比如登录失败后想在JSP页面上回显用户刚才输入的账号,一行就搞定:
<s:property value="#parameters.username" />这里#parameters.username就是OGNL表达式,意思是“从请求参数里取username”。类似的还有#session.user(取Session里的user对象)、#request.msg(取request属性)。值栈是Struts2最核心的数据容器,Action的属性、ModelDriven的模型对象、以及通过#符号引用的session和request,都会在渲染时被OGNL读取。
麻烦也出在这里。Struts2的很多标签属性在渲染时会“自动”对属性值做OGNL求值,典型的就是defaultValue、title这些配置项。如果你的代码把用户可控的URL参数直接拼进了这些标签属性,那用户输入的内容就会被当成OGNL表达式去执行。这种“便利”就是后面要讲的S2-029远程代码执行漏洞的根源之一。理解OGNL不是为了炫技,是为了排查漏洞时能看懂攻击者是怎么进来的。
2.3 一次登录请求的完整任务链
我们以一个最普通的登录表单为例。用户在JSP里填好用户名和密码点提交,这个请求从进入Tomcat到页面渲染,至少经历八个环节。看清楚这一条链,后续配置拦截器、定位乱码、排查绕过登录的问题都会快很多。
表:登录请求在Struts2中的流转环节
| 步骤 | 执行者 | 在登录场景里做了什么 |
|---|---|---|
| 1 | Tomcat Connector | 接收HTTP请求,交给web应用 |
| 2 | StrutsPrepareAndExecuteFilter | 创建ActionContext,初始化值栈 |
| 3 | Interceptor Stack | 默认拦截器组启动,比如servletConfig、params |
| 4 | params拦截器 | 把表单里的username/password填充进Action属性 |
| 5 | workflow拦截器 | 调用Action的validate()方法做基础校验 |
| 6 | Action.execute() | 调Service/DAO,比对用户名和密码 |
| 7 | Result | 根据返回值跳转或转发到success/login等视图 |
| 8 | JSP模板 + OGNL | 在页面上渲染登录结果或错误提示 |
这里最值得你记住的是第4步:params拦截器利用反射把请求参数名和Action属性名做匹配,自动完成赋值。你不用写request.getParameter("username")再手动setter,这正是Struts2当年比Struts1舒服的地方。但自动赋值也意味着,如果Action里有个和请求参数同名的敏感属性,外部就能传值进来修改它。后面讲安全的时候还会碰到这个坑。
2.4 为什么老项目普遍配Spring和Hibernate
“Struts2做的登录注册及用户信息管理系统”在市面上常见形态是SSH组合,而不是只有Struts2一个框架。原因很简单:Struts2只管控制层,如果数据访问全靠手写JDBC,事务管理和连接释放会让你痛不欲生。所以历史主流做法是Struts2负责请求分发,Spring管理Service层Bean和声明式事务,Hibernate负责ORM映射,把数据库表变成User对象。
这个组合在你的工程里表现为典型的三层目录:action包放控制类,service包写业务逻辑,dao包操作数据库,entity包放Hibernate映射实体。理解它不是为了维护方便,而是因为当你看到一个“注册成功但数据没写进MySQL”的问题时,要能判断是Action没调到Service,还是Spring事务没提交,又或者是Hibernate映射字段写错。排查的方向完全不一样。
3. 用Maven把SSH版登录注册跑起来:pom.xml、struts.xml和UserAction的最小实现
3.1 创建Maven war工程并锁定依赖
老项目里常见的是直接把jar包拷进WEB-INF/lib,十个项目里至少有八个在加载时因为jar版本冲突报ClassNotFoundException。现在重新搭,建议直接用Maven,至少能把依赖树理清楚。下面是一份最小可跑的pom依赖片段,只保留Struts2、Spring、MySQL驱动和JSTL这几个核心:
<dependency> <groupId>org.apache.struts</groupId> <artifactId>struts2-core</artifactId> <version>2.3.34</version> </dependency> <dependency> <groupId>org.apache.struts</groupId> <artifactId>struts2-spring-plugin</artifactId> <version>2.3.34</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>4.3.30.RELEASE</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.49</version> </dependency>这里有个参数值得单独说明:struts2-spring-plugin是Struts2和Spring集成用的桥梁,它让Spring容器来创建Action实例,这样Action里就能直接@Autowired注入Service。如果你不加这个包,Action和Service之间就得自己new,事务和依赖注入都发挥不出来。版本选择上不要无脑升到最新,先确认你本机JDK和Tomcat的版本,JDK 1.8搭配Struts 2.3.x是很稳的组合;直接上2.5.x会带来一些默认参数变化,需要额外适配。
3.2 web.xml:挂载Struts2入口过滤器
Struts2的入口是一个过滤器,不是Servlet。老版本项目里常见的是FilterDispatcher,新代码建议使用StrutsPrepareAndExecuteFilter,后者把“准备Struts环境”和“执行请求处理”分得更清楚,升级路径也更平滑。web.xml里这样配置:
<filter> <filter-name>struts2</filter-name> <filter-class>org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter</filter-class> </filter> <filter-mapping> <filter-name>struts2</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>这里的url-pattern建议直接写/*,这样所有请求先进Struts2,由struts.xml决定哪些走Action、哪些直接放行静态资源。不要图省事写成/action/*,否则某些动态请求绕开过滤器后会出现参数无法绑定、拦截器不生效的怪问题。放行静态资源不是靠修改这个pattern,而是靠Struts2的struts.action.excludePattern配置,这个后面会提到。
3.3 struts.xml核心配置:三个必须调的参数
struts.xml是整个框架的配置文件,放在src/main/resources根目录下。下面这份配置对应登录和注册两个Action,并加了一个简单的登录检查拦截器:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE struts PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 2.3//EN" "http://struts.apache.org/dtds/struts-2.3.dtd"> <struts> <constant name="struts.devMode" value="false" /> <constant name="struts.i18n.encoding" value="UTF-8" /> <constant name="struts.enable.DynamicMethodInvocation" value="false" /> <package name="user" namespace="/" extends="struts-default"> <interceptors> <interceptor name="loginCheck" class="com.demo.interceptor.LoginInterceptor" /> <interceptor-stack name="loginStack"> <interceptor-ref name="defaultStack" /> <interceptor-ref name="loginCheck" /> </interceptor-stack> </interceptors> <action name="login" class="com.demo.action.UserAction" method="login"> <result name="success">/index.jsp</result> <result name="input">/login.jsp</result> </action> <action name="register" class="com.demo.action.UserAction" method="register"> <result name="success">/login.jsp</result> <result name="input">/register.jsp</result> </action> </package> </struts>三个constant对应三个极易踩坑的参数。
struts.devMode在开发时设成true会输出更详细的调试信息,但生产环境一定改回false,否则错误堆栈会直接泄露给访问者,而且框架每次请求都会额外检查配置文件变化,性能明显下降。
struts.i18n.encoding直接决定请求参数的读取编码,这里写成UTF-8后,Tomcat收到的中文参数才能正确放进Action属性里。你后面遇到登录用户名中文乱码,第一件事就是检查这个值是不是被删掉了。
struts.enable.DynamicMethodInvocation是老系统里最容易出安全问题的开关。DMI开启后,URL可以写成user!delete.action直接调用Action里任意方法。这种写法虽然方便,但如果拦截器配置不严,等于把Action的每个方法都暴露给了外部。建议一律设为false,操作和页面跳转都通过独立的action节点配置。
拦截器栈这里只做了最小示例:defaultStack是Struts2框架自带的必选拦截器组,里面包含了params、workflow等核心拦截器;loginCheck是我们自己写的登录检查,放后面执行。注意这个栈名称会被后面所有需要登录的action引用。
3.4 UserAction与DAO:把表单数据写进MySQL
下面是一个最小可运行的UserAction,包含login和register两个方法。为缩短篇幅,Service层直接用内部调用代替,但保留DAO查询逻辑的写法:
public class UserAction extends ActionSupport { private String username; private String password; private UserService userService = new UserService(); public String login() { User user = userService.findByUsernameAndPassword(username, password); if (user == null) { addFieldError("username", "用户名或密码错误"); return INPUT; } ActionContext.getContext().getSession().put("user", user); return SUCCESS; } public String register() { User user = new User(); user.setUsername(username); user.setPassword(password); userService.save(user); return SUCCESS; } // getter/setter 省略 }这个类的逻辑很直白:login成功就把user对象放进Session,返回SUCCESS,页面跳转到index.jsp;失败就往字段错误里塞一条消息,返回INPUT,框架会重新展示登录页并回显错误。
ActionContext.getContext().getSession()拿到的是Map结构的Session,这里存进去的user对象,在JSP里可以用#session.user取出来。登录检查拦截器也是靠判断这个值是否为空来决定放不放行。
UserService里对应的DAO方法,最常见做法是这样:
public User findByUsernameAndPassword(String username, String password) { String sql = "select * from t_user where username=? and password=?"; return jdbcTemplate.queryForObject(sql, new UserRowMapper(), username, password); }注意这里的SQL用的是占位符传参,不要用字符串拼接。用户名字段一旦被拼进SQL里,就可能变成SQL注入的入口。这一条在用户信息管理系统里尤其重要,因为注册和登录是唯一不设防的对外入口。
3.5 启动Tomcat后用curl把登录流程走通
工程编译打包好后,把war包扔进Tomcat的webapps目录启动,然后用curl模拟一次表单提交,确认链路是通的:
curl -d "username=admin&password=123456" -L http://localhost:8080/ssh/login.action加-d参数表示发送POST表单格式数据,-L跟随重定向。如果配置正确,这个命令会返回登录成功后的页面内容。如果返回的是登录页,说明校验没过,你可以在本机浏览器里打开Network面板看看到底返回了哪个视图。
要验证拦截器是否生效,可以不带任何参数直接请求一个需要登录的action:
curl -i http://localhost:8080/ssh/user/list.action-i用来输出响应头。如果登录检查拦截器工作正常,你会看到302跳转到登录页。这一步是整个系统能否上线的基础验证,不要省略。
4. 用户信息管理系统的CRUD落地:模型驱动、validate校验和分页的三个默认参数
4.1 用ModelDriven接口收编注册表单
登录只需要username和password两个字段,Action里手写两个属性还行。用户信息管理系统里的编辑表单一般会有用户名、邮箱、手机号、角色、状态等七八个字段,如果每个字段都在Action里写一遍getter/setter,代码会非常臃肿。常见做法是让Action实现ModelDriven接口,把表单数据直接绑定到一个User对象上:
public class UserAction extends ActionSupport implements ModelDriven<User> { private User user = new User(); @Override public User getModel() { return user; } public String save() { userService.save(user); return SUCCESS; } }ModelDriven的作用是把getModel()返回的对象压到值栈栈顶。这样JSP表单里的name="username"、name="email"会自动绑定到user对象的同名属性上,不再需要Action里逐个声明。使用ModelDriven后,页面回显也简单了,<s:textfield name="email" />会自动从值栈里的user对象取出当前值。
这种方式唯一的边界问题是:值栈里同时存在Action属性和Model属性时,OGNL会优先找栈顶对象。所以不要在Action里再声明一个叫username的属性,否则表单数据可能会绑定到那个属性上,而不是User对象里。
4.2 三种校验写法:validate()方法、XML校验和注解校验
Struts2的校验有三套写法:重写Action的validate()方法、写UserAction-validation.xml校验文件、或者用注解。老项目里见的最多的是第一种,因为简单直观。下面是一个注册时必填和格式校验的示例:
@Override public void validate() { if (username == null || username.trim().isEmpty()) { addFieldError("username", "用户名不能为空"); } if (password == null || password.length() < 6) { addFieldError("password", "密码至少6位"); } if (!Pattern.matches("^[a-zA-Z0-9_]+@[a-zA-Z0-9_.]+$", email)) { addFieldError("email", "邮箱格式不正确"); } }validate()方法会在execute()之前被workflow拦截器自动调用,只要addFieldError添加了错误,流程就会中断,返回INPUT结果,并不会执行Action的方法。
这里有一个新手必踩的坑:struts.xml里必须给Action配置<result name="input">指向对应的JSP页面。很多初学者只写了success的result,一提交就报错说找不到input视图。这个result严格来说不属于Struts2内部逻辑,但整个框架的校验流程依赖它才能闭环。
XML校验适合对表单字段多、规则经常变的场景。文件命名有讲究:放在Action同包下,叫UserAction-validation.xml,其中UserAction是类名,Struts2会自动加载它。XML优点是不用改Java代码就能调规则,缺点是写起来啰嗦,新手不推荐上手就用。
4.3 密码加密:别再让user表里的password字段是明文
很多课设项目里的用户表直接存明文密码,一抓一个准。做用户信息管理系统时,至少要做到加盐哈希。Spring自带了一个工具类,不需要额外引包:
String salt = UUID.randomUUID().toString().substring(0, 8); String encryptedPassword = DigestUtils.sha256Hex(password + salt); user.setSalt(salt); user.setPassword(encryptedPassword);这里用UUID截取8位作为盐值,密码加上盐后做SHA-256哈希。注册时把盐和哈希一起存进数据库,登录时先取盐,再拼上用户输入的密码重新计算哈希,比对两个哈希是否一致。
要注意数据库字段设计:旧表里的password字段长度可能只有32位,SHA-256算出来的哈希是64位十六进制字符串,存不下就报数据截断错误。改表时把password字段升级到VARCHAR(64),再加一个salt字段,长度设为16就够。
4.4 分页列表:Hibernate的setFirstResult/setMaxResults
用户信息管理系统最难看的代码通常集中在列表页。数据少时一次性全查出来没问题,但用户表数据一多,全量加载会把页面卡死。常见做法是用分页查询,Hibernate里的实现如下:
public PageBean<User> findUsers(int pageNow, int pageSize, String keyword) { Session session = sessionFactory.getCurrentSession(); Criteria criteria = session.createCriteria(User.class); if (keyword != null && !keyword.trim().isEmpty()) { criteria.add(Restrictions.like("username", "%" + keyword + "%")); } Long totalCount = (Long) session.createCriteria(User.class) .add(Restrictions.like("username", "%" + keyword + "%")) .setProjection(Projections.rowCount()) .uniqueResult(); criteria.setFirstResult((pageNow - 1) * pageSize); criteria.setMaxResults(pageSize); List<User> list = criteria.list(); return new PageBean<>(pageNow, pageSize, totalCount.intValue(), list); }三行核心代码:setProjection(Projections.rowCount())用来统计总记录数,setFirstResult指定从第几条开始取,setMaxResults限制每页条数。网上很多教程把分页写成limit pageNow,pageSize拼进SQL的写法,那是MyBatis的思路,在Hibernate里用Criteria或HQL更优雅,也避免方言差异。
分页还有一个容易忽略的参数:排序。列表查询默认顺序是数据库物理顺序,翻页时很容易出现数据忽前忽后。建议统一加上criteria.addOrder(Order.desc("id")),保证翻页稳定。这个参数虽然小,但上线后用户反馈“列表顺序乱跳”时,这一行代码就是解药。
4.5 动态方法调用(DMI)的默认参数和安全边界
老系统里经常能看到这种URL:user!delete.action?id=3。!后面的部分是方法名,这就是动态方法调用在起作用。它的机制是允许URL直接指定Action里要执行哪个方法,省去struts.xml里一个个配置action节点。
问题是这个便利很容易突破安全边界。假设你的UserAction里有个deleteAll()方法没配拦截器检查,攻击者只要猜到方法名,拼出user!deleteAll.action就能直接触发。Struts2默认是开启DMI的,而很多老项目根本意识不到这个参数的存在。
我的建议是:所有新写的系统,struts.enable.DynamicMethodInvocation一律设为false;已有的老项目如果要改,先把所有!形式的链接在代码里搜出来,替换成method="delete"这种固定配置,才能安全关闭。废话不多说,这一项应该是改造老系统时优先级最高的安全加固项。
5. Struts2避坑指南:从S2-029到中文乱码的五条实战踩坑
5.1 Apache Struts2远程代码执行漏洞(S2-029):OGNL钻进标签属性
现象:安全扫描报告里出现漏洞编号S2-029,提示系统存在Apache Struts2远程代码执行漏洞;检查日志时发现异常信息里有类似#_memberAccess、@java.lang.Runtime@getRuntime()这类的OGNL表达式痕迹,但系统表面还在正常服务,没有明显故障。
原因:Struts2的某些标签属性在渲染时会做强制的OGNL求值,比如defaultValue、title这些可配置项。如果用户可控的请求参数被拼进了标签属性,且中间没有做过滤,那么参数里的OGNL表达式就会被当作代码执行。S2-029正是抓住了这个机制,攻击者提交精心构造的URL就能在服务器上执行命令。
解决:先按官方安全公告给出的修复版本升级Struts2核心包,这是唯一的根治手段。升级后重点回归登录、注册和列表导出这几个核心功能,因为框架内部API变化可能导致原有拦截器或result类型报错。如果短期内无法升级,至少做到两点:struts.enable.DynamicMethodInvocation设为false,同时在代码里全局搜索JSP中的<s:property>、<s:textfield>等标签,确认没有把URL参数直接放进属性值里。
5.2 中文用户名注册成功后在列表页变成“???”
现象:注册页面输入中文昵称,提交后数据库里要么是一串问号,要么是å¼ ä¸‰这种乱码;更诡异的是有时候登录页回显正常,但列表页查询出来的中文全乱。
原因:编码链路四层里必然有一层断了。第一层是Struts2的struts.i18n.encoding没设成UTF-8,导致请求参数按ISO-8859-1解码;第二层是MySQL连接串没带characterEncoding=UTF-8;第三层是Tomcat连接器的URIEncoding没配置;第四层是数据库表的字符集不是utf8mb4。任何一层不一致,中文就会在流转过程中变形。
解决:把四层一次性统一。struts.xml里设<constant name="struts.i18n.encoding" value="UTF-8"/>;web.xml加Spring的CharacterEncodingFilter强制请求编码;JDBC URL追加useUnicode=true&characterEncoding=UTF-8;MySQL建表时用ENGINE=InnoDB DEFAULT CHARSET=utf8mb4。改完后重新启动,清空浏览器缓存再验证。最有效的排查办法是先在数据库客户端直接执行一条中文INSERT,如果客户端入库正常,那问题就出在Java应用侧,不用怀疑数据库。
5.3 修改个人信息后Session里还是旧头像旧邮箱
现象:用户在前台页面上改了邮箱和手机号,提示修改成功,但页面右上角的用户信息还是旧值。只有重新登录或者清Cookie后才显示新的邮箱。
原因:登录成功后,Action把user对象放进了Session:ActionContext.getContext().getSession().put("user", user)。更新操作只调用了Service更新数据库,却没有同步更新Session里的user对象。Session里存的是一个旧副本,页面取值自然还是老数据。
解决:更新成功后,重新查询一次用户信息,再放回Session:
User newUser = userService.findById(user.getId()); ActionContext.getContext().getSession().put("user", newUser);这里还要注意一个细节:很多编辑页的表单里有password字段,用户在网页上没填密码,提交时Action里的password就变成null,如果直接拿这个对象去调update,很可能把数据库里的密码覆盖成空值。常见做法是在Service层判断:密码为空则只更新其他字段,不为空才做加密更新。
5.4 不登录直接敲后台列表URL,数据被看光
现象:退出登录后,手动在地址栏输入/user/list.action,页面直接显示出用户列表,没有被重定向到登录页。
原因:登录检查拦截器没有覆盖到目标Action。常见两种情况:一是拦截器栈只配置在了某个单独的action节点上,其他action没引用这个栈,导致漏掉了;二是拦截器栈的顺序写反了,把defaultStack放在最后,导致前面拦截器返回后流程已经结束,检查逻辑根本没执行到。
解决:把登录检查写到package级别,让包内所有Action默认使用同一个拦截器栈。上面的struts.xml示例里,loginStack放在package的interceptors节点下,然后给每个需要保护的action加<interceptor-ref name="loginStack"/>。要注意的是login和register这两个Action要排除在外,方法是在拦截器里判断请求的action名,或者用struts-exclude-methods参数。核心原则是“默认拦截,显式放行”,而不是默认放行再逐个补拦截。
5.5 并发登录时Tomcat线程耗尽,连接池全部被占满
现象:用压测工具模拟200个用户同时登录,系统在几十秒后失去响应,后台日志大量出现Connection is not available和ThreadPoolExecutor rejected异常。
原因:两个瓶颈叠加了。一是Action虽然按请求建实例,但DAO层如果是手写JDBC且没释放连接,每个请求都把连接占住不放,连接池很容易被打满;二是Tomcat默认线程数不匹配连接池大小,当连接池等待时间过长时,HTTP线程也全部阻塞在等待数据库连接上,最终导致Tomcat无可用线程处理新请求。
解决:把事务边界交给Spring来管理,DAO里只做查询和写入,不手动open和close连接。连接池配置要去看两个数字:maxActive代表最大连接数,maxWait代表获取连接的等待毫秒数,建议maxWait设成5000而不是无限等待,这样连接耗尽时请求会快速失败,而不是整个Tomcat线程池瘫痪。排查时先看连接池日志,确认是连接没释放还是连接数太小,再决定是修代码还是调参数。
6. 上线前把这些验证和开关做掉:curl烟囱测试、JUnit回归和最后的devMode关停
6.1 用JUnit把Service层的校验逻辑钉住
改动老项目最怕的是改完登录,注册跟着崩。改造前先写一层JUnit把Service层钉住。比如校验规则和分页边界:
@Test public void testPasswordTooShort() { UserAction action = new UserAction(); action.setPassword("123"); action.validate(); assertTrue(action.getFieldErrors().containsKey("password")); }这种方式不需要启动Tomcat,直接new Action就能触发validate逻辑,跑起来特别快。把注册校验、密码加密、分页页码越界这几类容易回归的链路先覆盖掉,后面再改代码时心里才有底。
6.2 用Curl写一份烟囱测试清单
JUnit测的是代码逻辑,HTTP层面的链路还得用真实请求验证。写一份固定清单,每次上线前跑一遍:
# 未登录访问受保护页面,应返回302跳转 curl -i http://localhost:8080/ssh/user/list.action # 正常登录拿Cookie,再访问受保护页面,应返回200 curl -d "username=admin&password=123456" -c cookies.txt http://localhost:8080/ssh/login.action curl -b cookies.txt http://localhost:8080/ssh/user/list.action # 重复注册同名用户,应返回字段错误 curl -d "username=admin&password=123456&email=admin@test.com" http://localhost:8080/ssh/register.action三个请求覆盖了登录拦截、Session写入和字段校验三条主线。这个清单应该放在项目的src/test/resources里随代码走,别只留在个人终端上。每次改完安全配置后,这三条命令能帮你快速判断有没有把登录链路弄断。
6.3 最后关掉devMode和DMI,确认开关真生效
上线前检查struts.xml里的两个参数,这是Struts2系统保命的最后一关:
<constant name="struts.devMode" value="false" /> <constant name="struts.enable.DynamicMethodInvocation" value="false" />devMode最直观的副作用是系统异常时把堆栈详情直接显示在页面上,生产环境等于把代码内部结构暴露给所有访问者。DMI更是安全短板,老项目改造的第一优先级就是关这两个开关。验证方式很简单:故意访问一个不存在的action路径,如果返回给浏览器的是整页异常堆栈,说明devMode没真正关闭;检查后台配置文件是不是被其他地方覆盖了,一个应用里存在多份struts.xml时,后加载的会覆盖前面的同名参数。
我当年维护这种老系统时吃过不小的亏,上线前只改了业务代码,没检查配置项,结果第二天安全扫描就报了漏洞。后来养成的习惯是:改完代码必须过一遍配置文件里的开关状态,顺手把这条清单固化进发布流程。希望帮到你。
本文还有配套的精品资源,点击获取