前置条件
本文衔接上文,请从上文开始spring boot3登录开发-2(1图形验证码接口实现) 用户表设计如下:
create table user ( id bigint auto_increment comment '主键' primary key, user_name varchar(32) null comment '用户昵称', password varchar(256) null comment '密码', user_account varchar(64) null comment '账号', user_role varchar(256) default 'user' null comment '用户角色:user / admin', avatar varchar(1024) null comment '头像', create_time datetime default (now()) null comment '创建时间', update_time datetime default CURRENT_TIMESTAMP 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 '用户表'; INSERT INTO `user` VALUES (1,'蒾酒','e10adc3949ba59abbe56e057f20f883e','admin','admin',NULL,'2024-02-02 18:54:44','2024-02-02 18:54:44',0,NULL,1);
内容简介
上文我们已经实现了图形验证码接口,本文我们实现登录逻辑
- 通过用户登录DTO(数据传输对象)接收用户登录填写信息
- 通过注解@NotNull、@Valid进行参数非空校验
- 通过redis缓存的验证码信息与用户提交的比对验证
- 通过全局异常处理处理参数为空、用户不存在、密码错误、验证码错误、用户被封禁等业务异常
用户登录逻辑实现
作者习惯于将业务代码全部放在service层里面,controller层只用于暴漏接口封装返回结果。
创建交互对象
1.创建用户登录DTO
说白了就是用户登录表单提交发起post请求,请求体负载的登录对象后端需要有个对象跟表单对象字段对应接收:请求体json->接收对象。这一过程也叫反序列化。
通过@NotNull做非空校验
import jakarta.validation.constraints.NotNull; import lombok.Data; /** * @author mijiupro */ @Data public class UserLoginDTO { @NotNull(message = "账号不能为空") private String userAccount;//用户账号 @NotNull(message = "密码不能为空") private String password;//密码 @NotNull(message = "验证码id不能为空") private String captchaId;//验证码id @NotNull(message = "验证码内容不能为空") private String captcha;//验证码内容 }
2.创建用户登录VO
通俗来说就是用户登录成功后返回给前端一个合法令牌token,以及一些非敏感信息方便前端展示,这些信息包装成一个对象,展示对象->json。这一过程又叫序列化。
import lombok.Builder; import lombok.Data; import java.io.Serializable; /** * @author mijiupro */ @Data @Builder public class UserLoginVO implements Serializable { private String token;//令牌 private String userName;//用户名 private String avatar;//头像 }
创建自定义登录业务异常
说白了就是登录代码可能会判断账号是否存在密码是否正确,当账号不存在或密码错误需要返回对应提示信息,这种类似情况多了你的代码就会很多if-return,代码就会很难看;那么通过自定义异常去到异常处理的方法里面写对应返回提示以及其他逻辑,这样直接抛出对应异常AOP拦截到该异常走对应异常处理逻辑即可。(一句话概括就是:把处理特殊业务异常情况的代码逻辑抽取出来放到别的类里面写,可以使代码更加清晰和可维护)
1.创建验证码错误异常
import com.mijiu.commom.enumerate.ResultEnum; import lombok.Getter; /** * @author mijiupro */ @Getter public class CaptchaErrorException extends RuntimeException { private final ResultEnum resultEnum;//返回提示信息枚举(code,message) public CaptchaErrorException(ResultEnum resultEnum) { this.resultEnum = resultEnum; } }
2.创建用户不存在异常
import com.mijiu.commom.enumerate.ResultEnum; import lombok.Getter; /** * 账户不存在异常 * * @author mijiupro */ @Getter public class AccountNotFoundException extends RuntimeException { private final ResultEnum resultEnum; public AccountNotFoundException(ResultEnum resultEnum) { this.resultEnum = resultEnum; } }
3.创建密码错误异常
import com.mijiu.commom.enumerate.ResultEnum; import lombok.Getter; /** * 密码错误异常 * * @author mijiupro */ @Getter public class PasswordErrorException extends RuntimeException { private final ResultEnum resultEnum; public PasswordErrorException(ResultEnum resultEnum) { this.resultEnum = resultEnum; } }
4.创建用户被封禁异常
import com.mijiu.commom.enumerate.ResultEnum; import lombok.Getter; /** * @author mijiupro */ @Getter public class AccountForbiddenException extends RuntimeException { private final ResultEnum resultEnum; public AccountForbiddenException(ResultEnum resultEnum) { this.resultEnum = resultEnum; } }
2.登录业务逻辑实现
代码逻辑:参数校验(使用注解方式校验)----验证码校验----账号存在检验----密码校验----用户状态判断
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; import com.mijiu.commom.enumerate.ResultEnum; import com.mijiu.commom.exception.AccountForbiddenException; import com.mijiu.commom.exception.AccountNotFoundException; import com.mijiu.commom.exception.CaptchaErrorException; import com.mijiu.commom.exception.PasswordErrorException; import com.mijiu.commom.model.dto.UserLoginDTO; import com.mijiu.commom.model.vo.UserLoginVO; import com.mijiu.commom.util.JwtUtils; import com.mijiu.entity.User; import com.mijiu.mapper.UserMapper; import com.mijiu.service.UserService; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import jakarta.validation.Valid; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import org.springframework.util.DigestUtils; import java.util.Map; /** * <p> * 用户表 服务实现类 * </p> * * @author 蒾酒 * @since 2024-02-03 */ @Service @Slf4j public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { private final UserMapper userMapper; private final JwtUtils jwtUtils; private final StringRedisTemplate stringRedisTemplate; public UserServiceImpl(UserMapper userMapper, JwtUtils jwtUtils, StringRedisTemplate stringRedisTemplate) { this.userMapper = userMapper; this.jwtUtils = jwtUtils; this.stringRedisTemplate = stringRedisTemplate; } @Override public UserLoginVO login(@Valid UserLoginDTO userLoginDTO) { // 获取验证码id String captchaId = userLoginDTO.getCaptchaId(); // 获取用户提交验证码 String userCaptcha = userLoginDTO.getCaptcha(); // 获取缓存验证码 String cacheCaptcha = stringRedisTemplate.opsForValue().get("login:captcha:" + captchaId); // 比较验证码是否正确 if (cacheCaptcha == null || !cacheCaptcha.equalsIgnoreCase(userCaptcha)) { throw new CaptchaErrorException(ResultEnum.USER_CAPTCHA_ERROR); } // 判断用户是否存在 User loginUser = new LambdaQueryChainWrapper<>(userMapper) .select(User::getId, User::getUserAccount, User::getPassword, User::getUserName, User::getUserRole, User::getAvatar, User::getStatus) .eq(User::getUserAccount, userLoginDTO.getUserAccount()) .one(); if (loginUser == null) { throw new AccountNotFoundException(ResultEnum.USER_NOT_EXIST); } log.info("loginUser: {}", loginUser); // 判断密码是否正确 String md5Password = DigestUtils.md5DigestAsHex(userLoginDTO.getPassword().getBytes()); if (!md5Password.equals(loginUser.getPassword())) { throw new PasswordErrorException(ResultEnum.USER_PASSWORD_ERROR); } // 判断用户状态是否正常 if (!loginUser.getStatus()) { throw new AccountForbiddenException(ResultEnum.USER_ACCOUNT_FORBIDDEN); } // 生成token String token = jwtUtils.generateToken(Map.of("userId", loginUser.getId(), "userRole",loginUser.getUserRole()), "user"); //构建响应对象 return UserLoginVO.builder() .userName(loginUser.getUserName()) .avatar(loginUser.getAvatar()) .token(token) .build(); } }
这里要说一下的是通常数据库不放密码明文,这样做可以防止别人获取数据库直接得到账密登录违规操作风险,代码中使用MD5加密是很容易被暴力破解的所以可以用MD5加盐策略或者其他安全加密算法。
3.测试接口
测试之前记得把图形验证码接口中redis缓存验证码的过期时间设置的长一点。
先生成一个验证码
日志打印图形验证码文本
正常测试
验证码错误测试
用户不存在测试
密码错误测试
账号被封禁测试
字段status修改为0代表被禁用