简介:基于SpringBoot+Layui+Shiro搭建的登录注册模板,面向需要快速实现用户认证功能的Java开发者。该项目以邮箱账号登录、邮箱验证码注册为核心流程,引入Shiro作为安全框架完成密码加密与访问控制,适合作为SpringBoot入门学习者理解安全认证整合的参考范例。压缩包共175个文件,体积约1.35MB,主要包含28个Java后端源码、33个JavaScript脚本、10个CSS样式及7个HTML页面,同时提供YML配置、字体图标、图片素材和说明文档,目录覆盖前端Layui页面、后端Controller/Service以及Shiro配置模块,结构清晰,便于直接导入项目对照学习。资源内还包含GIF动态演示,可直观查看登录、注册、验证码校验等交互过程,降低上手理解成本。目前已有988人学习下载,既适合课程设计借鉴,也能作为企业项目前期认证模块的快速搭建基础。
1. 登录注册模板不是玩具,是后端项目的起跑线
任何一个带用户体系的 Web 项目,无论最后做成电商、后台管理还是内容站,第一块地基都是登录注册。拿到一个名为“基于SpringBoot+Layui的登录注册模板.zip”的压缩包,本质上等于拿到了一套已经跑通的前后端联调范式:SpringBoot 负责接口与鉴权,Layui 负责页面与交互,二者通过 JSON 对接,不引入前端工程化构建链。这套模板最大的价值不是那几个页面,而是告诉你怎么组织后端分层、怎么处理会话、怎么写统一返回体、怎么在前端弹窗报错。
这个东西适合谁?首先是 Java 后端刚起步的人,需要一个不过度设计的范本;其次是做毕业设计或公司内部系统的人,想要一个能直接改的底子;还有一些是接了老项目维护任务的人,需要快速理解 SpringBoot + 模板引擎时代的代码长什么样。接下来我会按实际开发顺序拆开这个模板:先讲技术选型的来龙去脉,再讲工程结构和配置,然后分别过一遍后端与前端的关键代码,最后落在验证部署和版本坑上。
2. SpringBoot 与 Layui 组合的定位:前后端不分离时代的最后一块舒适区
2.1 为什么这个组合至今仍被大量模板采用
SpringBoot 诞生已超过十年,Layui 则更早,二者都不是新东西,但“SpringBoot + Layui”这个组合在中文技术社区里一直有稳定的存量需求。你去看任何源码下载站、毕设项目站,JavaWeb 分类下总量最大的仍然是这类组合。原因不复杂:Layui 是一套纯浏览器端加载的 UI 框架,引入方式是写一个layui.js和layui.css,不需要 Node.js、不需要 Webpack,也不存在跨域问题,因为它和后端在同一个域名下跑。SpringBoot 则把原本繁琐的 SSM 配置收拢成自动配置,写一个@RestController就能返回 JSON。
对比现在的 Vue + SpringBoot 前后端分离方案,Layui 模式的优势是“快”,劣势是“边界模糊”——前端代码和后端代码在同一个工程里,Java 代码里偶尔会飘着 JS 片段,页面逻辑无法独立测试。对学习者和中小型内部系统来说,快就是硬道理。SpringBoot 面试题里经常问的自动装配原理,在这个组合里体现得最直观:你引入spring-boot-starter-web后,DispatcherServlet、Tomcat、Jackson全部自动就位,你只写业务类即可。
2.2 Layui 的核心使用方式:layui.use 与模块化加载
Layui 的使用方式不是写一堆<script>标签引入组件,而是通过它的模块加载器按需拉取。常见做法是在 HTML 底部写这样一段初始化代码:
<script src="/lib/layui/layui.js"></script> <script> layui.use(['form', 'layer', 'laydate', 'jquery'], function(){ var form = layui.form; var layer = layui.layer; var $ = layui.jquery; // 表单提交监听 form.on('submit(loginSubmit)', function(data){ console.log(data.field); return false; // 阻止表单原生提交 }); }); </script>这里说明几个参数:layui.use第一参数是数组,里面列当前页面要用到的模块名,官方内置模块有form、layer、laydate、table、upload等;回调函数里通过layui.form拿到模块实例,这个过程叫“模块加载”。form.on('submit(loginSubmit)')中loginSubmit是<form>上lay-filter属性的值,不是 name 也不是 id,这个细节经常被搞混。
Layui 官网早已停止大版本更新,但 2.8+ 版本仍然可用,社区里也有针对 Vue 或原生 JS 的适配方案讨论。如果你的项目里有人问“layui 可以用 vue 吗”,技术上的回答是:Vue 是数据驱动视图,Layui 是直接操作 DOM,二者在表单联动场景下会互相干扰,不建议混用,模板项目里也不该出现这种架构。
2.3 SpringBoot 版本选择:模板默认 2.x,别一上来追 3.x
模板里的 SpringBoot 版本通常落在 2.7.x 这个区间。原因很简单:3.x 要求 JDK 17 起步,默认 Jakarta EE 命名空间(javax.*改成jakarta.*),很多老教程和老代码片段直接失效。如果你是在 idea 创建 SpringBoot 项目时选了 3.x,再去抄模板里的代码,会遇到HttpServletRequest导入不存在的报错,那不是代码问题,是版本差异。
如果你的环境只装了 JDK 17 或 21,又不想退回 1.8,有两条路:一是直接用 SpringBoot 3.x,同时把模板里的javax.servlet包名全部换成jakarta.servlet,改动量不大;二是用 IDE 的 Project Structure 把项目 SDK 切到 8,只要系统里装了 JDK 8 就能跑 2.x 的项目。模板类项目最忌讳在版本上标新立异,我的习惯是“SpringBoot 2.7.13 + JDK 8 + Maven 3.6+”,这套组合历经大量生产环境验证,兼容性风险最低。
2.4 登录注册的功能边界:模板里必须包含什么
一个合格的登录注册模板,功能上至少要覆盖三件事:注册时的重复名校验、登录时的密码校验、访问受限资源时的拦截跳转。再往上是验证码、短信登录、OAuth2 第三方登录,这些属于增强功能,模板可以不含,但代码结构上必须留出扩展位。下面用一个表格理清模板各层要承担的责任:
| 层次 | 对应位置 | 关键职责 |
|---|---|---|
| 控制层 | com.example.demo.controller | 接收 HTTP 请求,参数绑定,不写业务 |
| 业务层 | com.example.demo.service | 校验逻辑、密码加密、事务控制 |
| 数据访问层 | com.example.demo.mapper | SQL 操作,MyBatis 或 JPA 二选一 |
| 前端页面 | src/main/resources/static与templates | 用 Layui 渲染表单和反馈 |
| 会话状态 | HttpSession 或 Redis | 存储登录态,供拦截器读取 |
看清这张表,你拿到任何模板都能快速定位“哪里改逻辑、哪里改页面”。
3. 工程结构与核心配置:从打开 zip 到项目跑起来
3.1 Maven 目录与配置文件定位
解压 zip 后,第一步不是双击 pom.xml,而是先看目录层级。典型结构如下:
springboot-layui-login/ ├── pom.xml ├── sql/ │ └── login_demo.sql └── src/main/ ├── java/com/example/login/ │ ├── LoginApplication.java │ ├── controller/UserController.java │ ├── service/UserService.java │ ├── mapper/UserMapper.java │ └── config/WebConfig.java ├── resources/ │ ├── application.yml │ ├── mapper/UserMapper.xml │ ├── static/ │ │ ├── css/login.css │ │ └── js/layui.js │ └── templates/ │ ├── login.html │ └── register.html └── webapp/ (若使用JSP)application.yml是核心配置入口,下面给出一个最常见的 MySQL 5.7 配置:
server: port: 8080 servlet: context-path: / spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/login_demo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 thymeleaf: prefix: classpath:/templates/ suffix: .html cache: false mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.login.entity configuration: map-underscore-to-camel-case: true代码块里值得逐项解释:server.port是后端端口,改了之后 Layui 页面的请求地址也要同步改;context-path如果设置成/api,所有接口路径都要加前缀;spring.datasource四项是数据库连接的基础,serverTimezone必须要有,否则新版 MySQL 驱动会报时区错误;thymeleaf配置只在使用模板引擎时生效,如果模板是用纯静态 HTML 加 AJAX 方式做的,这部分可以删;map-underscore-to-camel-case设成 true,数据库nick_name就能自动映射到实体的nickName,省掉一堆@Results注解。
如果把 MySQL 换掉,比如换成 SQLServer,你需要同时改driver-class-name和url,并检查 SQL 文件里是否存在反引号——MySQL 用反引号,SQLServer 不认识。模板一般默认 MySQL,所以这个坑不要忽略。
3.2 数据库脚本:字段设计与初始化数据
模板的sql目录下会有一个建表脚本,核心表就一张,设计非常克制:
CREATE DATABASE IF NOT EXISTS login_demo DEFAULT CHARACTER SET utf8mb4; USE login_demo; DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(64) NOT NULL COMMENT '登录名', `password` varchar(255) NOT NULL COMMENT '密码(BCrypt加密)', `nick_name` varchar(64) DEFAULT NULL COMMENT '昵称', `email` varchar(128) DEFAULT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; INSERT INTO `user` (`username`, `password`, `nick_name`) VALUES ('admin', '$2a$10$mE.qNFmAlQqLOlE1iTj6p.8vM3slP8FvF8iYwVpQcJkDdG1oLbBdS', '管理员');强调两个细节。第一,password字段长度为 255,不要改成 32,因为 BCrypt 加密结果长度是 60 位,这是模板正确性的关键点;第二,初始管理员密码不是明文,而是 BCrypt 密文,你无法通过看数据库知道它是什么,模板通常在 README 里写了默认密码,如果没写,就删掉这条 INSERT,重新注册一个。
3.3 实体类与 Mapper:MyBatis 的 XML 映射
实体类User.java的字段与数据库列一一对应,这里不展开。重点看UserMapper.java和对应的 XML:
package com.example.login.mapper; import com.example.login.entity.User; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; @Mapper public interface UserMapper { User findByUsername(@Param("username") String username); int insert(User user); }XML 里对应的方法是:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.login.mapper.UserMapper"> <select id="findByUsername" resultType="User"> SELECT id, username, password, nick_name, email, create_time FROM user WHERE username = #{username} </select> <insert id="insert" parameterType="User" useGeneratedKeys="true" keyProperty="id"> INSERT INTO user (username, password, nick_name, email) VALUES (#{username}, #{password}, #{nickName}, #{email}) </insert> </mapper>namespace必须是 Mapper 接口的全限定名,id必须等于接口方法名,二者错一个运行时就报绑定异常。resultType写User的前提是type-aliases-package配置正确,并且map-underscore-to-camel-case已开启,否则nick_name映射不到nickName上。
这里提一个 MyBatis 与 SpringBoot 的兼容性注意点:如果你用的是 SpringBoot 3.x,需要引入mybatis-spring-boot-starter的 3.0+ 版本,2.x 版本的 starter 在 SpringBoot 3.x 下会因为包名变更而启动失败。模板里如果是mybatis-spring-boot-starter 2.x,配套 SpringBoot 2.x,切勿混装。
4. 后端认证逻辑:Session、拦截器与密码加密
4.1 注册接口:参数校验与 BCrypt 加密落地
注册接口是模板里逻辑最完整的一块,它要同时处理格式校验、查重和加密存储。看下面这段UserService代码:
package com.example.login.service; import com.example.login.common.Result; import com.example.login.entity.User; import com.example.login.mapper.UserMapper; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import javax.annotation.Resource; @Service public class UserService { @Resource private UserMapper userMapper; private BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); public Result<?> register(String username, String password, String email) { if (!StringUtils.hasText(username) || !StringUtils.hasText(password)) { return Result.error(400, "用户名和密码不能为空"); } if (username.length() < 3 || username.length() > 20) { return Result.error(400, "用户名长度需在3到20个字符之间"); } if (userMapper.findByUsername(username) != null) { return Result.error(400, "用户名已存在"); } User user = new User(); user.setUsername(username); user.setPassword(encoder.encode(password)); user.setEmail(email); userMapper.insert(user); return Result.success("注册成功"); } public Result<User> login(String username, String password) { User user = userMapper.findByUsername(username); if (user == null || !encoder.matches(password, user.getPassword())) { return Result.error(401, "用户名或密码错误"); } return Result.success(user); } }这段代码里最关键的一行是encoder.encode(password)。为什么不直接存明文?因为一旦数据库泄露,所有用户密码直接暴露,且用户很可能在其他平台复用了同一个密码,这就是撞库攻击的根源。BCrypt 算法自带盐值,同样的明文每次加密结果不同,破解成本高,是 Spring Security 内置的默认实现,不需要额外引入完整的安全框架,只引spring-security-crypto包即可。
matches(password, user.getPassword())是校验过程,它从密文中解析盐值并重新计算比对,注意参数顺序不能反:第一参是明文,第二参是库里的密文。如果你从网上粘了一段自定义 MD5 加密,趁早换掉,MD5 早已不适合存储密码。
注册里查重这步还有并发问题:两个请求同时查到用户名不存在,同时 insert,后提交的会爆唯一键冲突。模板通常不做这层处理,但你在理解时要心里有数,生产方案是捕获DuplicateKeyException转为友好提示,或者用数据库唯一索引兜底。
4.2 登录接口与统一返回体:模板的隐形规范
登录注册接口的返回值,模板几乎都采用统一的 JSON 结构,常见的是code + msg + data三字段。这也是前后端不分离项目里最容易统一风格的地方,后端不管成功失败都返回这个结构,前端用code === 200判断成败:
{ "code": 200, "msg": "登录成功", "data": { "id": 1, "username": "admin", "nickName": "管理员" } }配合一个Result工具类来生成:
package com.example.login.common; public class Result<T> { private int code; private String msg; private T data; public static <T> Result<T> success(T data) { Result<T> r = new Result<>(); r.code = 200; r.msg = "success"; r.data = data; return r; } public static <T> Result<T> error(int code, String msg) { Result<T> r = new Result<>(); r.code = code; r.msg = msg; return r; } // getter/setter 省略 }注意data的类型是泛型,失败时它为 null,前端拿不到也不报错。这个返回体是整个前后端联调的契约,如果拆成前后端分离架构,它就是接口文档的雏形。SpringBoot 默认用 Jackson 序列化对象为 JSON,LocalDateTime类型字段需要额外加@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss"),否则返回一串带 T 的 ISO 格式字符串,Layui 表格渲染会不好看。
登录成功后的会话保持方式是模板的另一个分水岭:有的用 HttpSession,有的用 JWT。模板项目几乎都选 Session,因为简单且同域部署天然支持。对应代码是request.getSession().setAttribute("loginUser", user),后续请求通过拦截器检查这个属性是否存在。
4.3 拦截器:保护需要登录才能访问的页面
登录状态检查不能靠每个 Controller 里 copy 一段 if,而是用 Spring MVC 的拦截器统一处理。模板中的WebConfig是这样配置的:
package com.example.login.config; import com.example.login.interceptor.LoginInterceptor; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import javax.annotation.Resource; @Configuration public class WebConfig implements WebMvcConfigurer { @Resource private LoginInterceptor loginInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns("/**") .excludePathPatterns( "/login", "/doLogin", "/register", "/doRegister", "/css/**", "/js/**", "/images/**", "/lib/**" ); } }addPathPatterns("/**")匹配所有路径,excludePathPatterns排除掉登录注册接口和静态资源。这里最容易踩的坑是漏掉静态资源目录:Layui 的 js、css 文件全在/lib下,如果不排除,登录页都加载不出样式,因为资源请求被拦截重定向了。
LoginInterceptor的实现很简短:
package com.example.login.interceptor; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; @Component public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); if (session.getAttribute("loginUser") != null) { return true; } // 判断是否为 AJAX 请求 String xRequestedWith = request.getHeader("X-Requested-With"); if ("XMLHttpRequest".equals(xRequestedWith)) { response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":401,\"msg\":\"未登录或会话已过期\"}"); } else { response.sendRedirect("/login"); } return false; } }这个拦截器区分了 AJAX 请求和普通页面跳转。如果用户在登录状态下打开了一个页面,Session 超时后再点页面里的按钮,Layui 的 AJAX 请求会收到 401 的 JSON,而不是一个 302 重定向页面——后者会导致前端解析 HTML 报语法错误。Layui 的$.ajax默认不会跟随重定向到 JSON 接口,这个细节是在实际联调中才会暴露的坑。
5. Layui 页面与接口对接:登录表单、注册表单与反馈交互
5.1 登录页:form.on 是核心入口
登录页的 HTML 结构本身不复杂,一个form加两个输入框加一个按钮,关键是lay-filter值前后端要一致。看这段连 HTML 带 JS 的完整片段:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>用户登录</title> <link rel="stylesheet" href="/lib/layui/css/layui.css"> <style> body { background: #f2f2f2; } .login-box { width: 380px; margin: 120px auto; background: #fff; padding: 30px; border-radius: 6px; } </style> </head> <body> <div class="login-box"> <h2 style="text-align:center;margin-bottom:20px;">用户登录</h2> <form class="layui-form" lay-filter="loginForm"> <div class="layui-form-item"> <input type="text" name="username" required lay-verify="required" placeholder="用户名" autocomplete="username" class="layui-input"> </div> <div class="layui-form-item"> <input type="password" name="password" required lay-verify="required" placeholder="密码" autocomplete="current-password" class="layui-input"> </div> <div class="layui-form-item"> <button class="layui-btn layui-btn-fluid" lay-submit lay-filter="loginSubmit">登录</button> </div> </form> </div> <script src="/lib/layui/layui.js"></script> <script> layui.use(['form', 'layer', 'jquery'], function(){ var form = layui.form; var layer = layui.layer; var $ = layui.jquery; form.on('submit(loginSubmit)', function(data){ $.ajax({ url: '/doLogin', type: 'POST', contentType: 'application/json', data: JSON.stringify(data.field), success: function(res){ if (res.code === 200) { layer.msg('登录成功', {icon: 1, time: 1000}, function(){ window.location.href = '/index'; }); } else { layer.msg(res.msg, {icon: 2}); } }, error: function(){ layer.msg('网络请求失败', {icon: 2}); } }); return false; }); }); </script> </body> </html>代码逻辑说明:lay-submit和lay-filter="loginSubmit"搭配,click 事件才不会走按钮原生行为,而是进入form.on('submit(loginSubmit)')回调;data.field是 Layui 自动收集的表单数据对象,key 对应输入框的name属性;JSON.stringify(data.field)把对象转 JSON 字符串,后端用@RequestBody接收。
这里有一个容易踩的坑:如果你用contentType: 'application/json',后端必须用@RequestBody接收,如果后端 Controller 写的是@RequestParam,会直接报 415 或参数缺失。一致的选择是“全 JSON 或全表单编码”。上面的代码选了 JSON,那么后端 Controller 就要写成这样:
@PostMapping("/doLogin") public Result<User> doLogin(@RequestBody LoginParam param, HttpServletRequest request) { Result<User> result = userService.login(param.getUsername(), param.getPassword()); if (result.getCode() == 200) { request.getSession().setAttribute("loginUser", result.getData()); } return result; }LoginParam是一个内部类或独立类,字段名username、password必须与前端data.field的 key 完全一致,Jackson 才能完成映射。如果你在拦截器里已经写了会话处理,这里只存 Session 即可。
5.2 注册页:lay验证规则与重复密码校验
注册页在登录页基础上增加字段,同时要处理“确认密码”一致性校验。Layui 自带lay-verify规则,也支持自定义规则:
layui.use(['form', 'layer', 'jquery'], function(){ var form = layui.form; var layer = layui.layer; var $ = layui.jquery; form.verify({ pass: [ /^[\S]{6,12}$/, '密码必须为6到12位,且不能出现空格' ], confirmPass: function(value){ var pwd = $('#password').val(); if (value !== pwd) { return '两次输入的密码不一致'; } } }); form.on('submit(regSubmit)', function(data){ $.ajax({ url: '/doRegister', type: 'POST', contentType: 'application/json', data: JSON.stringify(data.field), success: function(res){ if (res.code === 200) { layer.alert('注册成功,请登录', {icon: 1}, function(index){ layer.close(index); window.location.href = '/login'; }); } else { layer.msg(res.msg, {icon: 2}); } } }); return false; }); });自定义规则confirmPass在函数内部拿到当前输入值和#password的 DOM 值做比对,不一致就返回字符串,Layui 会把这个字符串当作错误提示展示在表单项下方。form.verify的三种写法要分清:正则直接写在[/.../, '提示']里;函数用function(value){ return '错误信息'; };如果需要读取其他字段,函数内用 jQuery 取值即可。
注册页里如果包含出生日期或入职日期这类字段,可以引出laydate组件——热搜词里有“laydate 最大日期当前日期”,这是 laydate 最常见的自定义需求,因为默认允许选未来日期,而用户生日不能晚于今天。正确写法是在渲染时指定max参数:
laydate.render({ elem: '#birthday', max: 0, // 0 表示当前日期为最大值 trigger: 'click' });max: 0是指“到今天为止”,如果写max: '2024-01-01'就是固定写死。动态最大日期的另一种做法是用new Date()生成字符串,但 laydate 传0是官方支持的最省事写法。
5.3 全局 AJAX 统一处理:哪种方案更适合模板
登录注册页每个页面都写一遍 AJAX 错误处理,代码很冗余。模板里常见的进阶做法是在页面中引入一个公共 JS 文件,重写 Ajax 的 success 回调:
// common.js layui.use(['layer', 'jquery'], function(){ var $ = layui.jquery; var layer = layui.layer; $.ajaxSetup({ dataType: 'json', complete: function(xhr) { if (xhr.status === 401) { layer.msg('登录已过期,请重新登录', {icon: 2}, function(){ window.location.href = '/login'; }); } } }); });这里监听 401 状态码,专门处理拦截器中将未登录请求标记为 401 的场景。这个文件在登录页不要引入,因为在未登录状态下访问登录页不算异常;注册页可以引入,注册接口本身不需要登录态。
6. 部署验证与常见陷阱:模板跑通了还能怎么演进
6.1 启动与验证的最小步骤
拿到模板后,按照“改配置 → 建库 → 起服务”三步走,验证接口是否联通。先看pom.xml里有没有spring-boot-maven-plugin,再确认依赖是否下载完整,然后执行:
mvn clean package -DskipTests java -jar target/login-demo-0.0.1-SNAPSHOT.jar启动成功后,用 curl 先验证后端接口,不要急着开浏览器。这一步能快速区分“后端 bug”和“前端 bug”:
curl -X POST http://localhost:8080/doRegister \ -H 'Content-Type: application/json' \ -d '{"username":"test01","password":"123456","email":"test@example.com"}' curl -X POST http://localhost:8080/doLogin \ -H 'Content-Type: application/json' \ -d '{"username":"test01","password":"123456"}'注意相同路径下,接口地址是否与前端代码里写的/doRegister完全一致,context-path是否已改。两条命令返回的 JSON 里code都应为 200。如果你在 Windows 环境下,curl 命令里 JSON 字符串要用双引号包整体、内部字段用单引号,或者直接转义,否则会报参数格式错误。
端口被占用是模板项目启动最常见的错误:Port 8080 was already in use。查看并释放端口的命令是:
netstat -ano | findstr :8080 taskkill /F /PID 进程号6.2 SpringBoot 版本过高导致的兼容性问题
如果你创建项目时用了最新版 SpringBoot,但不是模板自带的版本,会遇到几个典型报错。第一个是javax.servlet不存在,进入 SpringBoot 3.x 后所有涉及 Servlet API 的类都要改jakarta.servlet,拦截器、Controller、过滤器四处都要动;第二个是 MyBatis 版本不匹配,控制台提示Invalid value type for attribute 'factoryBeanObjectType',去把mybatis-spring-boot-starter升到 3.0+ 即可;第三个是 JDK 版本不兼容导致编译失败,SpringBoot 2.x 配合 JDK 17 会报cannot access class sun.misc之类的错误,最省事的解决方案是把项目的 Java level 调回 8。
6.3 从模板到生产系统:验证码与权限模型的演进
登录注册模板的尽头是真实项目,首要增强点是加图形验证码。常见做法是引入kaptcha或hutool-captcha生成图片,把验证码文本存入 Session,登录时先校验验证码再校验密码。这只是防御脚本刷接口,更严谨的做法是加登录失败次数限制,连续失败 5 次锁定账号 15 分钟,这个逻辑可以用一个ConcurrentHashMap<String, AtomicInteger>做内存版本,也可以用 Redis 做分布式版本。
权限模型方面,模板通常只有登录与未登录的区别。后续演进路线是引入角色字段,把user表加一个role列,然后在拦截器中判断角色是否可以访问/admin/**路径。更完整的方案是接 Spring Security,但那是另一个量级的复杂度,只做内部后台的话拦截器加角色判断足够撑到几百用户量级。
6.4 一个实用的 Session 超时处理技巧
模板的登录态存在 Session 里,默认超时时间是 30 分钟,这个值在application.yml中可以调整:
server: servlet: session: timeout: 60m如果设置为0或负数,Session 永不过期,适合纯内网演示环境,但生产环境不要这么干。为了在前端体验上感知超时,最简单的方案是前端每隔一段时间发一个轻量请求刷新 Session 或探测在线状态,这个请求路径可以排除在拦截器之外,比如/heartbeat。定时调用的 JS 代码用setInterval15 分钟发一次即可。这个方法成本很低,但能避免“写着写着突然 401 跳回登录页”的糟糕体验。
至此,一个基于 SpringBoot 与 Layui 的登录注册模板已经被完整拆解过一遍:先理解组合存在的理由,再看配置与数据库设计,然后分别过一遍后端会话逻辑与前端表单交互,最后落到启动验证与版本兼容性处理上。后续翻看其他同类模板时,重点观察它如何处理统一返回体、如何做 Session 持久化、是否做好了静态资源放行——这三个点决定了这套代码是教学演示级还是可用工程级。
本文还有配套的精品资源,点击获取