Bug01_服务器Docker环境下,邮件发送超时 & javax.net.ssl.SSLHandshakeException: No appropriate protocol

一、Bug1:邮件发送超时

Bug1描述:

我的Java项目在用户注册激活时有邮件发送需求,开发环境下localhost能够通过25号端口实现邮件发送;部署项目到生产环境下centos系统的服务器,出现邮件发送连接超时现象。

Bug1处理:

起初我以为如下日志信息是报错信息:

DEBUG SMTP: useEhlo true, useAuth true
DEBUG SMTP: trying to connect to host "smtp.163.com", port 465, isSSL false

后来发现开发环境下成功实现邮件发送同样会有此日志信息。
经调研,是阿里云服务器将25号端口封闭,无法通过此端口向外发送数据,需向阿里云提交申请解封。另一种解决方式是将服务器发送数据的端口改为465。为了修改端口,以邮件发送为目的,我更新后的代码如下:
[1] 相关的配置文件信息如下:

spring.mail.protocol=smtp
spring.mail.host=smtp.163.com
spring.mail.port=465
spring.mail.username=salieri97@163.com
spring.mail.password=MSWLTEBIGNKFXQVW
spring.mail.default-encoding=utf-8
spring.mail.properties.mail.debug=true
spring.mail.sendAddressId=http://120.79.133.235:8989/salieri/user/activation

[2] 以注册账户激活为目的,邮件服务模块如下:

package com.salieri.service.serviceImpl;

import com.salieri.service.MailService;
import com.salieri.utils.TrustAllTrustManager;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.net.ssl.*;
import java.io.UnsupportedEncodingException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import java.util.Properties;

// 网易邮箱使用465端口发送邮件,默认的25端口在阿里云服务器被封
@Service
@Transactional
public class MailServiceImpl implements MailService {

    // 发送人的邮箱账号
    @Value("${spring.mail.username}")
    private String myEmailAccount;

    // 客户端授权密码
    @Value("${spring.mail.password}")
    private String myEmailPassword;

    // 网易163邮箱的SMTP服务器地址
    @Value("${spring.mail.host}")
    private String myEmailSMTPHost;

    private static final String title = "激活邮件";


    // 激活账号邮件并发送
    @Override
    public void sendMailForActivationAccount(String activationUrl, String name) throws KeyManagementException, NoSuchAlgorithmException {  // activationUrl是邮件发送内容,name是接收方邮箱名


        //  直接通过主机认证
        HostnameVerifier hv = new HostnameVerifier() {
            public boolean verify(String urlHostName, SSLSession session) {
                return true;
            }
        };
        //  配置认证管理器
        javax.net.ssl.TrustManager[] trustAllCerts = {new TrustAllTrustManager()};
        SSLContext sc = SSLContext.getInstance("SSL");
        SSLSessionContext sslsc = sc.getServerSessionContext();
        sslsc.setSessionTimeout(0);
        sc.init(null, trustAllCerts, null);
        HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());


        String emailMsg = "点击链接:"+activationUrl+" 进行账户激活--EasyBuy";

        // 1. 创建参数配置, 用于连接邮件服务器的参数配置
        // 参数配置
        Properties props = new Properties();
        // 使用的协议(JavaMail规范要求)
        props.setProperty("mail.transport.protocol", "smtp");
        // 发件人的邮箱的 SMTP 服务器地址
        props.setProperty("mail.smtp.host", myEmailSMTPHost);
        // 需要请求认证
        props.setProperty("mail.smtp.auth", "true");

        // SSLSocketFactory类的端口
        props.put("mail.smtp.socketFactory.port", "465");
        // SSLSocketFactory类
        props.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
        props.put("mail.smtp.auth", "true");
        // 网易提供的ssl加密端口,QQ邮箱也是该端口
        props.put("mail.smtp.port", "465");

        // 2. 根据配置创建会话对象, 用于和邮件服务器交互
        Session session = Session.getDefaultInstance(props);
        // 设置为debug模式, 可以查看详细的发送 log
        session.setDebug(true);

        // 1. 创建一封邮件
        MimeMessage message = new MimeMessage(session);
        try {
            // 2. From: 发件人
            message.setFrom(new InternetAddress(myEmailAccount, title, "UTF-8"));
            // 3. To: 收件人(可以增加多个收件人、抄送、密送)
            message.setRecipient(MimeMessage.RecipientType.TO, new InternetAddress(name, "个人网站", "UTF-8"));
            // 4. Subject: 邮件主题
            message.setSubject(title, "UTF-8");
            // 5. Content: 邮件正文(可以使用html标签)
            message.setContent(emailMsg, "text/html;charset=UTF-8");
            // 6. 设置发件时间
            message.setSentDate(new Date());
            // 7. 保存设置
            message.saveChanges();

            // 4. 根据Session获取邮件传输对象
            Transport transport = session.getTransport();
            // 5. 使用 邮箱账号 和 密码 连接邮件服务器, 这里认证的邮箱必须与 message 中的发件人邮箱一致, 否则报错
            transport.connect(myEmailAccount, myEmailPassword);
            // 6. 发送邮件, 发到所有的收件地址, message.getAllRecipients() 获取到的是在创建邮件对象时添加的所有收件人, 抄送人, 密送人
            transport.sendMessage(message, message.getAllRecipients());
            // 7. 关闭连接
            transport.close();

        } catch (MessagingException e) {
            e.printStackTrace();
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }

    }
}

[3] TrustAllTrustManager工具类代码如下:

package com.salieri.utils;

public class TrustAllTrustManager implements javax.net.ssl.TrustManager, javax.net.ssl.X509TrustManager {

    public java.security.cert.X509Certificate[] getAcceptedIssuers() {
        return null;
    }

    public boolean isServerTrusted(java.security.cert.X509Certificate[] certs) {
        return true;
    }

    public boolean isClientTrusted(java.security.cert.X509Certificate[] certs) {
        return true;
    }

    public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType)
            throws java.security.cert.CertificateException {
        return;
    }

    public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType)
            throws java.security.cert.CertificateException {
        return;
    }

}

二、Bug2: javax.net.ssl.SSLHandshakeException: No appropriate protocol

Bug2描述:

在解决Bug1后,客户端前端Vue-cli程序调用服务器端Java后端邮件接口时再次出现邮件发送不成功现象(在本地测试是通过的),查看服务器端SringBoot输出日志发现javax.net.ssl.SSLHandshakeException: No appropriate protocol报错。

Bug2处理:

通过调研,发现是证书问题,于是依据解决方案1修改服务器端openjdk1.8配置。由于我在服务器端通过Docker部署,在容器运行时修改了jdk配置文件,命令行执行过程如下:

docker ps
docker inspect 588cb39e4b9b   # 588cb39e4b9b是jdk1.8的容器编号,此命令是为了查看jdk1.8内部配置文件位置
docker exec -it 588cb39e4b9b bash  # 进入jdk1.8容器
cd /usr/local/openjdk-8/lib/security
apt-get update
apt-get install vim
vim java.security

修改的地方参考解决方案1,而后重启springboot的jar包+jdk.18组合成的容器,在本地前端调用服务器后端接口发送邮件成功。