一、在数据库中建库建表
以下为 sql 语句
use bite_excel;
drop table if exists `users`;
CREATE TABLE users
(
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '用户ID, 主键, 自增长, 从10000001开始',
username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名, 唯一标识, 用于登录和显示',
email VARCHAR(100) UNIQUE COMMENT '邮箱地址, 唯一, 用于登录和通知',
password_hash VARCHAR(255) COMMENT '密码哈希值, 使用BCrypt加密存储',
INDEX idx_email (email),
INDEX idx_username (username)
) COMMENT '用户表' CHARSET = utf8mb4 AUTO_INCREMENT = 10000001;
二、发送邮箱验证码功能开发
需要完成的功能:1、生成6位数字的字符串的验证码 2、封装发送邮箱服务 3、服务端存储验证码结构
1、接口定义

2、定义请求类对象和响应类对象
java
package com.linzhixin.user.dto.request;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 发送验证码的请求参数
*/
@Data
public class SendCodeRequest {
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式错误") //用于验证字段是否符合邮箱格式。
private String email;
}
java
package com.linzhixin.user.dto.response;
import lombok.Builder;
import lombok.Data;
/**
* 查询用户信息
*/
@Data
@Builder
public class UserInfoResponse {
/**
* 用户 ID
*/
private Long userId;
/**
* 用户名
*/
private String username;
/**
* 邮箱
*/
private String email;
}
3、创建 UserController
java
/**
* 用户服务的控制器
*/
@RestController
@RequestMapping("/users")
public class UserController {
/**
* 发送邮箱验证码
* @param request
* @return
*/
public Result<SendCodeResponse> sendVerificationCode(@RequestBody @Valid SendCodeRequest request) {
}
4、定义验证码服务相关接口
java
package com.linzhixin.user.service;
/**
* 验证码相关服务接口
*/
public interface VerificationCodeService {
/**
* 发送验证码
* @param email 邮箱
* @return 过期时长(秒)
*/
int sendCode(String email);
5、定义验证码校验服务实现类
java
/**
* 验证码校验服务实现类
*/
@Service
@Slf4j
public class VerificationCodeServiceImpl implements VerificationCodeService {
@Override
public int sendCode(String email) {
// 1、生成验证码
String code = String.valueOf((int)(Math.random()*900000) + 100000);
return 0;
}
6、在 common-service 中封装发送QQ邮箱发送邮件的功能
定义邮件发送的接口
java
package com.linzhixin.common.service;
/**
* 邮件发送服务的接口
*/
public interface EmailService {
/**
* 发送邮件验证码
* @param to 收件人的邮箱
* @param code 验证码
* @return 是否发送成功
*/
boolean sendVerificationCode(String to, String code);
}
定义邮件发送的实现类
java
/**
* 发送邮箱的实现类
*/
@Service
@Slf4j
//只有当配置文件中存在 spring.mail.username 这个属性时,才会加载被注解的类或方法。
@ConditionalOnProperty(name = "spring.mail.username")
public class EmailServiceImpl implements EmailService {
/**
* spring 邮件发送器
*/
@Autowired
private JavaMailSender mailSender;
/**
* 发送方邮箱的地址
*/
@Value("${spring.mail.username}")
private String fromEmail;
/**
* 构建发送验证码的正文
* @param code 验证码
* @return 正文
*/
private String getContent(String code) {
return String.format("您好!" +
"您的验证码是: %s\n\n" +
"验证码的有效期是5分钟,请及时使用",
code
);
}
@Override
public boolean sendVerificationCode(String to, String code) {
// 发送邮箱的逻辑
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(fromEmail);
message.setTo(to);
String subject = "系统验证码";
String content = getContent(code);
message.setSubject(subject);
message.setText(content);
mailSender.send(message);
log.info("[发送邮箱] 验证码 {} 已发送至 []", code, to);
return true;
}
}
7、完善 验证码校验服务实现类 sendCode 方法
java
/**
* 验证码校验服务实现类
*/
@Service
@Slf4j
public class VerificationCodeServiceImpl implements VerificationCodeService {
@Autowired
private EmailService emailService;
@Autowired
private UserMapper userMapper;
@Autowired
private RedisService redisService;
@Override
public int sendCode(String email) {
// 1、生成验证码
String code = String.valueOf((int)(Math.random()*900000) + 100000);
// 2、发送验证码
boolean ifSendSuccess = emailService.sendVerificationCode(email, code);
// 3、查询数据库的判断逻辑 (根据邮箱去查询users表,存在即为登录,不存在即为注册)
UserEntity user = userMapper.findByLoginKey(email);
// 新注册
if(user == null) {
user = new UserEntity();
user.setId(0L);
user.setUserName(email);
}
// 验证码信息写到 redis 缓存
redisService.storeVerificationCode(code, user.getId(), user.getUserName());
if (ifSendSuccess) {
log.info("[验证码发送成功], 验证码{} 已经发送至{}", code, email);
return 300;
} else {
log.error("[验证码发送失败], 验证码{} 没有发送至{}", code, email);
return -1;
}
}
}
8、实现 userMapper.findByLoginKey,实现查询数据库的判断逻辑 (根据邮箱去查询users表,存在即为登录,不存在即为注册)
java
/**
* 用户表
*/
@Mapper
public interface UserMapper extends BaseMapper<UserEntity> {
/**
* 根据邮箱或者用户名二选一去查询用户
* @param key 邮箱或者用户名
* @return 用户
*/
@Select("select id, username, email, password_hash as passwordHash from users where username=#{key} or email=#{key} limit 1")
UserEntity findByLoginKey(@Param("key") String key);
}
9、在 common-service 中创建 redisService接口 与 redisServiceImpl 实现类,实现 storeVerificationCode 方法,把 验证码信息写到 redis 缓存
java
/**
* 缓存服务的实现类
*/
@Service
@Slf4j
public class RedisServiceImpl implements RedisService {
// StringRedisTemplate: Spring 提供的、专门操作 Redis 字符串数据的工具类
@Autowired
private StringRedisTemplate stringRedisTemplate;
private final ObjectMapper objectMapper = new ObjectMapper();
/**
* 验证码令牌前缀
*/
private static final String CODE_PREFIX = "code";
@Override
public void storeVerificationCode(String code, Long userId, String userName) {
// 1、先生成 key
String key = CODE_PREFIX + code;
CodeInfo codeInfo = new CodeInfo(userId, userName);
// 2、对象序列化之后,写入redis
try {
String value = objectMapper.writeValueAsString(codeInfo);
stringRedisTemplate.opsForValue().set(key, value, 300, TimeUnit.SECONDS);
log.info("[Redis存储成功] 验证码 {} 用户名 {}", code, userName);
} catch (JsonProcessingException e) {
log.error("[Redis存储失败] 验证码 {} 用户名 {}", code, userName);
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
private static class CodeInfo {
/**
* 用户ID
*/
private Long userId;
/**
* 用户名:邮箱或者用户名
*/
private String userName;
}
}
10、完善 UserController 发送验证码方法 + 编写启动类
java
**
* 用户服务的控制器
*/
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private VerificationCodeService verificationCodeService;
@Autowired
private UserService userService;
/**
* 发送邮箱验证码
* @param request
* @return
*/
@PostMapping("/verification-code")
@LogOperation("发送验证码")
public Result<SendCodeResponse> sendVerificationCode(@RequestBody @Valid SendCodeRequest request) {
int expireSeconds = verificationCodeService.sendCode(request.getEmail());
String sendTo = maskEmail(request.getEmail());
SendCodeResponse response = SendCodeResponse.builder()
.expireTime(expireSeconds)
.sendTo(sendTo)
.build();
return Result.success("验证码发送成功", response);
}
/**
* 邮箱脱敏
* @param email 邮箱
* @return 脱敏后的邮箱
*/
private String maskEmail(@NotBlank(message = "邮箱不能为空") @Email(message = "邮箱格式错误") String email) {
String[] parts = email.split("@", 2);
String name = parts[0];
String domain = parts[1];
if(name.length() <= 2) {
return name.charAt(0) + "***@" + domain;
}
return name.substring(0, 2) + "***@" + domain;
}
}
java
@SpringBootApplication
@MapperScan("com.linzhixin.user.mapper")
@ComponentScan(basePackages = {
"com.linzhixin.user",
"com.linzhixin.common"
})
@Slf4j
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
log.info("Port: 9001");
11、调用接口测试发送验证码功能
http://localhost:8080/api/v1/users/verification-code
三、注册+登录功能实现
1、接口定义

2、实现校验验证码功能
定义DTO(AuthRequest+AuthResponse)
java
package com.linzhixin.user.dto.request;
import lombok.Data;
/**
* 统一认证请求参数(注册+登录)
*/
@Data
public class AuthRequest {
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 邮箱
*/
private String email;
/**
* 验证码
*/
private String verificationCode;
}
java
package com.linzhixin.user.dto.response;
import lombok.Builder;
import lombok.Data;
/**
* 统一认证响应参数(注册+登录)
*/
@Data
@Builder
public class AuthResponse {
/**
* 用户ID
*/
private Long userId;
/**
* 用户名
*/
private String userName;
/**
* 邮箱
*/
private String email;
/**
* 用户访问令牌
*/
private String token;
/**
* 令牌的过期时间
*/
private String tokenExpireTime;
/**
* 是否为新用户
*/
private Boolean isNewUser;
}
服务层代码编写
实现逻辑

完善 VerificationCodeService 与 VerificationCodeServiceImpl
VerificationCodeServiceImpl类
java
@Override
public boolean verifyCode(String email, String code) {
return redisService.isCodeValid(code);
}
RedisServiceImpl 类
java
@Override
public boolean isCodeValid(String code) {
// 1、先生成 key
String key = CODE_PREFIX + code;
return stringRedisTemplate.hasKey(key);
}
3、实现验证码注册或登录功能
定义接口与实现类
java
/**
* 用户服务相关接口
*/
public interface UserService {
AuthResponse auth(AuthRequest authRequest);
}
java
/**
* 用户服务实现类
*/
@Service
@Slf4j
public class UserServiceImpl implements UserService {
@Autowired
private VerificationCodeService verificationCodeService;
@Autowired
private UserMapper userMapper;
@Override
public AuthResponse auth(AuthRequest authRequest) {
// 1、先验证当前是哪种方式(邮箱+验证码/账号+密码)
boolean isEmailCodeMode = StringUtils.isNotBlank(authRequest.getEmail()) && StringUtils.isNotBlank(authRequest.getVerificationCode());
boolean isPasswordMode = StringUtils.isNotBlank(authRequest.getUsername()) && StringUtils.isNotBlank(authRequest.getPassword());
if(!isEmailCodeMode && !isPasswordMode) {
throw new IllegalArgumentException("请提供有效的验证方式:邮箱+验证码/账号+密码");
}
UserEntity user = null;
Boolean isNewUser = false;
// 2、先处理邮箱+验证码的方式
if(isEmailCodeMode) {
if(!verificationCodeService.verifyCode(authRequest.getEmail(), authRequest.getVerificationCode())) {
throw new IllegalArgumentException("验证码无效或者过期");
}
// 3、注册还是登录
Long userId = verificationCodeService.getUserId(authRequest.getVerificationCode());
// 4、新用户来注册
if(userId == 0L) {
user = new UserEntity();
user.setEmail(authRequest.getEmail());
user.setUserName(createRandomName());
userMapper.insert(user);
isNewUser = true;
}
// 老用户登录,直接查询数据库完善user信息
user = userMapper.selectById(userId);
// 5、删除验证码,避免重复使用
verificationCodeService.remove(authRequest.getVerificationCode());
}
}
/**
* 用来生成随机用户名
* @return
*/
private String createRandomName() {
String name = "hello_" + String.valueOf((int)(Math.random() * 900000000) + 100000000);
// 1、用户名需要全局唯一
if(userMapper.existByUserName(name) == 0) {
return name;
}
return "hello_" + System.currentTimeMillis();
}
}
VerificationCodeServiceImpl 类
java
@Override
public Long getUserId(String code) {
return redisService.getUserIdByCode(code);
}
@Override
public void remove(String code) {
redisService.remove(code);
}
RedisServiceImpl 类
java
@Override
public Long getUserIdByCode(String code) {
// 1、先生成key
String key = CODE_PREFIX + code;
String value = stringRedisTemplate.opsForValue().get(key);
if(value != null) {
try {
CodeInfo codeInfo = objectMapper.readValue(value, CodeInfo.class);
return codeInfo.userId;
} catch (JsonProcessingException e) {
log.error("[Redis读取失败] 验证码 {} 报错 {}", code, e.getMessage());
}
}
return null;
}
@Override
public void remove(String code) {
String key = CODE_PREFIX + code;
stringRedisTemplate.delete(key);
log.info("验证码已经删除{}", code);
}
后续 处理 token 方式等 账号密码注册或登录功能 实现后一起处理
4、实现账号密码注册或登录功能
完善 UserServiceImpl 类 auth 方法
java
if(isPasswordMode) {
user = userMapper.findByLoginKey(authRequest.getUsername());
if(user == null) {
// 新用户,需要先注册
user = new UserEntity();
user.setUserName(authRequest.getUsername());
user.setPasswordHash(passwordEncoder.encode(authRequest.getPassword()));
userMapper.insert(user);
isNewUser = true;
else {
// 老用户,校验密码
if(!passwordEncoder.matches(authRequest.getPassword(), user.getPasswordHash())) {
throw new IllegalArgumentException("用户名与密码不匹配");
}
}
}
UserMapper.findByLoginKey()
java
/**
* 根据邮箱或者用户名二选一去查询用户
* @param key 邮箱或者用户名
* @return 用户
*/
@Select("select id, username, email, password_hash as passwordHash from users where username=#{key} or email=#{key} limit 1")
UserEntity findByLoginKey(@Param("key") String key);
5、获取 token 作为返回值
完善 UserServiceImpl 类 auth 方法
java
// 处理 token
String token;
// 重复登录直接获取
if(jwtUtil.isUserLogged(user.getId())) {
token = jwtUtil.getUserActiveToken(user.getId());
} else {
token = jwtUtil.createToken(user.getId(), user.getUserName());
}
LocalDateTime time = LocalDateTime.now();
LocalDateTime expireTime = time.plusHours(20);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM-dd HH:mm:ss");
String tokenExpireTime = expireTime.format(formatter);
return AuthResponse.builder()
.userId(user.getId())
.userName(user.getUserName())
.email(user.getEmail())
.token(token)
.tokenExpireTime(tokenExpireTime)
.isNewUser(isNewUser)
.build();
在 common-service 中创建 JwtUtil 并完善
java
package com.linzhixin.common.util;
import com.linzhixin.common.service.RedisService;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
/**
* JWT 工具类:复杂生产、校验、管理用户登录令牌
*/
@Component
public class JwtUtil {
@Autowired
private RedisService redisService;
@Value("72000")
private Long expireTime;
/**
* 签名密钥对象
*/
private SecretKey key;
/**
* H256密钥
*/
@Value("changeit-change-it-change-it-change-it-change-it")
private String secret;
/**
* 初始化密钥对象
*/
//@PostConstruct:
//在 Spring 容器完成 Bean 的依赖注入后,自动执行这个方法
//确保在业务方法使用 key 之前,它已经被初始化好
@PostConstruct
private void init() {
//// 1. 将字符串密钥转为字节数组(UTF-8 编码)
byte[] keyBytes = secret.getBytes(StandardCharsets.UTF_8);
// 2. 使用 HMAC-SHA 算法生成 Key 对象
this.key = Keys.hmacShaKeyFor(keyBytes);
}
/**
* 生成 JWT 令牌并写入缓存
* @param userId 用户ID
* @param username 用户名或邮箱
* @return 令牌
*/
public String createToken(Long userId, String username) {
Date now = new Date();
Date expiry = new Date(now.getTime() + expireTime);
String token = Jwts.builder()
.setSubject(String.valueOf(userId))
.claim("username", username)
.setIssuedAt(now)
.setExpiration(expiry)
.signWith(key, SignatureAlgorithm.HS256)
.compact();
// token 存到 redis
redisService.storeToken(token, userId, username, expireTime);
// 存储活跃用户(避免重复登陆)
redisService.storeUserActiveToken(userId, token, expireTime);
return token;
}
/**
* 获取当前用户的token
* @param userId 用户ID
* @return 用户token
*/
public String getUserActiveToken(Long userId) {
return redisService.getUserToken(userId);
}
}
redisServiceImpl.storeToken() 、 redisServiceImpl.storeUserActiveToken、
redisServiceImpl.getUserToken()
java
/**
* 个人令牌前缀
*/
private static final String TOKEN_PREFIX = "token:";
@Override
public void storeToken(String token, Long userId, String username, Long expireSeconds) {
String key = TOKEN_PREFIX + token;
TokenInfo tokenInfo = new TokenInfo(userId, username);
try {
String value = objectMapper.writeValueAsString(tokenInfo);
stringRedisTemplate.opsForValue().set(key, value, expireSeconds, TimeUnit.SECONDS);
} catch (JsonProcessingException e) {
log.error("[Redis令牌存储失败 {} {}]", token, e.getMessage());
}
}
/**
* 检查用户是否已经登录
* @param userId 用户ID
* @return 是否登录
*/
public boolean isUserLogged(Long userId) {
return redisService.isUserLogged(userId);
}
@Override
public void storeUserActiveToken(Long userId, String token, Long expireSeconds) {
String key = USER_SESSION_PREFIX + userId;
stringRedisTemplate.opsForValue().set(key, token, expireSeconds, TimeUnit.SECONDS);
}
@Override
public String getUserToken(Long userId) {
String key = USER_SESSION_PREFIX + userId;
return stringRedisTemplate.opsForValue().get(key);
}
四、获取用户信息功能实现
1、接口定义

2、UserController
java
/**
* 用户信息查询
*/
@GetMapping("/info")
@LogOperation("用户信息查询")
public Result<UserInfoResponse> getUserInfo(@RequestHeader(value = "Authorization", required = true) String authorization) {
return Result.success("success", userService.getUserInfo(authorization));
}
3、定义 响应类 UserInfoResponse
java
package com.linzhixin.user.dto.response;
import lombok.Builder;
import lombok.Data;
/**
* 查询用户信息
*/
@Data
@Builder
public class UserInfoResponse {
/**
* 用户 ID
*/
private Long userId;
/**
* 用户名
*/
private String username;
/**
* 邮箱
*/
private String email;
}
4、UserServiceImpl
java
@Override
public UserInfoResponse getUserInfo(String authorization) {
// 1、去 JWT 工具类里面查询用户ID
Long userId = jwtUtil.getUserIdByAuthorization(authorization);
if(userId == null) {
throw new IllegalArgumentException("无效的令牌");
}
UserEntity user = userMapper.selectById(userId);
if(user == null) {
throw new IllegalArgumentException("用户不存在");
}
return UserInfoResponse.builder()
.userId(user.getId())
.username(user.getUserName())
.email(user.getEmail())
.build();
}
5、JwtUtil
java
/**
* 获取用户ID
* @param authorization 前端传递的令牌
* @return 用户ID
*/
public Long getUserIdByAuthorization(String authorization) {
// 1、从 redis 中去获取
return redisService.getUserIdByAuthorization(authorization);
}
五、修改密码功能实现
1、接口定义

2、UserController
java
/**
* 修改密码
*/
@PostMapping("/change-password")
@LogOperation("修改密码")
public Result<ChangePasswordResponse> changePassword(
@RequestHeader(value = "Authorization", required = true) String authorization,
@RequestBody @Valid ChangePasswordRequest request) {
return Result.success("success", userService.changePassword(request, authorization));
}
3、定义请求类ChangePasswordRequest和响应类ChangePasswordResponse
java
/**
* 修改密码的请求参数
*/
@Data
@Builder
public class ChangePasswordRequest {
/**
* 当前的密码
*/
@NotBlank(message = "当前密码必填")
private String currentPassword;
/**
* 新密码
*/
@NotBlank(message = "新密码不能为空")
@Size(min = 6, max = 64, message = "新密码长度介于6到64个字符之间")
private String newPassword;
/**
* 确认密码必填
*/
@NotBlank(message = "确认密码不能为空")
private String confirmPassword;
}
java
/**
* 查询用户信息
*/
@Data
@Builder
public class UserInfoResponse {
/**
* 用户 ID
*/
private Long userId;
/**
* 用户名
*/
private String username;
/**
* 邮箱
*/
private String email;
}
4、UserService 类
java
@Override
@Transactional(rollbackFor = Exception.class)
public ChangePasswordResponse changePassword(ChangePasswordRequest request, String token) {
// 1、新密码和确认密码必须一致
if(!request.getNewPassword().equals(request.getConfirmPassword())) {
throw new IllegalArgumentException("请保证新旧密码一致");
}
// 2、新密码与老密码不能相同
if(request.getCurrentPassword().equals(request.getNewPassword())) {
throw new IllegalArgumentException("新旧密码不能相同");
}
// 3、根据 token 获取用户信息
Long userId = jwtUtil.getUserIdByAuthorization(token);
if(userId == null) {
throw new IllegalArgumentException("令牌无效");
}
// 4、获取到用户信息之后进行修改操作
UserEntity user = userMapper.selectById(userId);
user.setPasswordHash(passwordEncoder.encode(request.getNewPassword()));
userMapper.updateById(user);
// 5、封装响应
return ChangePasswordResponse.builder()
.userId(userId)
.username(user.getUserName())
.build();
}
5、JwtUtil
java
/**
* 获取用户ID
* @param authorization 前端传递的令牌
* @return 用户ID
*/
public Long getUserIdByAuthorization(String authorization) {
// 1、从 redis 中去获取
return redisService.getUserIdByAuthorization(authorization);
}
6、RedisServiceImpl
java
@Override
public String getUserToken(Long userId) {
String key = USER_SESSION_PREFIX + userId;
return stringRedisTemplate.opsForValue().get(key);
}
六、用户登出
1、接口定义

2、UserController
java
/**
* 用户退出系统
*/
@PostMapping("/logout")
@LogOperation("用户退出")
public Result<Void> logout(@RequestHeader(value = "Authorization", required = true) String authorization) {
userService.logout(authorization);
return Result.success("登出成功", null);
}
3、UserServiceImpl
java
@Override
public void logout(String authorization) {
jwtUtil.removeAuthorization(authorization);
}
4、JwtUtil
java
/**
* 删除令牌
* @param authorization 用户令牌
*/
public void removeAuthorization(String authorization) {
redisService.removeAuthorization(authorization);
}
5、RedisServiceImpl
java
@Override
public void removeAuthorization(String authorization) {
// 1、获取原生的 token
String token = authorization.replaceFirst("(?i)^Bearer ", "").trim();
// 2、删除用户令牌缓存
String tokenKey = TOKEN_PREFIX + token;
Long userId = getUserIdByAuthorization(token);
stringRedisTemplate.delete(tokenKey);
// 3、删除活跃用户缓存令牌
String userKey = USER_SESSION_PREFIX + userId;
stringRedisTemplate.delete(userKey);
}