Springboot3x 脚手架开发-整合登录注册
先理清要实现的功能
登录:
用户、密码、验证码
注册
用户、密码、邮箱、邮箱验证码
因此就需要知道 如何引入图形验证码、短信验证码 (还有模板优化)
图形验证码接口
接口介绍:/captchCode
当用户要登录的时候(即访问 /login时),自动调用该接口。返回验证码ID和验证码图片的base64编码。
前端保存该id,显示图片,把用户输入的验证码和验证码id一并提交到表单,在后端进行校验(校验验证码和账密码)。
先定义验证码接口数据对象
1
2
3
4
5
6
7
8
|
@Data
@Builder
public class CaptchaVO {
//验证码id
private String captchaId;
//验证码图片base64编码
private String captchaImage;
}
|
实现接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
@RestController
@RequestMapping("/captcha")
@Tag(name = "验证码接口" ,description = "验证码接口相关操作")
public class CaptchaController {
// 导入 redis
private final StringRedisTemplate stringRedisTemplate;
public CaptchaController(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@GetMapping
@Operation(summary = "获取验证码")
public CaptchaVO getCaptcha(String captchaId)
{
// 利用 hutoll 的工具获取验证码, 设置高度宽度(应该与前端一致) 位数 和干扰线
CircleCaptcha circleCaptcha = CaptchaUtil.createCircleCaptcha(130,50,4,10);
// 获取文本
String code = circleCaptcha.getCode();
// 获取图片base64
String imageBase64Data = circleCaptcha.getImageBase64Data();
// 如果没有传入id 则生成一个随机字符串作为id uuid
captchaId = Optional.ofNullable(captchaId).orElseGet(() -> UUID.randomUUID().toString());
// 保存到 redis中 并设置有效期为 30分钟
stringRedisTemplate.opsForValue().set("captcha:"+captchaId,imageBase64Data,30, TimeUnit.MINUTES);
// 返回对象给前端
return CaptchaVO.builder()
.captchaId(captchaId)
.captchaImage(imageBase64Data)
.build();
}
}
|
邮件发送验证码接口
接口介绍:接收前端的邮箱,对指定邮箱发送验证码。
准备:
需要准备一个邮箱开启 POP3/SMTP服务,用于发送邮件。(qq邮箱)
image-20241203170124960
开启后有个授权码(需要记住)
导入依赖:
1
2
3
4
5
6
7
8
|
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
|
application.yml 配置相关信息
1
2
3
4
5
6
7
8
9
10
11
12
|
mail:
host: smtp.qq.com
protocol: smtp
username: 1320104286@qq.com
password: dbamnosrjtwaibif
default-encoding: utf-8
properties:
mail.smtp.auth: true
mail.smtp.starttls.enable: true
main:
allow-bean-definition-overriding: true
allow-circular-references: true
|
创建接收邮箱的数据对象
1
2
3
4
5
|
@Data
public class EmailDto {
@NotBlank
private String email;
}
|
一个获取验证码的RandomUtil
1
2
3
4
5
6
7
8
9
10
11
12
|
public class RandomUtil {
public static String getSixRandomCode()
{
// 获取6位随机数字作为验证码
Random random = new Random();
StringBuilder code = new StringBuilder();
for (int i = 0; i < 6; i++) {
code.append(random.nextInt(10));
}
return code.toString();
}
}
|
编写邮件发送模板:在网上找到心仪的即可,也可以自己写。(存放在resources下的template)
编写邮件发送服务:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
@Service
@Slf4j
public class EmailServiceImpl implements EmailService {
private final StringRedisTemplate stringRedisTemplate;
@Autowired
private JavaMailSenderImpl mailSender; // 自动装配邮件发送器
@Autowired
private TemplateEngine templateEngine; // 自动装配模板引擎
// 构造函数注入StringRedisTemplate
public EmailServiceImpl(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
/**
* 发送带有验证码的邮件
* @param emailDto 包含收件人邮箱和可能的其他邮件信息的DTO对象
*/
@Override
public void sendEmail(EmailDto emailDto) {
String email = emailDto.getEmail(); // 获取收件人邮箱
String code = RandomUtil.getSixRandomCode(); // 生成六位随机验证码
String subject = "模版验证码"; // 邮件主题
// 将生成的验证码存储到redis中,有效期为15分钟
stringRedisTemplate.opsForValue().set("verifycode:"+email,code,5, TimeUnit.MINUTES);
// 将内容设置为模板
Context context = new Context();
// 将验证码分割成一个个字符
context.setVariable("verifyCode", Arrays.asList(code.split("")));
// 加载模板并处理, 改文件名不需要写全类名。
String process = templateEngine.process("EmailVerificationCode.html",context);
MimeMessage mimeMessage = mailSender.createMimeMessage();
try{
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage);
helper.setFrom("1320104286@qq.com"); // 设置发件人邮箱
helper.setTo(email); // 设置收件人邮箱
helper.setSubject(subject); // 设置邮件主题
helper.setSentDate(new Date()); // 设置发送日期
helper.setText(process,true); // 设置邮件内容为HTML格式
// true 表示加载为html
} catch (Exception e) {
throw new RuntimeException(e); // 发生异常时抛出运行时异常
}
mailSender.send(mimeMessage); // 发送邮件
log.info("发送成功!"); // 记录日志,表示邮件发送成功
}
}
|
到此邮件发送的服务就已经实现了。
登录接口
只需要账号、密码、图形验证码即可成功登录。
在数据库中查用户,设置jwt,存放到redis中,并把用户信息存放到本地线程
准备工作:
设计数据库表(按你自己的需求来)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
create database template;
create table user
(
id bigint auto_increment comment '主键' primary key,
user_name varchar(32) not null comment '用户昵称',
password varchar(256) not null comment '密码',
user_role varchar(256) default 'user' null comment '用户角色:user / admin',
avatar varchar(1024) null comment '头像',
email varchar(32) not null comment '邮箱',
phone varchar(15) null comment '电话',
create_time datetime null comment '创建时间',
update_time datetime null comment '更新时间',
is_delete tinyint(1) default 0 null comment '逻辑删除:1删除/0存在',
gender tinyint(1) null comment '性别',
status tinyint(1) default 1 not null comment '状态:1正常0禁用'
) comment '用户表';
|
1
2
|
INSERT INTO `user` VALUES
(1,'rose','e10adc3949ba59abbe56e057f20f883e','admin','admin','1320104286@qq.com','13376536162','2024-12-02 18:54:44','2024-12-02 18:54:44',0,1,1);
|
对应的实体类:User
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private String userName;
private String password;
private String userRole;
private String avatar;
private String email;
private String phone;
private String createTime;
private String updateTime;
private short isDelete;
private short gender;
private short status;
}
|
设计用户登录Dto
1
2
3
4
5
6
7
8
9
10
11
12
|
@Data
public class UserLoginDto {
@NotBlank(message = "用户名不能为空")
private String username;
@NotBlank(message = "密码不能为空")
private String password;
@NotBlank(message = "验证码id不能为空")
private String captchaId;
@NotBlank(message = "验证码不能为空")
private String captcha;
}
|
设计用户登录Vo
也就是登录成功后,返回给前端的信息,token以及一些非敏感信息。
1
2
3
4
5
6
7
|
@Data
@Builder
public class UserLoginVO implements Serializable {
private String token;//令牌
private String userName;//用户名
private String avatar;//头像
}
|
登录业务逻辑实现
代码逻辑:参数校验(使用注解方式校验)—-验证码校验—-账号存在检验—-密码校验—-用户状态判断
service:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
@Override
public UserLoginVO login(UserLoginDto userLoginDto) {
// 校验验证码
String captchaId = userLoginDto.getCaptchaId();
String userCaptcha = userLoginDto.getCaptcha();
String cacheCaptcha = stringRedisTemplate.opsForValue().get("captcha:"+captchaId);
if(cacheCaptcha == null || !cacheCaptcha.equalsIgnoreCase(userCaptcha))
{
// 手动抛出异常
throw new CaptchaErrorException(ResultEnum.USER_CAPTCHA_ERROR);
}
// 用户存在校验
User user = new User();
user.setUserName(userLoginDto.getUsername());
// 根据用户名查询是否存在
User selectedUser = userMapper.selectByUsername(user);
if(selectedUser == null)
{
throw new AccountNotFoundException(ResultEnum.USER_NOT_EXIST);
}
// 密码校验 -- 对获取的用户信息,进行校验,应该是加密后的
if(!PassUtils.verifyPassword(userLoginDto.getPassword(),selectedUser.getPassword()))
{
throw new PasswordErrorException(ResultEnum.USER_PASSWORD_ERROR);
}
log.info("密码校验成功");
// 用户状态判断
if(selectedUser.getStatus() == 0)
{
log.info("status 为 {}",selectedUser.getStatus());
throw new AccountForbiddenException(ResultEnum.FAIL);
}
log.info("用户状态正常");
// 生成token
String token = JwtUtils.generateToken(Map.of("userId", selectedUser.getId(),
"userRole",selectedUser.getUserRole()),
"user");
//构建响应对象
return UserLoginVO.builder()
.userName(selectedUser.getUserName())
.avatar(selectedUser.getAvatar())
.token(token)
.build();
}
|
注册接口
注册的时候输入:username \password email emailcode 即可注册
需要注意的是:为了安全性考虑,密码不能明文保存,而应该使用 加盐md5算法 进行加密储存。
准备工作:
设计注册交互对象:
1
2
3
4
5
6
7
8
9
10
11
|
@Data
public class UserRegisterDto {
@NotBlank(message = "用户不能为空")
private String username;
@NotBlank(message = "密码不能为空")
private String password;
@NotBlank(message = "邮箱不能为空")
@Pattern(regexp = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", message = "邮箱格式不正确")
private String email;
private String captcha;
}
|
编写密码工具类(包加密和校验)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
package top.rose.template.commom.util;
import cn.hutool.crypto.digest.BCrypt;
/**
* @ author: rose
* @ date: 2024-12-04
* @ version: 1.0
* @ description:
*/
public class PassUtils {
/**
* 加密密码
* @param plainPassword 明文密码
* @return 加密后的密码*/
public static String encryptPassword(String plainPassword) {
return BCrypt.hashpw(plainPassword, BCrypt.gensalt());
}
/**
* 验证密码
* @param plainPassword 明文密码
* @param encryptedPassword 加密后的密码
* @return 验证结果*/
public static boolean verifyPassword(String plainPassword, String encryptedPassword) {
return BCrypt.checkpw(plainPassword,encryptedPassword);
}
}
|
注册业务逻辑实现:
代码逻辑:参数校验 — 验证码校验 — 用户邮箱存在校验 —– 密码加密 —- 存储到数据库中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
@Override
public User register(@Validated UserRegisterDto userRegisterDto) {
// 验证码校验
// 获取 验证码1
String registerCode = userRegisterDto.getCaptcha();
String registerEmail = userRegisterDto.getEmail();
String cacheCode = stringRedisTemplate.opsForValue().get("verifycode:"+registerEmail);
if(cacheCode == null ||!cacheCode.equals(registerCode))
{
throw new CaptchaErrorException(ResultEnum.USER_CAPTCHA_ERROR);
}
log.info("验证码校验成功");
// 用户或邮件存在校验,根据用户名和邮件查看是否存在
String registerUsername = userRegisterDto.getUsername();
User user = new User();
user.setUserName(registerUsername);
User registerUser = userMapper.selectByUsernameOrEmail(user);
if(registerUser != null)
{
throw new AccountRegisterFailException(ResultEnum.USER_REGISTER_FAIL);
}
log.info("用户不存在");
// 密码加密
String password = userRegisterDto.getPassword();
String passwordEncrypt = PassUtils.encryptPassword(password);
user.setPassword(passwordEncrypt);
log.info("密码加密成功");
// 保存用户信息
user.setUserRole("user");
user.setCreateTime(LocalDateTime.now().toString());
user.setUpdateTime(LocalDateTime.now().toString());
user.setEmail(registerEmail);
userMapper.insert(user);
log.info("用户注册成功");
return user;
}
|
总结:
至此,就完成了登录注册功能的基础开发。
还可以学习优化的方向是
-
mapper编写的简化(用mybatis-plus)。
-
登录注册方式的增加
- 邮件登录,如果没有账号则进行注册后登录。
- 使用短信验证的方式。