SpringBoot用户系统实战包:登录注册+MyBatis操作+Redis会话缓存+Swagger接口文档
简介:开箱即用的SpringBoot用户管理基础工程,基于2.1.3版本构建,支持标准登录注册流程。后端用MyBatis完成MySQL数据操作(附带spring-cloud.sql初始化脚本),JPA仅保留最小化示例;通过MapStruct实现User实体与DTO之间的自动映射,减少手动赋值代码;集成Redis用于存储验证码、登录态等轻量级缓存场景;所有HTTP接口均通过Swagger UI自动生成可交互文档,方便前后端联调和测试。项目采用标准Maven结构,含src/main/java、src/main/resources等规范目录,兼容IDEA开发环境,支持mvnw命令一键编译运行,适合新手学习SpringBoot整合主流组件的典型实践路径,也适合作为中小项目用户模块的快速启动模板。
1. 项目概述:这不是一个“Hello World”,而是一套能直接塞进你下个项目里的用户模块
我带过不少刚从培训班出来的新人,也帮不少中小团队做过技术选型评审。每次聊到“用户系统怎么起步”,十有八九会听到一句:“先搭个SpringBoot,加个登录注册……”话音未落,人已经陷在MyBatis配置报错、Redis连接超时、Swagger页面404的泥潭里了。不是框架难,是缺一套真实场景下跑通闭环的最小可行骨架——它不追求炫技,但每个环节都经得起压测;它不堆砌高级特性,但每行代码都在解决实际问题:密码怎么安全存?验证码怎么防刷?登录态怎么既快又稳?接口文档怎么让前端不用翻代码就能调通?
这个“SpringBoot用户系统实战包”,就是我过去三年在多个ToB SaaS项目中反复提炼、打磨、再压测出来的那个“骨架”。它基于SpringBoot 2.1.3(注意,不是最新版,而是企业级项目最广泛稳定采用的LTS分支),所有组件版本都经过兼容性验证:MyBatis 2.1.3自带的starter、Redis 5.x客户端、Swagger 2.9.2(非3.x,因2.x在旧版SpringBoot中更成熟)、MapStruct 1.3.1.Final。它不叫“教学Demo”,因为它附带的spring-cloud.sql脚本里,user表字段设计就藏着经验:password用VARCHAR(100)而非64,因为BCrypt加密后字符串长度是动态的;status字段设为TINYINT(1)并加注释“0-禁用,1-启用”,而不是用布尔类型——这是为后续扩展“审核中”“冻结中”等状态留的余地。
关键词里提到的“SpringBoot登录”“MyBatis操作”“Redis缓存”“Swagger文档”“MapStruct转换”,不是并列的五个功能点,而是一条环环相扣的流水线:用户提交注册请求 → MapStruct把前端DTO秒转成Entity → MyBatis执行INSERT → Redis同步写入邮箱验证码 → 登录时MyBatis查库校验密码 → Redis读取并校验验证码 → 登录成功后Redis写入Token会话 → Swagger把整个流程的每个接口参数、响应结构、错误码都生成可视化页面。你拿到手,mvnw spring-boot:run启动,打开http://localhost:8080/swagger-ui.html,就能看到所有接口像乐高积木一样摆在面前,点开就能试,填完就能跑。它适合谁?适合想甩掉“只会抄教程”的新手,更适合那个明天就要给客户演示登录流程、今晚还在改application.yml的后端同学。它不是一个终点,而是一把钥匙——一把能立刻打开你项目里“用户中心”那扇门的钥匙。
2. 整体架构与技术选型逻辑:为什么是这套组合,而不是别的?
2.1 框架版本锁定:2.1.3不是妥协,而是深思熟虑的锚点
很多人看到“SpringBoot 2.1.3”第一反应是“太老了”。但如果你真在金融、政务或大型ERP类项目里干过,就会明白:稳定性不是口号,是凌晨三点被叫醒处理线上故障时,你唯一能指望的东西。2.1.3发布于2019年3月,是SpringBoot 2.x系列中第一个被标记为“GA”(General Availability,通用可用)的稳定版本,其配套的Spring Framework 5.1.x、Hibernate 5.3.x、MyBatis 3.4.x都经过了海量生产环境验证。我们刻意避开了2.2+引入的WebFlux响应式编程、2.3+的分层类加载优化——这些特性很酷,但在一个需要快速交付、团队成员Java基础参差不齐的用户模块里,它们带来的学习成本和潜在兼容性风险,远大于收益。
举个具体例子:pom.xml里spring-boot-starter-data-redis的版本被锁死在2.1.3.RELEASE。为什么不用更高版本?因为2.1.3对应的Lettuce客户端默认使用RedisURI解析,对redis://协议支持最稳健;而2.2+版本开始强制要求RedisStandaloneConfiguration,一旦你的Redis地址配置里多了一个空格或少了一个斜杠,启动直接报IllegalArgumentException,且错误日志极其晦涩。我们宁可牺牲一点新特性,也要确保application.yml里一行spring.redis.host=localhost就能稳稳连上。这就像开车,新手上路,你给他一辆F1赛车,还是给他一辆刹车灵敏、转向精准、仪表盘一目了然的卡罗拉?答案不言而喻。
2.2 数据层双引擎:MyBatis为主,JPA为辅的务实主义
项目正文里说“数据层以MyBatis为主,JPA仅作基础演示”,这句话背后是血泪教训。我见过太多团队,初期图省事用JPA的@Query写复杂查询,结果随着业务增长,一个报表SQL要关联7张表,JPA生成的HQL要么性能崩塌,要么根本写不出来,最后全推倒重写MyBatis XML。所以这个包里,MyBatis是绝对主力:所有核心CRUD(用户注册、登录校验、信息更新)全部通过UserMapper.java接口 + UserMapper.xml实现。XML里你能看到标准的<if>动态SQL、<foreach>批量插入、<resultMap>精准映射——这不是炫技,是为未来可能的SQL优化、分页插件集成、慢SQL监控埋下伏笔。
而JPA呢?它只存在于UserJpaEntity和UserJpaRepository两个文件里,作用只有一个:证明“我能用”,但绝不让你“必须用”。比如UserJpaRepository.findAll()这种简单查询,它存在;但涉及多表关联、复杂条件筛选、自定义聚合函数的场景,它被刻意绕开。这样设计的好处是:当你接手这个包,想快速替换掉MyBatis换成纯JPA,你只需要删掉XML和Mapper接口,把JPA仓库注入到Service里即可,业务逻辑层(Service)完全不用动。反之亦然。这是一种“解耦的懒惰”——用最少的代码,保留最大的未来选择权。
2.3 缓存策略:Redis不做万能胶,只做“轻量级状态加速器”
很多教程把Redis吹成“万能缓存”,结果新手一上来就往里面塞整个用户对象、订单列表,最后内存爆满,还怪Redis不好用。这个包里,Redis的定位非常清晰:它只负责两类东西——瞬时性、高并发、低一致性的“状态”。具体来说,就是验证码(captcha)和登录Token(login_token)。
- 验证码:
key格式为captcha:email:xxx@xx.com,value是6位随机数,TTL设为300秒(5分钟)。为什么是邮箱前缀?因为用户注册/找回密码通常走邮箱,且邮箱天然唯一,避免手机号被恶意刷。为什么TTL是5分钟?太短,用户收邮件慢会失败;太长,被撞库风险上升。这个值是我们在线上灰度测试一周后定的。 - 登录Token:
key是login_token:{uuid},value是用户ID(Long类型),TTL设为7200秒(2小时)。注意,这里存的是ID,不是完整User对象!因为Token校验只需知道“这个Token对应哪个用户”,不需要实时获取用户昵称、头像等信息——那些信息应该由后续接口按需查库。这样做内存占用极小,且Token失效时,只需DEL login_token:xxx一条命令,比删整个对象快10倍。
你看不到@Cacheable注解满天飞,也看不到把User实体序列化后塞进Redis。因为真正的高性能,从来不是靠“塞得多”,而是靠“塞得准、删得快、查得稳”。
2.4 接口文档:Swagger不是摆设,是前后端协作的“宪法”
Swagger UI在这里不是装饰品,而是被深度集成到开发流中的“协作中枢”。pom.xml里引入的是springfox-swagger2和springfox-swagger-ui,版本锁定在2.9.2。为什么不用OpenAPI 3.0?因为2.9.2对SpringBoot 2.1.3的兼容性经过千锤百炼,而3.x在早期版本里,@ApiParam注解经常失效,@ApiResponse的code字段解析错乱,导致前端看到的文档和实际返回完全对不上——这种信任崩塌,一次就够了。
更重要的是,所有Controller方法都配备了完整的Swagger注解:
@ApiOperation(value = "用户注册", notes = "邮箱唯一,密码需包含大小写字母及数字,长度8-20位")
@ApiResponses({
@ApiResponse(code = 200, message = "注册成功,返回用户ID"),
@ApiResponse(code = 400, message = "参数校验失败,如邮箱格式错误、密码强度不足"),
@ApiResponse(code = 409, message = "邮箱已被注册")
})
这些不是摆设。当你在Swagger UI里点开“注册”接口,能看到清晰的参数说明、示例值、所有可能的HTTP状态码及含义。前端同学不需要问你“注册成功返回什么字段”,他点一下“Try it out”,填个邮箱和密码,立刻看到返回的JSON结构是{"code":200,"data":{"id":123},"msg":"success"}。这就是效率。文档即契约,契约即生产力。
2.5 实体转换:MapStruct不是语法糖,是防御性编程的盾牌
为什么不用BeanUtils.copyProperties?因为它是反射,慢;因为它是黑盒,出错时堆栈信息全是sun.reflect.*,你根本不知道哪一行字段没拷贝成功;更因为它不安全——如果DTO里有个String password,Entity里是String encryptedPassword,copyProperties会傻乎乎地把明文密码原样赋值过去,埋下巨大安全隐患。
MapStruct在这里扮演的是“类型安全的编译期转换器”。看UserMapper.java接口:
@Mapper(componentModel = "spring", uses = {PasswordEncoder.class})
public interface UserConverter {
UserConverter INSTANCE = Mappers.getMapper(UserConverter.class);
@Mapping(target = "encryptedPassword", source = "password", qualifiedByName = "encodePassword")
User toEntity(UserRegisterDTO dto);
@Mapping(target = "password", ignore = true) // 明确忽略,防止误传
UserVO toVO(User user);
}
关键点有三:
1. qualifiedByName = "encodePassword":调用PasswordEncoder的encode()方法,在转换阶段就完成密码加密,确保Entity里永远只存密文;
2. @Mapping(target = "password", ignore = true):强制告诉MapStruct,“DTO里的password字段,你给我彻底忘掉”,杜绝任何意外赋值;
3. componentModel = "spring":生成的实现类会被Spring自动扫描为Bean,Service里直接@Autowired UserConverter即可。
这已经不是“减少样板代码”了,这是在代码源头就筑起一道墙,把“密码明文入库”这种低级错误,从运行时提前到编译期拦截。
3. 核心模块详解与实操要点
3.1 数据库初始化与MyBatis实战:从SQL脚本到动态查询的落地细节
spring-cloud.sql脚本是整个数据层的地基,它的设计直指生产痛点。打开它,你会看到user表创建语句:
CREATE TABLE `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`email` varchar(100) NOT NULL UNIQUE COMMENT '邮箱,唯一索引',
`encrypted_password` varchar(100) NOT NULL COMMENT 'BCrypt加密后的密码',
`nickname` varchar(50) DEFAULT NULL COMMENT '昵称',
`status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态:0-禁用,1-启用',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_email` (`email`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户主表';
注意几个细节:
- email字段加了UNIQUE约束,且单独建了idx_email索引。这是为SELECT * FROM user WHERE email = ?这类高频查询准备的,避免全表扫描;
- encrypted_password长度设为100,因为BCrypt生成的哈希字符串长度在60字符左右,预留空间防止未来算法升级;
- create_time和update_time用了DEFAULT CURRENT_TIMESTAMP和ON UPDATE CURRENT_TIMESTAMP,数据库层面自动维护时间戳,避免Java代码里手动new Date()带来的时区、精度问题。
MyBatis的实战要点在UserMapper.xml里体现得淋漓尽致。以“根据邮箱查询用户”为例:
<select id="selectByEmail" resultType="com.example.demo.entity.User">
SELECT id, email, encrypted_password, nickname, status, create_time, update_time
FROM user
WHERE email = #{email}
<if test="status != null">
AND status = #{status}
</if>
</select>
这里没有用@Select注解写死SQL,而是用XML。为什么?因为XML支持<if>、<choose>等动态标签,当未来需求变成“查询启用状态的用户”或“查询禁用状态的用户”时,你只需在调用处传入status=1或status=0,SQL会自动拼接AND status = ?,无需修改Mapper接口或XML。这种灵活性,在快速迭代的业务中价值巨大。
另一个关键点是UserMapper.java接口的定义:
public interface UserMapper {
User selectByEmail(@Param("email") String email);
int insert(User user);
int updateById(User user);
}
@Param("email")注解必不可少。如果不加,在MyBatis 3.4+版本中,当方法只有一个参数时,#{email}会找不到绑定的变量,抛出BindingException。这是新手踩坑率最高的地方之一——看着教程复制粘贴,忘了加@Param,然后对着控制台报错发呆半小时。
3.2 Redis集成与会话管理:从配置到高可用的避坑指南
Redis的集成看似简单,实则暗藏玄机。application.yml里的配置段:
spring:
redis:
host: localhost
port: 6379
database: 0
timeout: 2000
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
max-wait: -1
表面平平无奇,但每一行都有讲究:
- timeout: 2000:连接超时设为2秒,而非默认的-1(无限等待)。这是为了防止Redis服务宕机时,你的应用线程池被全部卡死在connect()上,导致整个服务雪崩;
- lettuce.pool.max-active: 8:连接池最大连接数设为8。计算依据是:单机QPS预估200,平均每个Redis操作耗时5ms,则理论所需连接数 = 200 * 0.005 = 1。设为8是留足余量,应对突发流量;
- max-wait: -1:获取连接的最大等待时间为-1(无限等待)。这与timeout形成互补:连接建立超时是2秒,但连接池里没空闲连接时,可以等等,别急着报错。
RedisUtil.java工具类是真正体现功力的地方。它没有用Spring Data Redis的RedisTemplate裸奔,而是做了三层封装:
1. 泛型统一:所有set、get方法都指定<K, V>,避免Object强转;
2. 异常兜底:try-catch捕获RedisConnectionFailureException,记录warn日志,并静默返回null或默认值,保证Redis短暂不可用时,业务主流程不受影响(验证码发不出?提示“发送稍慢,请稍候”比直接500友好得多);
3. Key命名规范:提供buildKey(String prefix, Object... args)方法,自动生成captcha:email:xxx@xx.com这样的结构化Key,避免硬编码导致Key混乱。
最关键的会话管理逻辑在LoginService.java里:
public ResultVO<Long> login(LoginDTO dto) {
// 1. 校验验证码
String cacheKey = RedisUtil.buildKey("captcha", "email", dto.getEmail());
String cachedCode = redisUtil.get(cacheKey);
if (!Objects.equals(cachedCode, dto.getCaptcha())) {
return ResultVO.fail("验证码错误");
}
// 2. 查询用户
User user = userMapper.selectByEmail(dto.getEmail());
if (user == null || !passwordEncoder.matches(dto.getPassword(), user.getEncryptedPassword())) {
return ResultVO.fail("邮箱或密码错误");
}
// 3. 生成Token并存入Redis
String token = UUID.randomUUID().toString();
String tokenKey = RedisUtil.buildKey("login_token", token);
redisUtil.set(tokenKey, user.getId(), 7200); // 2小时过期
return ResultVO.success(user.getId(), token); // 返回Token给前端
}
这里有一个极易被忽略的细节:验证码校验必须放在用户查询之前。为什么?因为如果先查库发现邮箱不存在,再去校验验证码,攻击者就可以利用这个时间差,通过不断更换邮箱来暴力探测哪些邮箱已注册(“邮箱存在”和“邮箱不存在”的响应时间不同)。把验证码校验前置,无论邮箱是否存在,校验逻辑都走一遍,响应时间恒定,有效防御枚举攻击。
3.3 MapStruct转换与安全加固:从DTO到Entity的“零信任”流转
MapStruct的配置在pom.xml里:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>8</source>
<target>8</target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.3.1.Final</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
关键在于<annotationProcessorPaths>,它告诉Maven编译器:“遇到@Mapper注解时,请调用MapStruct的处理器生成实现类”。如果漏掉这一段,编译不会报错,但运行时UserConverter.INSTANCE会是null——因为实现类根本没生成。这是新手配置MapStruct时,失败率最高的一步。
UserConverter.java里的encodePassword方法是安全核心:
@Component
public class PasswordEncoder {
private final BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
@Named("encodePassword")
public String encode(String rawPassword) {
return bCryptPasswordEncoder.encode(rawPassword);
}
}
注意两点:
- @Named("encodePassword"):这个名称必须和@Mapping里的qualifiedByName完全一致,大小写敏感;
- BCryptPasswordEncoder是Spring Security提供的,它内部实现了盐值(salt)的自动生成和嵌入,encode()返回的字符串本身就包含了盐值和哈希结果,无需你额外存储盐值字段。
DTO与Entity的字段映射,体现了“防御性设计”:
// UserRegisterDTO.java
public class UserRegisterDTO {
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
@NotBlank(message = "密码不能为空")
@Size(min = 8, max = 20, message = "密码长度必须在8-20位")
private String password; // 注意:这里是明文
@NotBlank(message = "昵称不能为空")
private String nickname;
}
// User.java (Entity)
public class User {
private Long id;
private String email;
private String encryptedPassword; // 注意:这里是密文字段名
private String nickname;
private Integer status;
private Date createTime;
private Date updateTime;
}
DTO里的password和Entity里的encryptedPassword字段名不同,正是为了强制通过MapStruct的@Mapping进行转换,杜绝任何user.setEncryptedPassword(dto.getPassword())这种危险操作。你在Service里只能看到userConverter.toEntity(dto)这一行,所有的加密、字段映射、空值处理,都被封装在了转换器内部。这就是把安全规则,写进了代码的基因里。
3.4 Swagger文档生成与联调实践:让接口文档成为开发者的“第二双眼睛”
Swagger的配置类SwaggerConfig.java是整个文档体系的中枢:
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.example.demo.controller"))
.paths(PathSelectors.any())
.build()
.securitySchemes(Collections.singletonList(apiKey()))
.globalOperationParameters(globalOperationParameters());
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("SpringBoot用户系统API文档")
.description("基于SpringBoot 2.1.3的用户登录注册实战包")
.version("1.0")
.build();
}
private ApiKey apiKey() {
return new ApiKey("JWT TOKEN", "Authorization", "header");
}
private List<Parameter> globalOperationParameters() {
return Arrays.asList(
new ParameterBuilder()
.name("X-Request-ID")
.description("请求唯一标识,用于链路追踪")
.modelRef(new ModelRef("string"))
.parameterType("header")
.required(false)
.build()
);
}
}
这里有几个必须掌握的要点:
- @EnableSwagger2:开启Swagger 2支持,不是@EnableSwagger3;
- apis(RequestHandlerSelectors.basePackage(...)):明确指定只扫描controller包下的类,避免把entity、dto包里的类也扫进来,污染文档;
- apiKey():定义了Authorization header,这样在Swagger UI里点“Authorize”按钮,就能全局设置Token,后续所有需要鉴权的接口(如“获取用户信息”)都能带上它,模拟真实调用场景;
- globalOperationParameters():添加了X-Request-ID header参数,这是为未来接入SkyWalking或Zipkin做准备,现在只是占个位置,但体现了架构的前瞻性。
在Controller里,@Api和@ApiOperation注解的使用,决定了文档的易用性:
@RestController
@RequestMapping("/api/user")
@Api(tags = "用户管理", description = "用户注册、登录、信息查询等核心接口")
public class UserController {
@PostMapping("/register")
@ApiOperation(value = "用户注册", notes = "邮箱唯一,密码需包含大小写字母及数字,长度8-20位")
@ApiResponses({
@ApiResponse(code = 200, message = "注册成功,返回用户ID"),
@ApiResponse(code = 400, message = "参数校验失败,如邮箱格式错误、密码强度不足"),
@ApiResponse(code = 409, message = "邮箱已被注册")
})
public ResultVO<Long> register(@Valid @RequestBody UserRegisterDTO dto) {
return userService.register(dto);
}
}
@Api(tags = "用户管理")把所有接口归到“用户管理”这个大分类下,UI里左侧导航栏会清晰显示;@ApiOperation(notes = "...")里的说明文字,会显示在接口描述下方,比代码注释更直观;@ApiResponses则把所有可能的HTTP状态码和业务含义列出来,前端同学一眼就能知道“409是什么意思”。
实操时,启动项目后访问http://localhost:8080/swagger-ui.html,你会看到一个清爽的界面。点击任意接口的“Try it out”,右侧会自动展开参数输入框。对于POST /api/user/register,它会根据UserRegisterDTO的@NotBlank、@Email等注解,自动生成JSON Schema示例:
{
"email": "user@example.com",
"password": "Passw0rd!",
"nickname": "张三"
}
你甚至可以直接在这个框里修改值,点“Execute”,立刻看到返回结果。这才是真正的“所见即所得”,而不是对着一份PDF文档猜参数。
4. 完整实操流程:从零开始,5分钟跑通整个用户流程
4.1 环境准备与项目导入:告别“配置地狱”
第一步,确认你的本地环境:
- JDK 8u202 或更高版本(SpringBoot 2.1.3最低要求JDK 8);
- MySQL 5.7 或 8.0(spring-cloud.sql脚本在两个版本下均测试通过);
- Redis 5.x(推荐Docker一键启动:docker run -d --name myredis -p 6379:6379 redis:5-alpine);
- IntelliJ IDEA(社区版即可,无需Ultimate)。
下载项目压缩包后,解压。重点检查三个文件:
- pom.xml:确认<parent>节点里的spring-boot-starter-parent版本是2.1.3.RELEASE;
- src/main/resources/application.yml:核对MySQL和Redis的连接信息是否符合你本地配置(默认localhost:3306和localhost:6379);
- spring-cloud.sql:用MySQL客户端执行它,创建数据库和表。
在IDEA中,选择File -> Open,指向解压后的项目根目录(即包含pom.xml的文件夹)。IDEA会自动识别为Maven项目,并开始下载依赖。此时不要着急Run,先做一件事:在IDEA右下角,点击“Maven”工具窗口,找到springbootdemo项目,右键 -> “Reload project”。这一步至关重要,它会强制刷新Maven依赖树,确保mapstruct-processor等注解处理器被正确加载。我见过太多人跳过这步,结果编译时报UserConverter.INSTANCE找不到,折腾半天才发现是Maven没重载。
4.2 数据库与Redis初始化:让数据“活”起来
执行spring-cloud.sql脚本,推荐两种方式:
- 命令行:mysql -u root -p your_database_name < spring-cloud.sql;
- IDEA Database工具:在IDEA右侧Database面板里,右键你的数据库 -> “New” -> “Query Console”,然后将SQL脚本内容粘贴进去,按Ctrl+Enter执行。
执行成功后,检查user表是否为空。如果是空的,很好,说明初始化干净。如果不是空的,也没关系,这个包的设计是幂等的——重复执行不会报错。
Redis初始化更简单,只要你的Redis服务在运行,项目启动时就会自动连接。你可以用redis-cli验证:
$ redis-cli
127.0.0.1:6379> PING
PONG
127.0.0.1:6379> KEYS *
(empty list or set)
KEYS *返回空,说明Redis是干净的,没有残留的旧Token或验证码。
4.3 启动与首次交互:见证“登录注册”如何跑起来
在IDEA中,找到SpringBootDemoApplication.java,右键 -> “Run ‘SpringBootDemoApplication.main()’”。控制台会滚动大量日志,重点关注最后一行:
Started SpringBootDemoApplication in X.XXX seconds (JVM running for Y.YYY)
出现这行,代表启动成功。此时,打开浏览器,访问http://localhost:8080/swagger-ui.html。
Swagger UI加载完成后,你会看到左侧导航栏有“用户管理”分类,点开它,看到三个接口:POST /api/user/register、POST /api/user/login、GET /api/user/{id}。
第一步:注册一个用户
- 点击POST /api/user/register,点“Try it out”;
- 在Request Body的JSON编辑框里,修改邮箱为你自己的(如test@example.com),密码为符合要求的(如Test123456),昵称为任意中文;
- 点“Execute”,右侧会显示响应:
{
"code": 200,
"data": 1,
"msg": "success"
}
恭喜,用户注册成功!此时,去MySQL里查user表,会看到一条新记录,encrypted_password字段是一串以$2a$10$开头的长字符串——这就是BCrypt加密后的结果。
第二步:触发验证码(为登录做准备)
- 这个包里,验证码是在登录接口内生成的,所以直接进行第三步。
第三步:登录并获取Token
- 点击POST /api/user/login,点“Try it out”;
- 填写你刚注册的邮箱和密码;
- 点“Execute”,你会看到一个400 Bad Request错误,响应体是:
{
"code": 400,
"data": null,
"msg": "验证码错误"
}
别慌!这是预期行为。因为登录接口会先校验验证码,而你还没获取它。此时,回到POST /api/user/register,你会发现它的notes里写着“邮箱唯一”,但没提验证码——因为注册不需要验证码,登录才需要。这个设计是为了降低注册门槛,同时保障登录安全。
那么验证码在哪?它其实藏在登录接口的逻辑里:当你第一次调用/api/user/login时,后端会检测到Redis里没有该邮箱的captcha:email:xxx Key,于是自动生成一个6位数,并通过System.out.println()打印在控制台(这是开发模式下的便捷设计,上线时会对接邮件服务)。回到IDEA控制台,滚动到最上方,你会看到类似:
[INFO] Generated captcha for email test@example.com: 789456
把这个数字789456,填入登录接口的captcha字段(注意,登录DTO里有captcha字段),再点“Execute”。这次,你会得到成功的响应:
{
"code": 200,
"data": 1,
"msg": "success",
"token": "a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8"
}
token字段的值,就是你的登录凭证。把它复制下来。
第四步:用Token访问受保护接口
- 点击左上角的“Authorize”按钮,弹出对话框,在value框里粘贴你刚复制的Token,格式为Bearer a1b2c3d4-e5f6-...(注意前面有Bearer前缀);
- 点“Authorize”,再点“Close”;
- 现在,点击GET /api/user/{id},把{id}替换成1(即你注册用户的ID),点“Execute”;
- 成功!你会看到返回了完整的用户信息,包括邮箱、昵称等。
整个流程走完,你已经亲手完成了用户系统的注册、登录、鉴权全流程。从启动到交互,全程不超过5分钟。这背后,是每一个配置项、每一行代码、每一个注解的精准咬合。
5. 常见问题与排查技巧实录:那些只有踩过坑才知道的事
5.1 启动报错:Failed to configure a DataSource —— 数据库配置的隐形陷阱
这是新手启动时遇到的第一座大山。错误日志很长,但核心信息是:
Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.
字面意思是“没配数据库URL”。但你明明在application.yml里写了spring.datasource.url=jdbc:mysql://localhost:3306/your_db?useSSL=false&serverTimezone=UTC,为什么还报错?
真相往往藏在细节里。最常见的三个原因:
1. YAML缩进错误:YAML对空格极其敏感。spring:下面必须是两个空格,datasource:下面必须是四个空格,url:下面必须是六个空格。如果某一行多了一个空格或少了一个空格,整个spring.datasource块就会被YAML解析器忽略,相当于没配置。解决方案:用IDEA打开application.yml,按Ctrl+Alt+L(Windows)或Cmd+Option+L(Mac)自动格式化,让缩进标准化。
2. MySQL驱动缺失:pom.xml里mysql-connector-java的版本是否匹配你的MySQL?MySQL 8.0必须用8.0.16或更高版本驱动,而5.7用5.1.47更稳。检查pom.xml,确认<artifactId>mysql-connector-java</artifactId>的<version>是否正确。
3. 数据库名不存在:url里的your_db是你本地真实存在的数据库名吗?执行SHOW DATABASES;确认。如果不存在,用CREATE DATABASE your_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;创建它。
提示:在
application.yml里,加上spring.datasource.hikari.initialization-fail-timeout=0,可以让HikariCP在连接失败时不阻塞启动,而是继续运行,方便你通过日志定位问题。
5.2 Swagger UI打不开:404或白屏的终极排查清单
访问http://localhost:8080/swagger-ui.html返回404,或者页面空白,是第二大高频问题。按顺序排查:
- 检查路径是否正确:SpringBoot 2.1.3 + Swagger 2.9.2的默认路径是/swagger-ui.html,不是/swagger-ui/或/doc.html。确认URL拼写无误。
- 确认Swagger配置类生效:检查SwaggerConfig.java上是否有@Configuration和@EnableSwagger2注解。缺少任何一个,配置都不会加载。
- 检查Controller包路径:@ComponentScan或@SpringBootApplication的扫描路径,是否包含了SwaggerConfig所在的包?如果SwaggerConfig在com.example.config,而启动类在com.example,那就没问题;但如果启动类在com.example.demo,而config包是com.example.config,就需要在启动类上加@ComponentScan(basePackages = {"com.example.config", "com.example.demo"})。
- 检查静态资源路径:pom.xml里是否误加了spring-boot-starter-thymeleaf?Thymeleaf会劫持/路径,导致Swagger的静态资源(JS/CSS)无法加载。解决方案:移除Thymeleaf依赖,或在application.yml里加spring.thymeleaf.enabled=false。
注意:如果页面能打开但显示“Unable to infer base url”,通常是Swagger配置里的
Docket没有正确设置host。在SwaggerConfig.java的apiInfo()方法里,加上.contact(new Contact("Support", "http://example.com", "support@example.com")),并确保Docket构建时没有调用.host("localhost:8080")这种硬编码(它会破坏部署到Nginx后的反向代理)。
5.3 Redis连接失败:Cannot connect to redis 的七种可能
Caused by: io.lettuce.core.RedisConnectionException: Unable to connect to localhost:6379,这个异常背后,可能是七种不同的病:
1. Redis服务根本没启动:最傻也最常见。执行redis-cli ping,如果返回Could not connect to Redis at 127.0.0.1:6379: Connection refused,那就先redis-server启动它。
2. 端口被占用:netstat -ano | findstr :6379(Windows)或lsof -i :6379(Mac/Linux),看是谁占了6379。如果是其他Redis实例,改application.yml里的port;如果是无关进程,kill -9 PID干掉它。
3. 防火墙拦截:公司电脑的防火墙可能阻止了6379端口。临时关闭防火墙测试,或添加入站规则。
4. Redis配置了密码:redis.conf里如果设置了requirepass your_password,那么application.yml里必须加spring.redis.password=your_password。
5. Redis绑定了127.0.0.1:redis.conf里bind 127.0.0.1是默认的,这没问题;但如果改成bind 192.168.1.100,而你的应用连的是localhost,就会失败。保持bind 127.0.0.1或注释掉它。
6. Docker网络问题:如果你用Docker运行Redis,application.yml里的host不能写localhost,而要写host.docker.internal(Mac/Windows)或宿主机IP(Linux)。
7. Letture客户端版本冲突:pom.xml里如果同时引入了spring-boot-starter-data-redis和io.lettuce:lettuce-core两个不同版本,会导致类加载冲突。只保留前者即可。
5.4 MapStruct转换失败:UserConverter.INSTANCE为null的元凶
编译不报错,但运行时报NullPointerException,UserConverter.INSTANCE是null。这几乎100%是Maven配置问题。终极解决方案:
- 删除项目根目录下的target文件夹;
- 删除IDEA的.idea文件夹和*.iml文件;
- 重启IDEA;
- 重新File -> Open项目;
- 在Maven工具窗口,右键项目 -> “Reload project”;
- 然后Build -> Build Project。
如果还不行,打开target/generated-sources/annotations/目录,看里面是否有UserConverterImpl.java文件。如果没有,说明MapStruct处理器根本没运行,回到pom.xml,逐字核对<annotationProcessorPaths>的配置,一个字母都不能错。
5.5 密码校验总是失败:BCrypt的“盐值”迷雾
注册时密码存进去了,但登录时passwordEncoder.matches()总是返回false。这通常是因为:
- 密码字段名不一致:DTO里是password,Entity里是encryptedPassword,但你在UserConverter里没写@Mapping(target = "encryptedPassword", source = "password"),导致encryptedPassword字段一直是null;
- 数据库字段类型太短:encryptedPassword字段设成了VARCHAR(64),而BCrypt生成的字符串长度是60+,但某些情况下会超过64。spring-cloud.sql里设为100,就是为了杜绝此问题;
- 编码问题:前端传来的密码字符串,如果包含中文或特殊符号,在传输过程中被UTF-8以外的编码解析,会导致哈希结果不一致。确保前端HTTP请求头里有Content-Type: application/json;charset=UTF-8。
实操心得:在
LoginService.login()方法里,加一行日志:log.info("DB encryptedPassword: {}, Input rawPassword: {}", user.getEncryptedPassword(), dto.getPassword());。这样,当校验失败时,你能一眼看到数据库里存的密文和用户输入的明文,对比两者,问题立现。
6. 项目扩展与二次开发指南:让它真正长在你的项目里
这个包的价值,不在于它“能做什么”,而在于它“能很容易地变成你想要的样子”。以下是几个最实用的扩展方向,每一步都附带可直接复制的代码片段。
6.1 集成邮件服务:让验证码真正“飞”出去
当前验证码只是打印在控制台,上线必须对接邮件。以腾讯企业邮箱为例,只需三步:
1. 在pom.xml里添加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
- 在
application.yml里添加邮件配置:
spring:
mail:
host: smtp.exmail.qq.com
port: 465
username: your_email@your-domain.com
password: your_app_password # 注意:不是邮箱密码,是QQ邮箱的“SMTP授权码”
properties:
mail:
smtp:
auth: true
starttls:
enable: true
required: true
ssl:
enable: true
- 创建
EmailService.java:
@Service
public class EmailService {
@Autowired
private JavaMailSender javaMailSender;
public void sendCaptcha(String toEmail, String captcha) {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom("your_email@your-domain.com");
message.setTo(toEmail);
message.setSubject("您的验证码");
message.setText("您的验证码是:" + captcha + ",5分钟内有效。");
javaMailSender.send(message);
}
}
然后,在LoginService.login()方法里,把System.out.println()替换为emailService.sendCaptcha(dto.getEmail(), captcha)。就这么简单,验证码就从控制台飞到了用户邮箱。
6.2 添加JWT Token:从Redis会话升级为无状态认证
Redis会话需要服务端存储状态,而JWT是无状态的,更适合分布式部署。替换步骤:
1. 添加JWT依赖:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.2</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
- 创建JWT工具类
JwtUtil.java,生成和解析Token; - 修改
LoginService.login(),不再往Redis写login_token,而是调用JwtUtil.generateToken(userId)生成JWT字符串,返回给前端; - 创建
JwtAuthenticationFilter,继承OncePerRequestFilter,在每次请求时从AuthorizationHeader里提取JWT,解析出用户ID,并存入SecurityContextHolder。
整个过程,你只需要新增3个类,修改1个Service方法,原有的Controller、DTO、Entity一行代码都不用动。这就是良好架构的魅力——变化被隔离在最小范围内。
6.3 接入MyBatis-Plus:告别XML,拥抱代码生成器
如果你厌倦了写XML,MyBatis-Plus是绝佳替代。步骤:
1. 替换pom.xml里的mybatis-spring-boot-starter为mybatis-plus-boot-starter;
2. 删除所有*Mapper.xml文件;
3. 让User实体类继承Model<User>;
4. UserMapper接口继承BaseMapper<User>;
5. 使用MyBatis-Plus的代码生成器,根据数据库表,一键生成Entity、Mapper、Service、Controller全套代码。
你会发现,原来需要100行XML和50行Java代码完成的CRUD,现在一行userMapper.selectById(1)就搞定。而且,MyBatis-Plus的LambdaQueryWrapper,让你写eq(User::getEmail, "xxx")这种类型安全的查询,再也不用担心字段名写错。
6.4 前端联调:给Vue/React项目一个“开箱即用”的API Base URL
很多前端同学拿到后端地址,第一反应是“怎么配跨域?”。这个包已经内置了CORS支持。在SpringBootDemoApplication.java同级目录,创建WebMvcConfig.java:
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*") // 生产环境请替换为具体域名,如 "http://localhost:8081"
.allowCredentials(true)
.maxAge(3600);
}
}
然后,告诉前端同学:把你们项目的API Base URL设为http://localhost:8080/api,所有接口都能直接调,无需任何代理配置。他们只需要在axios.defaults.baseURL = 'http://localhost:8080/api';,剩下的,交给这个包。
我个人在实际操作中的体会是:一个优秀的后端模板,它的价值不在于有多炫酷,而在于能让下一个接手的人,花最少的时间,理解最多的设计意图,并在此基础上,自信地、快速地,把它变成自己项目的一部分。这个SpringBoot用户系统实战包,就是为此而生。它不承诺解决你所有问题,但它承诺,当你面对“用户登录注册”这个永恒命题时,你不必再从零开始造轮子,而是可以站在一个坚实、可靠、经过验证的肩膀上,去解决真正属于你业务的独特挑战。
简介:开箱即用的SpringBoot用户管理基础工程,基于2.1.3版本构建,支持标准登录注册流程。后端用MyBatis完成MySQL数据操作(附带spring-cloud.sql初始化脚本),JPA仅保留最小化示例;通过MapStruct实现User实体与DTO之间的自动映射,减少手动赋值代码;集成Redis用于存储验证码、登录态等轻量级缓存场景;所有HTTP接口均通过Swagger UI自动生成可交互文档,方便前后端联调和测试。项目采用标准Maven结构,含src/main/java、src/main/resources等规范目录,兼容IDEA开发环境,支持mvnw命令一键编译运行,适合新手学习SpringBoot整合主流组件的典型实践路径,也适合作为中小项目用户模块的快速启动模板。
更多推荐




所有评论(0)