- 注重版权,转载请注明原作者和原文链接
- 作者:Yuan-Programmer
- 个人博客:https://www.xiaoyuan-boke.com 正在持续完善中
- 进来的小伙伴点点赞呀
花了几个小时做了一个SpringBoot+Vue实现邮箱登录注册找回密码的demo项目,项目已在Gitee上开源,Gitee开源地址(有接口文档):https://gitee.com/yuandewei/Yuan-SpringBoot/tree/master
跟着我的脚本一步一步实现代码,学会了你也能自己写出来 (或者根据接口文档自己写后端)✨
⛄ 小袁有话说
今天的教程内容呢是实现邮箱注册登录账号,以及发送邮箱验证码校验验证码和找回密码等,效果图如下
文章底部有视频效果展示
话不多说,开始今天的详细教程
数据表的设计,没啥问题,邮箱、密码、加密盐,这里只是演示邮箱功能,所以字段就没有设计太多,需要的就自己额外设计了
redis本次案例用于存储请求权限码和邮箱验证码
复制链接下载windows解压版 https://user.xiaoyuan-boke.com/Redis-x64-5.0.14.zip
下载好直接解压就好,打开cmd命令控制台,来到刚刚解压的位置(我这里是F盘下的redis文件夹)
输入指令启动 redis-server.exe redis.windows.conf
,显示下面这样则成功启动,窗口不能关闭,关闭了redis也跟着关闭了
我这里呢是直接创建一个SpringBoot的项目,启动类加上组件扫描
和映射文件扫描
,同时创建基础结构目录,如图
项目如何创建这里就不一步一步教啦,相信之前看过几篇教学应该都会了
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.3.9.RELEASEversion>
<relativePath/>
parent>
<properties>
<java.version>1.8java.version>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.4.0version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.76version>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-lang3artifactId>
<version>3.12.0version>
dependency>
<dependency>
<groupId>commons-codecgroupId>
<artifactId>commons-codecartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-mailartifactId>
dependency>
dependencies>
application 这个也没啥问题吧,大家应该都能看懂
server:
port: 8081
spring:
# 数据源配置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql:///user?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
username: xiaoyuan
password: root
redis:
port: 6379
host: localhost
# 时间格式转换
jackson:
time-zone: GMT+8
date-format: yyyy-MM-dd HH:mm:ss
profiles:
# 引入application-email配置文件
include: email
mybatis-plus:
# mapper文件映射路径
mapper-locations: classpath*:mapper/*.xml
configuration:
# 打印SQL语句
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
logging:
level:
com.shyroke.mapper: debug
application-email.yml 这个是配置邮箱信息的
spring:
mail:
host: smtp.qq.com
username: 自己的QQ邮箱
password: 授权码
protocol: smtp
default-encoding: UTF-8
properties:
mail:
smtp:
auth: true
starttls:
enable: true
required: true
ssl:
enable: true
这里主要一点,要实现邮箱发送功能,得开启SMTP服务,打开自己的QQ邮箱,点击设置 -> 点击账户 -> 开启SMTP服务 -> 获取授权码,将授权码复制到上面的 application-email.yml 文件里
User 实体类,entity 包下
@Data
@TableName("t_user")
public class User {
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
private String email;
private String password;
private String salt;
}
UserMapper 数据访问接口,mapper 包下,继承MyBatis-Plus的 BaseMapper,内部封装了单表的大部分操作
@Repository
public interface UserMapper extends BaseMapper<User> {
}
constant 包下新建 RedisConstant 类,
public interface RedisConstant {
// Key
String EMAIL = "EMAIL_"; // 邮箱缓存前缀
String EMAIL_REQUEST_VERIFY = "EMAIL_REQUEST_VERIFY_"; // 邮箱请求的权限码
// 缓存时间
int EXPIRE_TEN_SECOND = 10; // 10s
int EXPIRE_ONE_MINUTE = 60; // 1分钟
int EXPIRE_FIVE_MINUTE = 5 * 60; // (五分钟)
int EXPIRE_HALF_HOUR = 30 * 60; // 半小时(30分钟)
int EXPIRE_ONE_DAY = 24 * 60 * 60; // (1天)
}
新建 HttpStatusEnum 枚举类
@Getter
public enum HttpStatusEnum {
EMAIL_ERROR(4001, "邮箱格式不正确"),
PARAM_ERROR(4002, "参数格式不正确"),
CODE_ERROR(4002, "验证码不正确"),
PASSWORD_ERROR(4003, "密码错误"),
USER_NOT_EXIST(4004, "用户不存在"),
EMAIL_ALREADY_EXIST(4005, "邮箱已被注册"),
PASSWORD_INCONSISTENT(4006, "密码不一致"),
PARAM_ILLEGAL(4007, "参数不合法"),
INTERNAL_SERVER_ERROR(500, "服务器异常"),
UNKNOWN_ERROR(66666, "未知异常, 联系管理员"),
ILLEGAL_OPERATION(88888, "非法操作");
private final int code;
private final String msg;
HttpStatusEnum(int code, String msg) {
this.code = code;
this.msg = msg;
}
}
vo 包下新建 R 统一返回类
@Data
public class R {
private Boolean success;
private Integer code;
private String message;
private Map<String, Object> data = new HashMap<>();
// 把构造方法私有化
private R() {}
// 成功静态方法
public static R ok() {
R r = new R();
r.setSuccess(true);
r.setCode(200);
r.setMessage("成功");
return r;
}
// 失败静态方法
public static R error() {
R r = new R();
r.setSuccess(false);
r.setCode(20001);
r.setMessage("失败");
return r;
}
// 失败静态方法
public static R error(HttpStatusEnum httpStatus) {
R r = new R();
r.setSuccess(false);
r.setCode(httpStatus.getCode());
r.setMessage(httpStatus.getMsg());
return r;
}
public R success(Boolean success){
this.setSuccess(success);
return this;
}
public R message(String message){
this.setMessage(message);
return this;
}
public R code(Integer code){
this.setCode(code);
return this;
}
public R data(String key, Object value){
this.data.put(key, value);
return this;
}
public R data(Map<String, Object> map){
this.setData(map);
return this;
}
}
新建param包,包下新建LoginParam类
@Getter
public class LoginParam {
private String email; // 邮箱
private String password; // 密码
private String passwordConfirm; // 确认密码
private String code; // 验证码
}
utils 包下新建StringUtil类,编写自定义的一些字符串工具类(如邮箱校验,验证码生成)
public class StringUtil {
/**
* 邮箱校验
*
* @param email 邮箱
* @return true or false
*/
public static boolean checkEmail(String email) {
String check = "^([a-zA-Z]|[0-9])(\\w|\\-)+@[a-zA-Z0-9]+\\.([a-zA-Z]{2,4})$";
Pattern regex = Pattern.compile(check);
Matcher matcher = regex.matcher(email);
return matcher.matches();
}
/**
* 密码校验(长度 6-18,至少包含1个字母)
* @param password
* @return
*/
public static boolean checkPassword(String password) {
String check = "(?=.*[a-zA-Z])[a-zA-Z0-9]{6,18}";
Pattern regex = Pattern.compile(check);
Matcher matcher = regex.matcher(password);
return matcher.matches();
}
/**
* 随机生成六位数字验证码
*/
public static String randomSixCode() {
return String.valueOf(new Random().nextInt(899999) + 100000);
}
/**
* 随机生成加密盐(4位随机字母 + 4位固定特殊字符)
*/
public static String randomEncryptedSalt() {
return RandomStringUtils.randomAlphabetic(4) + "#!$@";
}
}
至此,基本完成了80%的工作,剩下的就是最重要的部分了,service业务层的设计,这部分我会详细介绍
service 包下创建 MailService 邮箱服务类,这里只列出了普通邮件和HTML邮件的代码,其他类型邮件网上也有的
javaMailSender Java内部封装好的邮箱发送类,只需要导入对应的依赖,直接注入即可
from 通过@Value注解读取 application-email 的username字段(也就是自己的邮箱)
message.setForm 中的三个参数,第一个是发件人(也就是自己),第二个是发件人昵称,也就是下面图片框出来的,第三个参数编码
@Component
public class MailService {
@Resource
private JavaMailSender javaMailSender;
@Value("${spring.mail.username}")
private String from;
/**
* 发送简单的邮箱
*
* @param to 收件人
* @param theme 标题
* @param content 正文内容
* @param cc 抄送
*/
public void sendSimpleMail(String to, String theme, String content, String... cc) {
// 创建邮件对象
SimpleMailMessage message = new SimpleMailMessage();
try {
message.setFrom(String.valueOf(new InternetAddress(from, "小袁博客平台", "UTF-8"))); // 发件人
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
message.setTo(to); // 收件人
message.setSubject(theme); // 标题
message.setText(content); // 内容
if (ArrayUtils.isNotEmpty(cc)) {
message.setCc(cc);
}
// 发送
javaMailSender.send(message);
}
/**
* 发送HTML邮件
*
* @param to 收件人地址
* @param subject 邮件主题
* @param content 邮件内容
* @param cc 抄送地址
* @throws MessagingException 邮件发送异常
*/
public void sendHtmlMail(String to, String subject, String content, String... cc) throws MessagingException {
MimeMessage message = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setFrom(from);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(content, true);
if (ArrayUtils.isNotEmpty(cc)) {
helper.setCc(cc);
}
javaMailSender.send(message);
}
}
先在主目录创建 config 包,包下新建 ThreadPoolConfig 类,这是线程池的配置类,帮助统一管理线程,线程池的其他作用就不一一介绍啦,挺多的,可以B站找视频学习
@Configuration
@EnableAsync // 开启线程池
public class ThreadPoolConfig {
@Bean("taskExecutor")
public Executor asyncServiceExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 设置核兴线程数
executor.setCorePoolSize(5);
// 设置最大线程数
executor.setMaxPoolSize(20);
// 设置队列大小
executor.setQueueCapacity(Integer.MAX_VALUE);
// 设置线程活跃时间 60s
executor.setKeepAliveSeconds(60);
// 设置默认线程名称
executor.setThreadNamePrefix("小袁博客平台");
// 是否所有任务执行完毕后关闭线程池
executor.setWaitForTasksToCompleteOnShutdown(true);
// 执行初始化
executor.initialize();
return executor;
}
}
service 包下新建 threadService 线程服务类
发送邮箱是一件很耗时的操作,如果不开辟线程去执行在主线程执行的话,会等待很久导致请求超时,而且我们发送验证码也不需要等到邮件到了才返回给前端提示
所以我们开辟线程去执行邮箱发送操作,不占用主线程运行
@Component
public class ThreadService {
@Autowired
private MailService mailService;
/**
* 发送邮箱
* @param to 收件人
* @param theme 主题
* @param content 内容
*/
@Async("taskExecutor")
public void sendSimpleMail(String to, String theme, String content) {
mailService.sendSimpleMail(to, theme, content);
}
}
新建 CommonService 公共服务接口类,注意是接口,这里讲解一下发送验证码的设计
使用权限码校验防止故意重复使用该接口,每次发送验证码请求前都先请求一个随机的权限码(有效时间越短越好,当然也不要太短,5 - 10s即可)
public interface CommonService {
/**
* 获取请求权限码
* @param emailJson 邮箱
* @return
*/
R getRequestPermissionCode(String emailJson);
/**
* 发送邮箱验证码
* @param loginParam (邮箱和权限码)
* @return
*/
R sendEmailCode(LoginParam loginParam);
}
新建 CommonServiceImpl 类实现 CommonService 接口
代码每行都写有注释,内容不难,参数校验 -> 邮箱校验 -> 权限码校验 -> 发送验证码
@Component
public class CommonServiceImpl implements CommonService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private ThreadService threadService;
@Override
public R getRequestPermissionCode(String emailJson) {
// 非空校验
if (StringUtils.isBlank(emailJson)) return R.error(HttpStatusEnum.PARAM_ILLEGAL);
// JSON转换,提取email的值
String email = JSON.parseObject(emailJson).getString("email").trim();
// 邮箱校验
if (!StringUtil.checkEmail(email)) {
return R.error(HttpStatusEnum.EMAIL_ERROR);
}
// 随机生成权限码
String permissionCode = UUID.randomUUID().toString();
// 存入redis,缓存10s
redisTemplate.opsForValue().set(RedisConstant.EMAIL_REQUEST_VERIFY + email, permissionCode, RedisConstant.EXPIRE_TEN_SECOND, TimeUnit.SECONDS);
return R.ok().data("permissionCode", permissionCode);
}
@Override
public R sendEmailCode(LoginParam loginParam) {
if (loginParam == null) return R.error(HttpStatusEnum.PARAM_ILLEGAL);
// 获取权限码和邮箱
String email = loginParam.getEmail();
String permissionCode = loginParam.getCode();
// 参数校验
if (StringUtils.isAnyBlank(email, permissionCode)) {
return R.error(HttpStatusEnum.PARAM_ILLEGAL);
}else if (!StringUtil.checkEmail(email)) {
// 邮箱校验
return R.error(HttpStatusEnum.EMAIL_ERROR);
}else {
// 权限码比对
String rightCode = redisTemplate.opsForValue().get(RedisConstant.EMAIL_REQUEST_VERIFY + email);
if (!permissionCode.equals(rightCode)) {
// 不通过
return R.error(HttpStatusEnum.ILLEGAL_OPERATION);
}
}
// 全部通过
// 随机生成6位数字验证码
String code = StringUtil.randomSixCode();
// 正文内容
String content = "亲爱的用户:\n" +
"您此次的验证码为:\n\n" +
code + "\n\n" +
"此验证码5分钟内有效,请立即进行下一步操作。 如非你本人操作,请忽略此邮件。\n" +
"感谢您的使用!";
// 发送验证码
threadService.sendSimpleMail(email, "您此次的验证码为:" + code, content);
// 丢入缓存,设置5分钟过期
redisTemplate.opsForValue().set(RedisConstant.EMAIL + email, code, RedisConstant.EXPIRE_FIVE_MINUTE, TimeUnit.SECONDS);
return R.ok();
}
}
邮箱服务已经设计好了,接下来写邮箱服务的接口,然后前端运行测试一下,邮箱功能是否正常
接口路径如果不和我这个一样的话,前端api下的请求记得也要改路径
@RestController
@RequestMapping("/common")
@CrossOrigin
public class CommonController {
@Autowired
private CommonService commonService;
// 权限码请求接口
@PostMapping("code/request")
public R getRequestPermissionCode(@RequestBody String emailJson) {
return commonService.getRequestPermissionCode(emailJson);
}
// 邮箱验证码接口
@PostMapping("code/email")
public R sendEmailCode(@RequestBody LoginParam loginParam) {
return commonService.sendEmailCode(loginParam);
}
}
启动后端项目,启动前端项目,成功发送邮箱验证码,没有问题
新建 UserService 接口类
public interface UserService extends IService<User> {
/**
* 登录
* @param loginParam (邮箱和密码)
* @return
*/
R login(LoginParam loginParam);
/**
* 注册
* @param loginParam (邮箱、密码、确认密码、验证码)
* @return
*/
R register(LoginParam loginParam);
/**
* 找回密码
* @param loginParam (邮箱、密码、验证码)
* @return
*/
R findPassword(LoginParam loginParam);
}
新建 UserServiceImpl 类实现 UserService 接口
@Service
@Transactional
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Override
public R login(LoginParam loginParam) {
return null;
}
@Override
public R register(LoginParam loginParam) {
return null;
}
@Override
public R findPassword(LoginParam loginParam) {
return null;
}
}
登录注册找回密码我一个一个来介绍
参数校验 -> 用户是否存在 -> 密码是否正确 -> 登录成功
@Override
public R login(LoginParam loginParam) {
if (loginParam == null) return R.error(HttpStatusEnum.PARAM_ILLEGAL);
// 获取参数
String email = loginParam.getEmail();
String password = loginParam.getPassword();
if (StringUtils.isAnyBlank(email, password)) {
// 非空
return R.error(HttpStatusEnum.PARAM_ILLEGAL);
}else if (!StringUtil.checkEmail(email)) {
// 邮箱格式校验
return R.error(HttpStatusEnum.EMAIL_ERROR);
}else if (!StringUtil.checkPassword(password)) {
// 密码格式
return R.error(HttpStatusEnum.PASSWORD_ERROR);
}
// 构件条件对象 select salt from user where email = #{email} limit 1
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.select("salt");
wrapper.eq("email", email);
wrapper.last("limit 1");
// 查询结果
User user = this.baseMapper.selectOne(wrapper);
if (user == null) {
// 用户不存在
return R.error(HttpStatusEnum.USER_NOT_EXIST);
}
// 获取加密盐
String salt = user.getSalt();
// 重新设置条件 select id from user where email = #{email} and password #{password} limit 1
wrapper.clear();
wrapper.select("id");
wrapper.eq("email", email);
wrapper.eq("password", DigestUtils.md5Hex(password + salt));
wrapper.last("limit 1");
// 查询用户
user = this.baseMapper.selectOne(wrapper);
return user == null ? R.error(HttpStatusEnum.PASSWORD_ERROR) : R.ok();
}
参数校验 -> 邮箱是否被注册 -> 验证码比对-> 删除redis验证码 -> 生成加密盐 -> 加密密码 -> 注册用户
@Override
public R register(LoginParam loginParam) {
if (loginParam == null) return R.error(HttpStatusEnum.PARAM_ILLEGAL);
// 获取参数
String email = loginParam.getEmail();
String password = loginParam.getPassword();
String passwordConfirm = loginParam.getPasswordConfirm();
String code = loginParam.getCode();
if (StringUtils.isAnyBlank(email, password, passwordConfirm, code)) {
// 非空
return R.error(HttpStatusEnum.PARAM_ILLEGAL);
}else if (!StringUtil.checkEmail(email)) {
// 邮箱格式校验
return R.error(HttpStatusEnum.EMAIL_ERROR);
}else if (!password.equals(passwordConfirm)) {
// 密码一致校验
return R.error(HttpStatusEnum.PASSWORD_INCONSISTENT);
}else if (!StringUtil.checkPassword(password) || code.length() != 6) {
// 密码格式和验证码长度校验
return R.error(HttpStatusEnum.PARAM_ILLEGAL);
}
// 构造查询条件对象
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.select("id");
wrapper.eq("email", email);
wrapper.last("limit 1");
// 查询用户,是否存在
if (this.baseMapper.selectOne(wrapper) != null) {
return R.error(HttpStatusEnum.EMAIL_ALREADY_EXIST);
}
// 获取正确的验证码
String rightCode = redisTemplate.opsForValue().get(RedisConstant.EMAIL + email);
if (!code.equals(rightCode)) {
// 验证码比对
return R.error(HttpStatusEnum.CODE_ERROR);
}
// 删除验证码
redisTemplate.delete(RedisConstant.EMAIL + email);
// 注册用户
User user = new User();
// 获取加密盐
String salt = StringUtil.randomEncryptedSalt();
// 邮箱
user.setEmail(email);
// 密码加密(原明文密码 + 随机加密盐) md5加密
user.setPassword(DigestUtils.md5Hex(password + salt));
// 加密盐
user.setSalt(salt);
// 插入数据
return this.baseMapper.insert(user) == 0 ? R.error(HttpStatusEnum.UNKNOWN_ERROR) : R.ok();
}
参数校验 -> 用户是否存在 -> 验证码校验 -> 删除redis验证码 -> 覆盖密码
@Override
public R findPassword(LoginParam loginParam) {
if (loginParam == null) return R.error(HttpStatusEnum.PARAM_ILLEGAL);
// 获取参数
String email = loginParam.getEmail();
String password = loginParam.getPassword();
String code = loginParam.getCode();
if (StringUtils.isAnyBlank(email, password, code)) {
// 非空
return R.error(HttpStatusEnum.PARAM_ILLEGAL);
}else if (!StringUtil.checkEmail(email)) {
// 邮箱格式校验
return R.error(HttpStatusEnum.EMAIL_ERROR);
}else if (!StringUtil.checkPassword(password) || code.length() != 6) {
// 密码格式和验证码长度校验
return R.error(HttpStatusEnum.PARAM_ILLEGAL);
}
// 构造查询条件对象
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.select("id", "salt");
wrapper.eq("email", email);
wrapper.last("limit 1");
// 查询用户,是否存在
User user = this.baseMapper.selectOne(wrapper);
if (user == null) {
return R.error(HttpStatusEnum.USER_NOT_EXIST);
}
// 获取正确的验证码
String rightCode = redisTemplate.opsForValue().get(RedisConstant.EMAIL + email);
if (!code.equals(rightCode)) {
// 验证码比对
return R.error(HttpStatusEnum.CODE_ERROR);
}
// 删除验证码
redisTemplate.delete(RedisConstant.EMAIL + email);
// 修改密码
User user1 = new User();
user1.setId(user.getId());
user1.setPassword(DigestUtils.md5Hex(password + user.getSalt()));
// 修改
return this.baseMapper.updateById(user1) == 0 ? R.error(HttpStatusEnum.UNKNOWN_ERROR) : R.ok();
}
接口路径如果不和我这个一样的话,前端api下的请求记得也要改路径
@RestController
@RequestMapping("/user")
@CrossOrigin
public class UserController {
@Autowired
private UserService userService;
// 登录
@PostMapping("login")
public R login(@RequestBody LoginParam loginParam) {
return userService.login(loginParam);
}
// 注册
@PostMapping("register")
public R register(@RequestBody LoginParam loginParam) {
return userService.register(loginParam);
}
// 找回密码
@PostMapping("findPassword")
public R findPassword(@RequestBody LoginParam loginParam) {
return userService.findPassword(loginParam);
}
}
最后看看视频演示的效果
SpringBoot+Vue实现邮箱登录注册找回密码
好了,整篇的教程呢到这也就结束,整篇教程即为原创一字一字手敲,也花了心思想怎么写怎么设计才能更好的直观简洁展示给大家,让大家能看懂
最后,关于教程还有什么不懂的可以评论区留言,我一定会回复的,或者有什么更好的建议和想法也可以在评论区留言,看到好的我会一一采纳,感谢大家的支持
再一次附上Gitee开源地址:https://gitee.com/yuandewei/Yuan-SpringBoot/tree/master 不用大伙翻上去复制了
- 都看到这里啦,点点赞呀
- 感谢阅读