java如何实现邮箱注册
前一段时间,突发奇想,趁着工作之余。从0开始搭建一套vue+springcloud的个人半成品作品。而其中用户注册使用到了邮箱进行注册,以此来记录。
·
前言
`前一段时间,突发奇想,趁着工作之余。从0开始搭建一套vue+springcloud的个人半成品作品。而其中用户注册使用到了邮箱进行注册,以此来记录
一、用户注册流程
不过,这里只是一个简单的注册流程。
用户操作流程
- 用户点击注册弹出页面,输入邮箱进行发送
- 邮箱获取验证码(有些方案可能是通过链接是否被点击验证过了来判断)
- 填入注册信息,进行提交
- 后台自动生成用户
解耦流程
主要通过MQ去解耦
- gateway中进行邮件发送安全性校验并通过MQ解耦,发送到邮件服务
- 邮件服务通过MQ消费邮件消息,并发送邮件
- gateway中处理用户注册安全性校验,并通过MQ解耦,发送给权限系统服务
- 权限系统服务处理MQ消息,并生成特定角色的用户
接下来去分析和实践上述功能
二、集成邮件服务
引入jar包
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>javax.mail</artifactId>
<version>1.4.7</version>
</dependency>
安全性校验
发送请求 /api/gateway/get-register-code
private final EmailRuleConfig emailRuleConfig;
public Result<RegisterCodeVO> getRegisterCode(String email) {
email = email.trim();
//1、邮箱合法性验证
//1.1 邮箱格式校验(前端也会进行校验)
boolean isEmail = Validator.isEmail(email);
if (!isEmail) {
return Result.error("对不起!您的邮箱格式不正确!");
}
//1.2 邮箱黑名单校验(在配置文件中配置)
boolean isBlackEmail = emailRuleConfig.getBlackList().stream().anyMatch(email::endsWith);
if (isBlackEmail) {
return Result.error("对不起!您的邮箱无法注册本网站!");
}
//2、邮箱安全性校验
//2.1 60s内不能重复发送邮件
String lockKey = MessageFormat.format(AuthConstant.REGISTER_EMAIL_LOCK, email);
if (redisUtil.hasKey(lockKey)) {
return Result.error("对不起,您的操作频率过快,请在" + redisUtil.getExpire(lockKey) + "秒后再次发送注册邮件!");
}
//2.2 邮箱是否已被注册过
QueryWrapper<SysUser> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("email", email).eq("status", 1);
SysUser userInfo = sysUserService.getOne(queryWrapper, false);
if (userInfo != null) {
return Result.error("对不起!该邮箱已被注册,请更换新的邮箱!");
}
//2.3 随机生成邮箱验证码,并且十分钟有效
String numbers = RandomUtil.randomNumbers(6);
redisUtil.set(MessageFormat.format(AuthConstant.REGISTER_KEY_PREFIX, email), numbers, 10 * 60);//默认验证码有效10分钟
//2.4 调用邮件发送服务
EmailSendDto entity = new EmailSendDto();
entity.setEmailCode(numbers);
entity.setEmail(email);
SendResult sendResult = rocketMQTemplate.syncSend(MqConstant.SEND_EMAIL_CODE, JSON.toJSON(entity));
log.info("邮箱注册消息发送响应:" + sendResult.toString());
redisUtil.set(lockKey, "0", 60);
RegisterCodeVO registerCodeVo = new RegisterCodeVO();
registerCodeVo.setEmail(email);
registerCodeVo.setExpire(5 * 60);
return Result.success(registerCodeVo);
}
上述获取验证码流程主要为
- 邮箱合法性验证(包含格式和邮箱黑名单校验)
- 安全性校验(包含验证码有效性以及控制重复发送等)
后缀过滤配置文件
@Component
@PropertySource(value = "classpath:email-rule.yml", factory = CompositePropertySourceFactory.class)
@ConfigurationProperties(prefix = "blog")
@Data
@RequiredArgsConstructor
public class EmailRuleConfig {
private List<String> blackList;
}
class CompositePropertySourceFactory extends DefaultPropertySourceFactory {
@Override
public org.springframework.core.env.PropertySource<?> createPropertySource(@Nullable String name, EncodedResource resource) throws IOException {
String sourceName = Optional.ofNullable(name).orElse(resource.getResource().getFilename());
if (!resource.getResource().exists()) {
return new PropertiesPropertySource(sourceName, new Properties());
} else if (sourceName.endsWith(".yml") || sourceName.endsWith(".yaml")) {
Properties propertiesFromYaml = loadYaml(resource);
return new PropertiesPropertySource(sourceName, propertiesFromYaml);
} else {
return super.createPropertySource(name, resource);
}
}
private Properties loadYaml(EncodedResource resource) throws IOException {
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
factory.setResources(resource.getResource());
factory.afterPropertiesSet();
return factory.getObject();
}
}
email-rule.yml
# 邮箱地址黑名单
blog:
blacklist:
- "@ccmail.uk"
- "@exdonuts.com"
- "@hamham.uk"
- "@digdig.org"
- "@owleyes.ch"
- "@stayhome.li"
- "@fanclub.pm"
- "@hotsoup.be"
- "@simaenaga.com"
- "@tapi.re"
- "@fuwari.be"
- "@magim.be"
- "@mirai.re"
- "@moimoi.re"
- "@heisei.be"
- "@honeys.be"
- "@mbox.re"
- "@uma3.be"
- "@fuwa.li"
- "@kpost.be"
- "@risu.be"
- "@fuwa.be"
- "@usako.net"
- "@eay.jp"
- "@via.tokyo.jp"
- "@ichigo.me"
- "@choco.la"
- "@cream.pink"
- "@merry.pink"
- "@neko2.net"
- "@fuwamofu.com"
- "@ruru.be"
- "@macr2.com"
- "@f5.si"
- "@ahk.jp"
- "@svk.jp"
邮件发送服务
@Slf4j
@Service
@RocketMQMessageListener(topic = MqConstant.SEND_EMAIL_CODE, consumerGroup = "system-service-code")
public class MQConsumeMailSendListener implements RocketMQListener<String>, RocketMQPushConsumerLifecycleListener {
@Autowired
private TemplateEngine templateEngine;
private final static String baseUrl = "邮箱跳转的地址";
@Override
public void onMessage(String body) {
JSONObject po = JSON.parseObject(body);
String email = po.getString("email");
String emailCode = po.getString("emailCode");
if (StringUtils.isEmpty(email) || StringUtils.isEmpty(emailCode)) {
return;
}
try {
sendMail(email, emailCode);
} catch (Exception e) {
log.error("用户注册的邮件任务发生异常------------>{}", e.getMessage());
}
}
@Override
public void prepareStart(DefaultMQPushConsumer defaultMQPushConsumer) {
defaultMQPushConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
}
private void sendMail(String email, String emailCode) throws Exception {
DateTime expireTime = DateUtil.offsetMinute(new Date(), 10);
// 设置渲染到html页面对应的值
Context context = new Context();
context.setVariable("BLOG_NAME", UnicodeUtil.toString("微交流学习平台"));
context.setVariable("BLOG_SHORT_NAME", UnicodeUtil.toString("微交流学习平台"));
context.setVariable("BLOG_URL", baseUrl);
context.setVariable("EMAIL_BACKGROUND_IMG", "图片背景图地址");
context.setVariable("CODE", emailCode);
context.setVariable("EXPIRE_TIME", expireTime.toString());
//利用模板引擎加载html文件进行渲染并生成对应的字符串
String emailContent = templateEngine.process("emailTemplate_registerCode", context);
MailInfo mailInfo = new MailInfo();
mailInfo.setReceiveMailAccount(email);
mailInfo.setMailContent(emailContent);
mailInfo.setMailTitle("微交流学习平台的注册邮件");
MailUtil.sendEmail(mailInfo);
}
}
邮件服务主要做两件事
- 利用freemarker生成邮件模板
- 发送邮件
emailTemplate_registerCode.html
<!DOCTYPE html>
<html lang="zh" xmlns:th=http://www.thymeleaf.org>
<head>
<meta charset="UTF-8">
<title>[[${BLOG_SHORT_NAME}]]用户注册验证码</title>
</head>
<body>
<div style="background: white;
width: 100%;
max-width: 800px;
margin: auto auto;
border-radius: 5px;
border:#1bc3fb 1px solid;
overflow: hidden;
-webkit-box-shadow: 0px 0px 20px 0px rgba(0, 0, 0, 0.12);
box-shadow: 0px 0px 20px 0px rgba(0, 0, 0, 0.18);">
<header style="overflow: hidden;">
<center>
<img style="width:100%;z-index: 666;"
th:src="${EMAIL_BACKGROUND_IMG}">
</center>
</header>
<div style="padding: 5px 20px; ">
<p style=" position: relative;
color: white;
float: left;
z-index: 999;
background: #1bc3fb;
padding: 5px 30px;
margin: -25px auto 0 ;
box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.30) ">
亲爱的邮箱注册用户
</p>
<br>
<center>
<h3>
来自 <span style=" text-decoration: none;color: #FF779A;" th:text="${BLOG_SHORT_NAME}"></span> 邮件提醒
</h3>
<p style="text-indent:2em;">
您收到这封电子邮件是因为您 (也可能是某人冒充您的名义) 在<a style=" text-decoration: none;color: #1bc3fb "
target="_blank" th:href="${BLOG_URL}" rel="noopener"> [[${BLOG_SHORT_NAME}]] </a>上进行注册。假如这不是您本人所申请,
请不用理会这封电子邮件, 但是如果您持续收到这类的信件骚扰, 请您尽快联络管理员。
</p>
<div style="background: #fafafa repeating-linear-gradient(-45deg,#fff,#fff 1.125rem,transparent 1.125rem,transparent 2.25rem);box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15);margin:20px 0px;padding:15px;border-radius:5px;font-size:14px;color:#555555;text-align: center; ">
请使用以下验证码完成后续注册流程:<br>
<span style=" color: #FF779A;font-weight: bolder;font-size: 25px;" th:text="${CODE}"></span><br>
注意:请您在收到邮件10分钟内([[${EXPIRE_TIME}]]前)使用,否则该验证码将会失效。
</div>
<br>
<div style=" text-align: center; ">
<a style="text-transform: uppercase;
text-decoration: none;
font-size: 14px;
background: #FF779A;
color: #FFFFFF;
padding: 10px;
display: inline-block;
border-radius: 5px;
margin: 10px auto 0;"
target="_blank" th:href="${BLOG_URL}" rel="noopener" th:text="${BLOG_SHORT_NAME}+'|传送门🚪'"></a>
</div>
<p style="font-size: 12px;text-align: center;color: #999; ">
欢迎常来访问!<br>
© 2022 <a style="text-decoration:none; color:#1bc3fb " th:href="${BLOG_URL}" rel="noopener"
target="_blank" th:text="${BLOG_NAME}"></a>
</p>
<p></p>
</center>
</div>
</div>
</body>
</html>
发送邮件
@Slf4j
public class MailUtil {
// 发件人的 邮箱 和 密码(替换为自己的邮箱和密码)
public static final String myEmailAccount = "发件人的邮箱";
public static final String myEmailPassword = getPass();
private static final String CHARSET_CODE = "UTF-8";
private static String getPass() {
return "发件人的密码";
}
// 发件人邮箱的 SMTP 服务器地址, 必须准确, 不同邮件服务器地址不同, 一般(只是一般, 绝非绝对)格式为: smtp.xxx.com
// public static String myEmailSMTPHost = "smtp.126.com";
public static final String myEmailSMTPHost = "smtp.163.com";
private static final String PORT = "465";
public static void sendEmail(MailInfo mailInfo) throws Exception {
if (Objects.isNull(mailInfo)) {
return;
}
// 1. 创建参数配置, 用于连接邮件服务器的参数配置
Properties properties = new Properties();
properties.put("mail.smtp.host", myEmailSMTPHost);
properties.put("mail.transport.protocol", "smtp");
properties.put("mail.smtp.auth", "true");
properties.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory"); // 使用JSSE的SSL
properties.put("mail.smtp.socketFactory.fallback", "false"); // 只处理SSL的连接,对于非SSL的连接不做处理
properties.put("mail.smtp.starttls.enable", "true");
properties.put("mail.smtp.port", PORT);
properties.put("mail.smtp.socketFactory.port", PORT);
properties.put("mail.smtp.ssl.checkserveridentity", true);
Session session = Session.getInstance(properties);
session.setDebug(true);
// 3. 创建一封邮件
MimeMessage message = new MailUtil().createMimeMessage(session, mailInfo);
// 4. 根据 Session 获取邮件传输对象
Transport transport = session.getTransport();
// 5. 使用 邮箱账号 和 密码 连接邮件服务器, 这里认证的邮箱必须与 message 中的发件人邮箱一致, 否则报错
transport.connect(mailInfo.getSendEmailAccount(), mailInfo.getSendEmailPassword());
// 6. 发送邮件, 发到所有的收件地址, message.getAllRecipients() 获取到的是在创建邮件对象时添加的所有收件人, 抄送人, 密送人
transport.sendMessage(message, message.getAllRecipients());
// 7. 关闭连接
transport.close();
}
public MimeMessage createMimeMessage(Session session, MailInfo mailInfo) throws Exception {
// 1. 创建一封邮件
MimeMessage message = new MimeMessage(session);
initContent(message, mailInfo);
System.out.println("mailInfo " + JSON.toJSONString(mailInfo));
// 2. From: 发件人(昵称有广告嫌疑,避免被邮件服务器误认为是滥发广告以至返回失败,请修改昵称)
message.setFrom(new InternetAddress(mailInfo.getSendEmailAccount(), mailInfo.getSendPersonName(), "UTF-8"));
// 3. To: 收件人(可以增加多个收件人、抄送、密送)
String[] accounts = mailInfo.getReceiveMailAccount().split(",");
Address[] addresses = new Address[accounts.length];
for (int i = 0; i < accounts.length; i++) {
if (Strings.isEmpty(mailInfo.getReceivePersonName())) {
addresses[i] = new InternetAddress(accounts[i]);
} else {
addresses[i] = new InternetAddress(accounts[i], mailInfo.getReceivePersonName(), "UTF-8");
}
}
message.setRecipients(MimeMessage.RecipientType.TO, addresses);
// 4. Subject: 邮件主题(标题有广告嫌疑,避免被邮件服务器误认为是滥发广告以至返回失败,请修改标题)
message.setSubject(mailInfo.getMailTitle(), "UTF-8");
// 5. Content: 邮件正文(可以使用html标签)(内容有广告嫌疑,避免被邮件服务器误认为是滥发广告以至返回失败,请修改发送内容)
// 6. 设置发件时间
message.setSentDate(new Date());
// 7. 保存设置
message.saveChanges();
return message;
}
private void initContent(MimeMessage message, MailInfo mailInfo) throws MessagingException {
List<File> files = mailInfo.getFileList();
// 非附件模式
if (CollectionUtils.isEmpty(files)) {
message.setContent(mailInfo.getMailContent(), "text/html;charset=UTF-8");
return;
}
for (File file : files) {
MimeMessageHelper helper = new MimeMessageHelper(message, true, "utf-8");
helper.addAttachment(file.getName(), file);
helper.setText(mailInfo.getMailContent(), true);
}
}
}
用户信息生成
安全性校验
/api/gateway/register-by-email
String codeKey = MessageFormat.format(AuthConstant.REGISTER_KEY_PREFIX, dto.getEmail());
if (!redisUtil.hasKey(codeKey)) {
return Result.error("验证码不存在或已过期");
}
if (!redisUtil.get(codeKey).equals(dto.getCode())) {
return Result.error("验证码不正确");
}
if (StringUtils.isEmpty(dto.getPassword())) {
return Result.error("密码不能为空");
}
if (dto.getPassword().length() < 6 || dto.getPassword().length() > 20) {
return Result.error("密码长度应该为6~20位!");
}
if (StringUtils.isEmpty(dto.getUsername())) {
return Result.error("用户名不能为空");
}
if (dto.getUsername().length() > 20) {
return Result.error("用户名长度不能超过20位!");
}
int count = sysUserService.count(new QueryWrapper<SysUser>().eq("user_code",
dto.getUsername()).eq("status", 1));
if (count > 0) {
return Result.error("用户名已存在!");
}
SysUser entity = new SysUser().setEmail(dto.getEmail())
.setUserCode(dto.getUsername()).setUserPwd(dto.getPassword());
try {
SendResult sendResult = rocketMQTemplate.syncSend(MqConstant.REGISTER_BY_EMAIL, JSON.toJSON(entity));
log.info("邮箱用户注册消息发送响应:" + sendResult.toString());
} catch (Exception e) {
log.error("邮箱用户注册失败, {}",e);
return Result.error("邮箱注册失败,请稍后再试");
}
return Result.success("注册成功");
}
上述流程主要为
- 用户输入的注册信息校验(包含验证码与信息合法性校验)
- 用户信息通过MQ发送给权限服务处理邮箱注册内容
生成用户信息
主要为权限服务处理
@Slf4j
@Service
@RocketMQMessageListener(topic = MqConstant.REGISTER_BY_EMAIL, consumerGroup = "system-service-register")
public class MQConsumeMailRegisterListener implements RocketMQListener<String>, RocketMQPushConsumerLifecycleListener {
@Autowired
private ISysUserService sysUserService;
@Override
public void onMessage(String body) {
JSONObject po = JSON.parseObject(body);
String email = po.getString("email").trim();
String username = po.getString("userCode");
String password = po.getString("userPwd");
if (StringUtils.isEmpty(email) || StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
return;
}
//生成用户信息
sysUserService.RegisterByEmail(username, password, email);
}
@Override
public void prepareStart(DefaultMQPushConsumer defaultMQPushConsumer) {
defaultMQPushConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
}
}
踩坑
以上只是实现了一个简单的邮箱注册功能并实践到自己的作品中(有兴趣朋友可以登录笔者半成品作品玩玩~)。我们也可以通过点击邮件中的链接来进行邮箱验证,也是一种很好的解决方案
本地服务可以正常发送服务,部署到服务器上邮件发送报错
Could not connect to SMTP host: smtp.163.com, port: 465
解决方案:
修改服务器中JDK中的java.security:去掉SSL3,TLSv1, TLSv1.1即可
更多推荐
已为社区贡献1条内容
所有评论(0)