java如何实现邮箱注册


前言

`前一段时间,突发奇想,趁着工作之余。从0开始搭建一套vue+springcloud的个人半成品作品。而其中用户注册使用到了邮箱进行注册,以此来记录

一、用户注册流程

不过,这里只是一个简单的注册流程。

用户注册
邮箱验证

用户操作流程

  1. 用户点击注册弹出页面,输入邮箱进行发送
  2. 邮箱获取验证码(有些方案可能是通过链接是否被点击验证过了来判断)
  3. 填入注册信息,进行提交
  4. 后台自动生成用户

解耦流程

主要通过MQ去解耦

  1. gateway中进行邮件发送安全性校验并通过MQ解耦,发送到邮件服务
  2. 邮件服务通过MQ消费邮件消息,并发送邮件
  3. gateway中处理用户注册安全性校验,并通过MQ解耦,发送给权限系统服务
  4. 权限系统服务处理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);
    }

上述获取验证码流程主要为

  1. 邮箱合法性验证(包含格式和邮箱黑名单校验)
  2. 安全性校验(包含验证码有效性以及控制重复发送等)

后缀过滤配置文件

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

邮件服务主要做两件事

  1. 利用freemarker生成邮件模板
  2. 发送邮件

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">&nbsp;[[${BLOG_SHORT_NAME}]]&nbsp;</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>
                    &nbsp; &nbsp;

                    <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("注册成功");
    }

上述流程主要为

  1. 用户输入的注册信息校验(包含验证码与信息合法性校验)
  2. 用户信息通过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即可

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值