Back

SpringBoot3 整合登录注册

Springboot3.4.0 实现登录注册功能

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
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;
    }

总结:

至此,就完成了登录注册功能的基础开发。

还可以学习优化的方向是

  1. mapper编写的简化(用mybatis-plus)。

  2. 登录注册方式的增加

    1. 邮件登录,如果没有账号则进行注册后登录。
    2. 使用短信验证的方式。
author: rose
Built with Hugo
Theme Stack designed by Jimmy
© Licensed Under CC BY-NC-SA 4.0