首页/新闻资讯/正文详情

Struts2登录注册源码拆解:请求链路、鉴权与二次开发避坑

发布时间:2026/11/3 19:15:46 来源:云帆数科 栏目:资讯中心
Struts2登录注册源码拆解:请求链路、鉴权与二次开发避坑
简介这份 Struts2 登陆注册及用户信息管理系统源码包面向正在学习 Java Web 与 MVC 分层开发的学生和初级开发者可用于课程设计、毕业设计参考或框架入门练习。包内共 74 个文件包含 Java 源码、编译后的 class、struts.xml 等 XML 配置、JSP 视图、GIF/JPG 界面素材、SQL Server 的 mdf/ldf 数据文件以及运行所需 jar 依赖压缩包约 4.27MB基本覆盖从控制层、视图层到数据库落地的完整链路。已有 845 人学习说明其作为小型实战案例具有一定参考价值。读者可结合 Action 与 Result 的跳转关系、拦截器配置、登录校验和注册表单验证等实现理解 Struts2 的请求处理流程也能通过用户信息修改、注销及数据库表结构梳理会话跟踪与用户授权的基本做法。案例还附带图解说明与基础页面资源便于快速定位各模块职责适合按需拆解学习、课程演示或二次改造。1. 从一份 Struts2 登陆注册源码说起它到底能跑出什么把一份 Struts2 登陆注册的源码包解压出来很多人的第一反应是「这玩意儿现在还能跑吗」。能跑而且它把 Java Web 里最典型的几条链路——表单提交、Action 收参、Session 鉴权、用户信息的增删改查——全压缩进了一个不到二十个类的工程里没有注解、没有自动装配、没有 starter配置就写在 XML 里请求走向一眼能看到底。这套东西适合两类人。一类是要交课程设计或者做毕设需要一套能注册、能登录、能管用户的完整骨架改改页面就能用另一类是天天写 Spring Boot想回头看看 MVC 框架在没有注解的年代是怎么把一次请求从web.xml串到 Action 再串到 JSP 的。下面按「先跑通、再拆链路、最后改功能」的顺序把这份 Struts2 登陆注册系统拆开讲。2. 工程骨架与请求链路web.xml、struts.xml 到 Action 的完整走向拿到源码先别急着改代码先看清楚一个 HTTP 请求在工程里走了哪几步。Struts2 的项目结构非常固定认清楚这三层分工后面改任何功能都不会乱。2.1 目录结构哪一层在干什么典型的工程长这样不同源码包名字会略有出入但层次基本一致src/ com/demo/action/UserAction.java # 收参数、调 Service、决定跳哪个页面 com/demo/service/UserService.java # 业务规则重名判断、密码加盐、事务边界 com/demo/dao/UserDao.java # 只写 SQL返回实体 com/demo/dao/DBHelper.java # 取连接、关资源 com/demo/model/User.java # 实体字段和表一一对应 com/demo/interceptor/LoginInterceptor.java com/demo/util/MD5Utils.java WebRoot/ WEB-INF/web.xml # 过滤器入口 WEB-INF/jsp/user_list.jsp # 用户列表页放在 WEB-INF 下防直接访问 WEB-INF/jsp/user_edit.jsp login.jsp register.jsp struts.xml # action 映射 lib/ # struts2-core、xwork、ognl、freemarker、commons-*、mysql-connector判断一份 Struts2 源码写得规不规范看两个地方就够了Action 里有没有出现Connection或者 SQL 字符串DAO 里有没有出现HttpServletRequest。两个都没有说明分层是干净的二次开发时改起来不会牵连一片两个都有那就得先做一轮搬运否则你加一个字段要改五个文件。实体类User是贯穿全程的东西字段大致是id / username / password / salt / realName / email / status / createTime。注意salt这个字段很多课设版源码是没有的密码直接存明文或者裸 MD5后面第 3 章会讲为什么一定要留它。2.2 web.xml请求从哪个过滤器进来Struts2 不是 Servlet它靠一个过滤器把所有请求接住再分发给对应的 Actionweb-app version3.0 xmlnshttp://java.sun.com/xml/ns/javaee filter filter-namestruts2/filter-name !-- 老版本是 org.apache.struts2.dispatcher.FilterDispatcher -- filter-classorg.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter/filter-class /filter filter-mapping filter-namestruts2/filter-name url-pattern/*/url-pattern /filter-mapping welcome-file-list welcome-filelogin.jsp/welcome-file /welcome-file-list /web-appurl-pattern写成/*表示所有请求都过一遍 Struts2 的调度器包括.jsp、.css、.js。这里有个隐性代价静态资源也会进过滤器链虽然 Struts2 会放行但多一层判断。如果你发现项目里图片加载慢或者 404先看这段配置和后面的struts.action.extension常量。filter-class是版本差异最大的地方。早期版本是FilterDispatcher后来换成StrutsPrepareAndExecuteFilter。源码包里如果写着前者而你换成了新版 jar启动直接抛ClassNotFoundException。别硬记打开lib目录看 jar 的名字再去里面找对应的类这是最省事的确认方式。提示部署时如果访问任何地址都跳到登录页先确认url-pattern不是写成了/user/*这种窄范围再确认欢迎页login.jsp真实存在。2.3 struts.xmlpackage、namespace 和 action真正的路由表在struts.xml里。这个文件决定「哪个 URL 打到哪个类的哪个方法、返回什么视图」是整个工程最该先读的文件struts !-- 全局编码中文参数乱码九成是这里没配 -- constant namestruts.i18n.encoding valueUTF-8/ !-- 开发期打开改 struts.xml 不用重启上线必须关 -- constant namestruts.devMode valuetrue/ constant namestruts.action.extension valueaction,,/ !-- 公开区域登录、注册不需要鉴权 -- package namepub namespace/ extendsstruts-default action namelogin classcom.demo.action.UserAction methodlogin result namesuccess typeredirect/user/list.action/result result nameinput/login.jsp/result /action action nameregister classcom.demo.action.UserAction methodregister result namesuccess typeredirect/login.jsp/result result nameinput/register.jsp/result /action /package !-- 受保护区域所有用户管理功能统一挂鉴权拦截器 -- package namesecure namespace/user extendsstruts-default interceptors interceptor nameloginCheck classcom.demo.interceptor.LoginInterceptor/ interceptor-stack nameauthStack interceptor-ref namedefaultStack/ interceptor-ref nameloginCheck/ /interceptor-stack /interceptors global-results result namelogin typeredirect/login.jsp/result /global-results action namelist classcom.demo.action.UserAction methodlist interceptor-ref nameauthStack/ result namesuccess/WEB-INF/jsp/user_list.jsp/result /action action nameedit classcom.demo.action.UserAction methodedit interceptor-ref nameauthStack/ result namesuccess/WEB-INF/jsp/user_edit.jsp/result /action /package /struts几个参数值得单独说。namespace/user意味着这个包下的 action 完整访问路径是/user/list.actionmethodlist表示调用UserAction的list()方法一个类承载多个功能靠的就是它。typeredirect和默认的dispatcher区别很大前者让浏览器重新发一次请求地址栏会变刷新不会重复提交表单后者是服务器内部转发地址栏不变但用户在登录成功后按 F5 会再登一次。登录成功后跳列表页我一般强制用redirect。global-results里的login是留给拦截器用的——拦截器发现没登录返回字符串loginStruts2 就会到这里找对应的结果。这个字符串必须和拦截器return的值一字不差大小写都算。2.4 本地跑起来建库、改连接、发第一个请求跑通这个项目需要三样东西JDK、一个 Servlet 容器Tomcat 一类、MySQL。步骤不复杂但顺序别乱。先建表字段名要和User实体对齐CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL UNIQUE, password CHAR(32) NOT NULL, -- 存 MD5 结果定长 32 salt CHAR(8) NOT NULL, -- 每个用户独立盐值 real_name VARCHAR(32), email VARCHAR(64), status TINYINT DEFAULT 1, -- 1 正常0 禁用 create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;然后把DBHelper里的四要素改成你自己的public class DBHelper { private static final String URL jdbc:mysql://127.0.0.1:3306/demo_user?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai; private static final String USER root; private static final String PWD 你的密码; public static Connection getConnection() throws SQLException { try { Class.forName(com.mysql.cj.jdbc.Driver); // 老驱动是 com.mysql.jdbc.Driver } catch (ClassNotFoundException e) { throw new RuntimeException(驱动没放进 lib 目录, e); } return DriverManager.getConnection(URL, USER, PWD); } }serverTimezone这个参数不加MySQL 8 的驱动会直接抛时区异常驱动类名写错则是经典的No suitable driver。这两条几乎每个第一次跑这个项目的人都会撞上一次。把工程部署到容器访问http://localhost:8080/项目名/register.action能出注册页就说明过滤器、struts.xml、JSP 三段都通了。如果出来的是 404先看struts.action.extension有没有配空串这样不带后缀也能访问再看访问路径前缀有没有漏掉项目名。3. 登录注册核心链路Action 收参、拦截器鉴权与 OGNL 值栈骨架跑通之后最值得逐行读的是 Action。它承担了收参数、做校验、调业务、写 Session、决定返回值五件事是 Struts2 里信息密度最高的类。3.1 登录 Action收参、比对、写 Sessionpublic class UserAction extends ActionSupport implements ModelDrivenUser, SessionAware { private User model new User(); // ModelDriven 的根对象表单直接往里灌 private MapString, Object session; // SessionAware 由框架注入 private UserService userService new UserService(); private ListUser userList; private int pageNo 1; private int pageSize 10; private int total; Override public User getModel() { return model; } Override public void setSession(MapString, Object s) { this.session s; } public String login() { User db userService.findByUsername(model.getUsername()); if (db null) { addActionError(用户名或密码错误); // 不提示「用户不存在」避免被枚举账号 return INPUT; } String encoded MD5Utils.md5(model.getPassword() db.getSalt()); if (!encoded.equals(db.getPassword())) { addActionError(用户名或密码错误); return INPUT; } if (db.getStatus() ! null db.getStatus() 0) { addActionError(该账号已被禁用); return INPUT; } db.setPassword(null); // Session 里不放密码和盐 session.put(LOGIN_USER, db); return SUCCESS; } }ModelDrivenUser是关键。表单里写input nameusernameStruts2 通过 OGNL 直接调用getModel().setUsername(...)所以 Action 里不需要写一堆getParam。返回值只有三种走向SUCCESS走 struts.xml 里的 success 结果INPUT回到表单页并带上错误信息ERROR留给系统异常。addActionError存的是全局错误页面用s:actionerror/取addFieldError(username, ...)存的是字段级错误配合s:fielderror fieldNameusername/显示在输入框旁边两者别混用。密码这块必须提一句。这份源码如果直接存明文你至少在二次开发前把它改成md5(密码 salt)。盐值在注册时用UUID.randomUUID().toString().substring(0, 8)生成并存库登录时取出来拼上去再算。裸 MD5 现在查彩虹表几秒钟就能反查加了盐之后攻击成本才上来。3.2 注册 Action重名检测要放在事务里注册的逻辑比登录多一步先判断用户名在不在再插入。这两步之间有并发窗口两个请求同时判断「不存在」然后都插入数据库唯一索引会拦下一个但错误会以异常形式抛到页面上体验很差public String register() { User exist userService.findByUsername(model.getUsername()); if (exist ! null) { addFieldError(username, 该用户名已被占用); return INPUT; } String salt UUID.randomUUID().toString().replace(-, ).substring(0, 8); model.setSalt(salt); model.setPassword(MD5Utils.md5(model.getPassword() salt)); model.setStatus(1); try { userService.save(model); // 内部捕获唯一索引冲突转成业务异常 return SUCCESS; } catch (DuplicateKeyException e) { addFieldError(username, 该用户名已被占用); // 兜住并发窗口 return INPUT; } }save里把SQLIntegrityConstraintViolationException包成自定义的DuplicateKeyException往外抛Action 接住转成字段错误。这套「先查一次、再兜一次」的写法看着冗余但在真实并发下能省掉大量脏数据排查时间血泪经验。3.3 登录拦截器把「没登录就踢走」做成可复用组件Struts2 的拦截器是整个框架最有价值的部分。用户管理那一堆 action 每个都要判断 Session写一遍就够了public class LoginInterceptor extends AbstractInterceptor { private static final SetString WHITE_LIST new HashSet(Arrays.asList(login, register, logout)); Override public String intercept(ActionInvocation invocation) throws Exception { String method invocation.getProxy().getMethod(); // 当前调用的方法名 if (WHITE_LIST.contains(method)) { return invocation.invoke(); // 白名单直接放行 } MapString, Object session invocation.getInvocationContext().getSession(); if (session.get(LOGIN_USER) null) { return login; // 匹配 struts.xml 里 global-results 的 login } return invocation.invoke(); // 放行继续走后面的拦截器和 Action } }invocation.invoke()是放行的意思它把控制权交给拦截器链里的下一个节点一路走到 ActionAction 执行完再一层层往回返。忘了写这一句你会得到一个永远白屏、日志里什么都没有的页面——这是 Struts2 拦截器最经典的翻车方式。invocation.getProxy().getMethod()拿的是 struts.xml 里methodxxx的配置值不是 URL。想按 URL 判断就得用ServletActionContext.getRequest().getServletPath()。白名单用方法名判断更稳因为 URL 可以通过通配符映射绕过去。拦截器写完必须在struts.xml里用interceptor-ref显式挂上而且挂上去的 stack 里一定要包含defaultStack。只写自己的拦截器参数自动装配、类型转换、校验、文件上传全都会失效表现是 Action 里model的所有字段都是 null。3.4 OGNL 与值栈JSP 上那几个 s: 标签到底取到了什么页面部分是这个项目里最容易被跳过、也最容易出玄学 bug 的地方。JSP 里s:property value.../取值走的是 OGNL 表达式加值栈ValueStack不是 request 属性%-- 取 Action 的普通属性直接写属性名 --% s:property valuetotal/ %-- 取 Session必须带 # 前缀 --% 欢迎你s:property value#session.LOGIN_USER.realName/ %-- 遍历集合status 是循环状态变量可取 index、odd、first --% s:iterator valueuserList varu statusst tr class${st.odd ? odd : } tds:property value#u.id//td tds:property value#u.username//td /tr /s:iterator规则很简单值栈顶部是当前 Action属性能直接点到#session、#request、#application、#parameters是命名的上下文必须带#s:iterator里用var指定变量名后也要带#。不带#又恰好 Action 里有个同名属性取到的就不是你以为的对象页面数据看着「串行」了八成是这里的问题。4. 用户信息管理模块分页查询、表单校验与 DAO 层的落地写法登录之后能看到的那一屏用户列表才是这个项目里代码量最大的部分。它涉及分页、条件查询、单条编辑、删除确认每一块都有 Struts2 特有的写法。4.1 列表与分页只查一页总数单独查分页绝不要用SELECT *全部捞出来再在 Java 里切。正确姿势是 SQL 带LIMIT同时用一条COUNT拿总数public ListUser findByPage(int pageNo, int pageSize, String keyword) { ListUser list new ArrayList(); StringBuilder sql new StringBuilder( SELECT id, username, real_name, email, status, create_time FROM t_user WHERE 11 ); boolean hasKw keyword ! null !keyword.trim().isEmpty(); if (hasKw) { sql.append(AND (username LIKE ? OR real_name LIKE ?) ); } sql.append(ORDER BY id DESC LIMIT ?, ?); try (Connection conn DBHelper.getConnection(); PreparedStatement ps conn.prepareStatement(sql.toString())) { int idx 1; if (hasKw) { ps.setString(idx, % keyword.trim() %); ps.setString(idx, % keyword.trim() %); } ps.setInt(idx, (pageNo - 1) * pageSize); // offset ps.setInt(idx, pageSize); // 每页条数 try (ResultSet rs ps.executeQuery()) { while (rs.next()) { list.add(mapRow(rs)); } } } catch (SQLException e) { throw new RuntimeException(分页查询失败, e); } return list; }WHERE 11是个旧习惯好处是后面拼AND不用判断「是不是第一个条件」坏处是没有任何索引影响纯写法问题。offset (pageNo - 1) * pageSize这个减法算错一位第一页就会漏掉前十条属于改分页时最容易手滑的地方。Action 侧的list()负责把pageNo / pageSize / total / userList四个属性塞好页面用s:iterator渲染表格底部分页条需要算总页数public String list() { if (pageNo 1) { pageNo 1; } total userService.count(model.getUsername()); // 复用关键字 int maxPage (total pageSize - 1) / pageSize; // 上取整 if (maxPage 0 pageNo maxPage) { pageNo maxPage; } userList userService.findByPage(pageNo, pageSize, model.getUsername()); return SUCCESS; }(total pageSize - 1) / pageSize是整数上取整的标准写法。漏掉这个-1总数正好整除时会多出一个空页页码越界不修正用户删掉最后一条再刷新就会看到空白列表还找不到原因。4.2 表单校验validate 方法与校验 XML 怎么选Struts2 有两套校验Java 里重写validate()派生方法或者写UserAction-register-validation.xml。前者直观好调试后者适合字段多、规则经常变的场景。这个项目推荐方法式// 方法级校验验证拦截器在执行 register() 前会先调 validateRegister() public void validateRegister() { String u model.getUsername(); if (u null || u.trim().length() 4 || u.trim().length() 20) { addFieldError(username, 用户名长度需在 4 到 20 个字符之间); } if (model.getPassword() null || model.getPassword().length() 6) { addFieldError(password, 密码至少 6 位); } if (model.getEmail() ! null !model.getEmail().isEmpty() !model.getEmail().matches(^[\\w.-][\\w-]\\.[\\w.]$)) { addFieldError(email, 邮箱格式不正确); } }validateXxx()只对methodxxx生效所以登录、注册、编辑可以各写各的互不干扰。校验失败时框架自动返回INPUT不需要你在方法里手动return。有一点要记住只要某个方法加了校验list()这种纯查询方法千万别写validateList()否则列表页会因为一个无关字段报错而打不开。编辑用户时通常不希望强制改密码那就把密码校验拆到单独的方法里或者在validateEdit()里判断password是否为空串再决定要不要校验。字符串空判断一定要用isEmpty()而不是 null表单提交的空输入框传上来是不是 null这个差别坑过太多人。4.3 编辑与删除ModelDriven 的隐藏参数陷阱编辑页有个必须处理的问题表单里只有id / realName / email / status没有username和password。而ModelDriven会把表单里存在的字段灌进 model不存在的字段保持默认值 null。如果你直接拿这个 model 去 update会把用户名和密码覆盖成 nullpublic String edit() { User db userService.findById(model.getId()); if (db null) { addActionError(用户不存在或已被删除); return login; } // 只允许后台修改的字段从表单取值其余从库里回填 db.setRealName(model.getRealName()); db.setEmail(model.getEmail()); db.setStatus(model.getStatus()); userService.update(db); return SUCCESS; }先查后改把「不允许表单改的字段」从数据库记录里带过来这是所有信息管理系统都通用的写法。删除同理一定按主键删并且做软删除把status置 0比物理删除更稳妥——用户管理里的数据往往有关联真删了想恢复只能翻数据库备份世上没有后悔药。批量删除的时候还能碰到一个经典坑input typecheckbox nameids value1提交上来是字符串数组Action 里得声明private Integer[] ids;并且提供 setter。类型写错成int[]一个都不勾选时框架会尝试把空字符串转成 int直接抛类型转换异常。4.4 Service 与事务手写 JDBC 时边界在哪Struts2 本身不管事务这份源码里事务边界通常靠UserService手动控制public void update(User user) { Connection conn null; try { conn DBHelper.getConnection(); conn.setAutoCommit(false); // 开启事务 userDao.update(conn, user); userDao.updateLog(conn, user.getId(), UPDATE); // 操作日志两条必须同成同败 conn.commit(); } catch (Exception e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ignore) {} } throw new RuntimeException(更新用户失败, e); } finally { if (conn ! null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException ignore) {} } } }要点就三个setAutoCommit(false)一定要在业务操作之前DAO 方法要接收外部传入的Connection不能自己另开一个否则事务根本不生效finally里恢复自动提交并关闭连接容器有连接池的情况下漏关几次就把池子耗干了。这是不使用 Spring 时的标准手工事务写法改功能时照着这个骨架扩展就行。5. 避坑与排查Struts2 老项目最容易翻车的六个地方这类项目的报错信息往往指向性很差页面一片空白、日志一行不打印全靠经验定位。下面几条是我实际踩过、并且每次带人跑这个项目都会再遇到一遍的问题。5.1 启动与配置阶段的坑现象项目启动报ClassNotFoundException: org.apache.struts2.dispatcher.FilterDispatcher。原因web.xml里配的是老版本的过滤器类名而lib里放的是新版 jar类路径变了。解决打开 jar 看dispatcher包下的实际类名新版统一改成org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter或者反过来把 jar 换成与配置匹配的版本。两头对齐就行别只改一边。现象改完struts.xml必须重启才生效开发时来回重启几十次。原因struts.devMode没开或者开了但容器缓存了配置。解决加constant namestruts.devMode valuetrue/同时确认struts.configuration.xml.reload为 true。上线前务必改回 falsedevMode 会把完整堆栈和配置信息直接输出到浏览器页面上属于典型的信息泄漏。现象提交的表单参数全是nullAction 里model.getUsername()拿不到值。原因自定义拦截器栈里只写了interceptor-ref nameloginCheck/把defaultStack顶掉了参数装配拦截器没执行。解决自定义栈里必须先interceptor-ref namedefaultStack/再挂自己的拦截器。顺序也很关键鉴权拦截器要放在参数装配之后否则白名单判断拿不到方法名之外的东西。5.2 数据与页面阶段的坑现象中文用户名存进数据库变成???或者页面上显示成乱码方块。原因三段编码不一致——JSP 页面pageEncoding、struts.i18n.encoding常量、JDBC URL 的characterEncoding任何一段是 ISO-8859-1 都会乱。解决三处统一成 UTF-8数据库表和字段用utf8mb4。如果只在读旧数据时乱说明是「存的时候就已经错了」改配置救不回来得重新写入一遍。现象登录成功后跳转正常但按 F5 刷新会弹出「是否重新提交表单」确认后又登一次。原因登录 action 的 result 用了默认的dispatcher转发浏览器地址栏还停在login.action。解决成功结果改成result namesuccess typeredirect/user/list.action/result。跳转类的结果一律用 redirect只有需要带请求域数据回显表单时才用 dispatcher。现象用户列表页数据「串行」第一行显示的是第二行的邮箱。原因s:iterator里用了var但取值时没加#OGNL 从值栈顶层找同名属性找到了 Action 里的model于是每一行渲染的都是同一个对象。解决统一加#例如s:property value#u.email/。检查方式很粗暴把var随便改个不存在的名字如果页面还能出数据那必然是没加#。6. 二次开发给它加上角色权限与操作日志这份源码跑通之后最大的短板是所有登录用户权限完全一样——只要能进/user/list.action就能删任何人。要往「能真正用」的方向改第一步是加角色第二步是让鉴权拦截器认识角色。改法不复杂。库里加一列roleADMIN/USER实体加字段登录时一起塞进 Session。然后做一个 URL 到角色的映射表拦截器在放行前查表// LoginInterceptor 内部放行前的角色判断 private static final MapString, String ROLE_RULES new HashMap(); static { ROLE_RULES.put(delete, ADMIN); // 方法名 - 所需角色 ROLE_RULES.put(batchDelete, ADMIN); ROLE_RULES.put(list, USER); } String method invocation.getProxy().getMethod(); String need ROLE_RULES.get(method); if (need ! null) { User u (User) session.get(LOGIN_USER); if (!need.equals(u.getRole())) { return forbidden; // struts.xml 里配一个 403 页面 } } return invocation.invoke();关键点是别把角色硬编码在 JSP 里用s:if test#session.LOGIN_USER.role ADMIN控制按钮显隐。按钮藏起来只是视觉上的直接把 URL 敲进地址栏照样能删。真正的边界在拦截器页面上那点隐藏只是提升体验两件事不能互相替代。改造点改动文件验证方式加角色字段建表 SQL、User.java、注册/编辑 JSP新注册用户默认USER库里能查到方法级鉴权LoginInterceptor.java、struts.xml用普通账号访问delete.action应落到 403操作日志UserService事务内新增一条删一个用户后日志表有对应记录且与主表同时提交登录失败计数新增fail_count、lock_time字段连续错 5 次后账号锁定正确密码也进不去改完一定要回归一遍最朴素的路径注册 → 登录 → 改自己的邮箱 → 退出 → 再登录。这套流程能覆盖 80% 的配置错误。最后说个我自己的习惯。从那以后我每次接这类 Struts2 老项目第一件事都不是读代码而是把web.xml和struts.xml打印出来摊在桌上对着 URL 一条一条走一遍请求链路把「谁能访问、走到哪个方法、失败回哪」写成一张纸。这张纸没写清楚之前我一行 Java 都不改。希望帮到你。本文还有配套的精品资源点击获取

相关推荐

Jev不是模型而是TypeSafe AI协议层:API调用稳定性工程实践
Jev不是模型而是TypeSafe AI协议层:API调用稳定性工程实践

1. Jev 不是新模型,而是 TypeSafe AI 推出的开发者协议层:它解决的从来不是“谁更聪明”,而是“怎么不翻车”最近刷到“Jev爆火”“Jev模型官网”“Jev密钥申请”这类标题,我第一反应是——等等,这名字怎么听着像拼写错… · 2026/11/2 13:17:14

OCC/VLA标注技术:智驾数据标注的革新与实践
OCC/VLA标注技术:智驾数据标注的革新与实践

1. 项目概述:OCC/VLA标注技术如何重塑智驾数据标注在自动驾驶行业摸爬滚打这些年,我亲眼见证了数据标注从简单的2D框选进化到如今的多模态智能标注。最近业内热议的OCC(Occupancy Grid Mapping)和VLA(Vision-Language-… · 2026/11/2 13:17:14

AGI运动组织:机器人动作规划与执行的核心架构
AGI运动组织:机器人动作规划与执行的核心架构

1. 运动组织与AGI基础理论的关系解析在通用人工智能(AGI)研究领域,运动组织是一个常被忽视但至关重要的基础理论模块。这个看似简单的概念实际上构成了智能体与环境交互的底层架构。我在过去十年参与多个AGI项目时发现,许多团队在追求复杂认知能力时&… · 2026/11/2 13:17:14

企业业财系统对接实战:金蝶与百胜数据通道建设
企业业财系统对接实战:金蝶与百胜数据通道建设

1. 项目背景与核心价值最近刚完成一个企业数字化转型的实战项目,帮一家中型制造企业打通了金蝶云星空ERP与百胜E3OMS系统的数据通道。这个案例特别典型——财务部门用金蝶做账,供应链团队用百胜管订单,两边数据长期对不上,月底对账… · 2026/11/4 8:15:49

ESP32S3+LVGL驱动ST7789与xpt2046触摸屏实战
ESP32S3+LVGL驱动ST7789与xpt2046触摸屏实战

简介:面向嵌入式与物联网开发者的ESP32S3图形界面驱动示例,聚焦在IDF 5.2.2框架下将LVGL图形库与ST7789显示驱动、xpt2046电阻触摸控制器打通这一常见难点,适合已具备一定ESP-IDF基础、希望快速验证屏幕显示与触摸交互的中级开发者参考。资源… · 2026/11/4 8:15:49

SAP Fiori授权体系:PFCG、Catalog与SU24全解析
SAP Fiori授权体系:PFCG、Catalog与SU24全解析

1. 项目概述:Fiori授权体系全景透视 在SAP Fiori应用开发与运维过程中,授权配置堪称最令开发者头疼的"暗礁区"。我经历过多个Fiori项目上线后因权限问题导致用户无法访问的紧急救援,发现90%的问题都源于对PFCG、Catalog和SU24之间衔… · 2026/11/4 8:15:49

GPT-6 Sol与Claude Opus 5.5崛起,Codex额度焦虑如何被API按量计费化解
GPT-6 Sol与Claude Opus 5.5崛起,Codex额度焦虑如何被API按量计费化解

1. 没人喊“重置额度”了,Codex 社区这周到底发生了什么 1.1 Codex 的额度机制,为什么“周五重置”曾是固定节目 用过 Codex 的朋友应该都熟悉一个节奏:周一到周四,社区里讨论最多的不是“这个任务怎么拆”,而是“额度… · 2026/11/4 8:15:49

开发者如何打造高效技术博客:从架构到优化
开发者如何打造高效技术博客:从架构到优化

1. 技术博客的诞生:为什么每个开发者都需要一个自留地 五年前的一个深夜,我盯着屏幕上又一次报错的代码,突然意识到一个问题:过去半年里,我至少解决了三十个类似的bug,但每次遇到时依然要重新搜索解决方案。… · 2026/11/4 8:15:49

Dreamtonics发布Vocoflex:10秒歌声克隆真人音色
Dreamtonics发布Vocoflex:10秒歌声克隆真人音色

前阵子音乐软件圈里有个不大不小的消息:Dreamtonics 正式发布了 Vocoflex。这款产品值得单独拿出来聊,原因很简单——一家做 Synthesizer V 的公司,突然把目标从“合成歌手的声音”转向“克隆真人的声音”,而且整个学习过程只需要… · 2026/11/4 8:14:48

双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49

STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 6:59:29

DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 7:02:38

AI辅助技术栈评估:提升研发决策效率60%
AI辅助技术栈评估:提升研发决策效率60%

1. 项目背景与核心价值技术决策一直是研发团队面临的重大挑战。随着技术生态的快速演进,技术栈的选择、维护和迭代变得愈发复杂。我们团队在过去两年里,先后经历了三次重大技术栈迁移,每次都需要投入大量人力进行技术评估和风险分析。直到去年… · 2026/11/4 0:00:17

从生成Token到直接决策:Jev如何重塑AI Agent工程实践
从生成Token到直接决策:Jev如何重塑AI Agent工程实践

1. 从"生成文字"到"拍板决策":Jev 到底在解决什么问题大多数人第一次接触大模型,脑子里建立的模型是"输入问题、输出文字"。你问它今天天气怎么样,它给你一段描述;你让它写个周报,它给你… · 2026/11/4 0:00:17

32G统一内存跑大模型:内存账、TPS实测与端云分流
32G统一内存跑大模型:内存账、TPS实测与端云分流

想在一台 32G 统一内存的小主机上把大模型跑成"日常可用",这件事我前后折腾了小半年。标题里说的这台 Mac mini M6,是我们这次讨论的目标平台,具体到手规格以实物为准;但下面这套推算方法和实测思路,对任何统… · 2026/11/4 0:00:17

双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49

STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 6:59:29

DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 7:02:38

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码