Office365发送和接收邮件认证模式升级

为了提升安全性,Office365升级了邮件客户端认证模式,需要使用OAuth 2.0认证,废弃以前老的用户名和密码的方式。因此以前用JavaMail开发的邮件发送和接收功能将不能使用,比如从其他邮件服务迁移到Office365,必须用新的方式修改相关代码。

升级说明

微软官方说明地址:https://learn.microsoft.com/zh-cn/exchange/troubleshoot/administration/cannot-connect-mailbox-pop-imap-outlook?source=recommendations

https://learn.microsoft.com/zh-cn/exchange/clients-and-mobile-in-exchange-online/deprecation-of-basic-authentication-exchange-online#pop-imap-and-smtp-auth

建议租户禁用基本身份验证,并迁移到新式客户端的新式身份验证租户。也就是【现代身份验证(基于 OAuth 2.0 令牌的身份验证)】

OAuth 2.0认证

OAuth 2.0认证模式有四种模式

  1. 客户端认证模式(POST)

  2. 资源密码模式(POST)

    1. https://xxxxxxxx/oauth/token?grant_type=password&client_id=xxxx&client_secret=xxxx&username=username&password=password
    2. 直接用用户的用户名和密码授权,一般是内部子系统使用
  3. 隐式授权模式(GET)

    1. http://xxxxxxxx/oauth/authorize?response_type=token&client_id=xxxx
    2. 授权后redirect带回access_token,安全性比授权码模式低
  4. 授权码模式(需要两步,常见且安全性较高)

了解更多OAuth2https://learn.microsoft.com/zh-cn/azure/active-directory/develop/active-directory-v2-protocols

提前准备

目前似乎是保留了SMTP模式的基本身份验证:

在 2022 年 10 月 1 日永久禁用基本身份验证时,SMTP 身份验证仍可用。 SMTP 仍然可用的原因是,许多多功能设备(如打印机和扫描仪)无法更新为使用新式身份验证。 但是,我们强烈建议客户尽可能不使用基本身份验证和 SMTP AUTH。 发送经过身份验证的邮件的其他选项包括使用替代协议,例如 Microsoft 图形 API

首先应该要注册APP:https://learn.microsoft.com/zh-cn/azure/active-directory/develop/quickstart-register-app

由于是服务器后台发送和接收邮件,是预设的邮件地址和账号,因此选用client_credentials模式,在Office365上配置好相应的APP,并得到tenantclientIdclientSecret

身份验证介绍:https://learn.microsoft.com/zh-cn/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth

主要有下面三个配置项需要用到:

应用程序(客户端) ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx

目录(租户) ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx

clientSecret (12个月):xxxx~xxxxxxxxxxxxxxxxxxxxxx

服务地址

配置服务地址:https://support.microsoft.com/zh-cn/office/pop-imap-%E5%92%8C-smtp-%E8%AE%BE%E7%BD%AE-8361e398-8af4-4e97-b147-6c6c4ac95353

Email提供程序 IMAP 设置 POP 设置 SMTP 设置
Microsoft 365
Outlook
Hotmail
Live.com
服务器:outlook.office365.com
端口:993
加密:SSL/TLS
服务器:outlook.office365.com
端口:995
加密:SSL/TLS
服务器:smtp.office365.com
端口:587
加密:STARTTLS

Java客户端升级

JavaMail客户端需要升级:

文档地址:https://javaee.github.io/javamail/OAuth2

升级到1.5.5之后,最新应该是1.6.2

不支持pop3

JavaMail不支持POP3传输OAuth2令牌,只能使用IMAP协议

image-20230302111139232

使用pop3协议执行会报错误:

javax.mail.AuthenticationFailedException: Protocol error. Connection is closed. 10

at com.sun.mail.pop3.POP3Store.protocolConnect(POP3Store.java:213)
at javax.mail.Service.connect(Service.java:366)
at javax.mail.Service.connect(Service.java:246)

实际代码

简单依赖,httpclientslf4j日志、jacksonjson解析器、以及javax.mail

<dependencies>
    <dependency>
        <groupId>org.apache.httpcomponents</groupId>
        <artifactId>httpclient</artifactId>
        <version>4.5.14</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.14.2</version>
    </dependency>
    <dependency>
        <groupId>com.sun.mail</groupId>
        <artifactId>javax.mail</artifactId>
        <version>1.6.2</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>1.7.36</version>
    </dependency>
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.2.11</version>
    </dependency>
</dependencies>

测试代码:

public class Office365MailTest {

    private static final Logger LOGGER = LoggerFactory.getLogger(Office365MailTest.class);

    private static final String SMTP_SERVER_ADDR = "smtp.office365.com";
    private static final int SMTP_SERVER_PORT = 587;

    private static final String IMAP_SERVER_ADDR = "outlook.office365.com";
    private static final int IMAP_SERVER_PORT = 993;

    private static final String USER_NAME = "xxxx@xxxx.com";

    private static final String MAIL_FROM = "xxxx@xxxx.com";
    private static final String MAIL_TO = "xxxx@xxxx.com";

    private static final String MAIL_SUBJECT = "Test测试邮件标题";
    private static final String MAIL_CONTENT = "Test测试邮件正文";

    private static final String TENANT_ID = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx";
    private static final String CLIENT_ID = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx";
    private static final String CLIENT_SECRET = "xxxxxxxxxxxxxxxxxxxxxxxxx";

    public static void main(String[] args) throws Exception {
//        sendEmail(false);
        recvMail(false);
    }
    private static void sendEmail(boolean debug) throws Exception {
        String protocol = "smtp";
        Properties props = getBaseProperties(protocol, SMTP_SERVER_ADDR, SMTP_SERVER_PORT, debug);
        final Session session = Session.getInstance(props);
        try {
            final Message message = new MimeMessage(session);
            message.setRecipient(Message.RecipientType.TO, new InternetAddress(MAIL_TO));
            message.setFrom(new InternetAddress(MAIL_FROM));
            message.setSubject(MAIL_SUBJECT);
            message.setText(MAIL_CONTENT);
            message.setSentDate(new Date());
            Transport transport = session.getTransport(protocol);
            transport.connect(SMTP_SERVER_ADDR, USER_NAME, getAuthToken(TENANT_ID, CLIENT_ID, CLIENT_SECRET));
            transport.send(message);
        } catch (final MessagingException ex) {
            LOGGER.error("邮件发送错误", ex);
        }
    }
    private static void recvMail(boolean debug) throws Exception {
        String protocol = "imap";
        Properties props = getBaseProperties(protocol, IMAP_SERVER_ADDR, IMAP_SERVER_PORT, debug);
        props.put("mail.store.protocol", protocol);
        String token = getAuthToken(TENANT_ID, CLIENT_ID, CLIENT_SECRET);
        Session session = Session.getInstance(props);
        Store store = session.getStore(protocol);
        store.connect(IMAP_SERVER_ADDR, USER_NAME, token);
        Folder folder = store.getFolder("INBOX"); // 读inbox
        folder.open(Folder.READ_WRITE);
        Message[] messages = folder.getMessages();
        for (Message message : messages) {
            LOGGER.info("{}", message.getSubject());
        }
    }
    private static Properties getBaseProperties(String protocol, String address, Integer port, boolean debug) {
        Properties props = new Properties();
        props.put("mail." + protocol + ".host", address);
        props.put("mail." + protocol + ".port", port);
        if ("smtp".equals(protocol)) {
            props.put("mail." + protocol + ".starttls.enable", "true");
        } else {
            props.put("mail." + protocol + ".ssl.enable", "true");
        }
        props.put("mail." + protocol + ".ssl.protocols", "TLSv1.2");
        props.put("mail." + protocol + ".auth", "true");
        props.put("mail." + protocol + ".auth.mechanisms", "XOAUTH2");
        if (debug) { // 调试模式
            props.put("mail.debug", "true");
            props.put("mail.debug.auth", "true");
        }
        return props;
    }
    private static String getAuthToken(String tenant, String clientId, String clientSecret) throws Exception {
        CloseableHttpClient client = null;
        CloseableHttpResponse loginResponse = null;
        try {
            SSLContext sslContext = SSLContexts.custom().loadTrustMaterial(null, (cert, authType) -> true).build();
            SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
            client = HttpClients.custom().setSSLSocketFactory(sslSocketFactory).build();
            HttpPost loginPost = new HttpPost("https://login.microsoftonline.com/" + tenant + "/oauth2/v2.0/token");
            String scopes = "https://outlook.office365.com/.default";
            String encodedBody = "client_id=" + clientId + "&scope=" + scopes + "&client_secret=" + clientSecret
                    + "&grant_type=client_credentials";
            loginPost.setEntity(new StringEntity(encodedBody, ContentType.APPLICATION_FORM_URLENCODED));
            loginPost.addHeader(new BasicHeader("cache-control", "no-cache"));
            loginResponse = client.execute(loginPost);
            InputStream inputStream = loginResponse.getEntity().getContent();
            Map<String, String> parsed = new ObjectMapper().readValue(inputStream, Map.class);
            return parsed.get("access_token");
        } finally {
            HttpClientUtils.closeQuietly(client);
            HttpClientUtils.closeQuietly(loginResponse);
        }
    }
}

收邮件成功:

16:26:41.567 [main] INFO com.fugary.mail.Office365MailTest - test标题
16:26:42.189 [main] INFO com.fugary.mail.Office365MailTest - test
16:26:42.811 [main] INFO com.fugary.mail.Office365MailTest - 最新测试版邮件发送

常见错误

  1. IMAP错误

javax.mail.MessagingException: A3 BAD User is authenticated but not connected.
;
nested exception is:
com.sun.mail.iap.BadCommandException: A3 BAD User is authenticated but not connected.

问题一般是用户没有权限,可以配置下权限或者换一个有权限的用户。

  1. SMTP错误

javax.mail.AuthenticationFailedException: 535 5.7.139 Authentication unsuccessful, SmtpClientAuthentication is disabled for the Tenant. Visit https://aka.ms/smtp_auth_disabled for more information. [SJ0PR03CA0292.namprd03.prod.outlook.com 2023-03-02T08:56:02.982Z 08DB1AFAC2F526FF]

at com.sun.mail.smtp.SMTPTransport$Authenticator.authenticate(SMTPTransport.java:965)
at com.sun.mail.smtp.SMTPTransport.authenticate(SMTPTransport.java:876)
at com.sun.mail.smtp.SMTPTransport.protocolConnect(SMTPTransport.java:780)
at javax.mail.Service.connect(Service.java:366)
at javax.mail.Service.connect(Service.java:246)
at com.fugary.mail.Office365MailTest.sendEmail(Office365MailTest.java:66)
at com.fugary.mail.Office365MailTest.main(Office365MailTest.java:50)

没有开启SMTP客户端。

评论

  1. sain
    9月前
    2023-8-18 8:17:03

    您好,我想问一下如果通过页面跳转方式认证,应该如何实现,能出一篇文章讲解吗?微软官方文档对这块没有写的特别清楚,看起来也需要事先注册分配好租户,但实际上我们用foxmail或者QQ邮箱登录时,并没有让你去填写租户id,他们是如何实现的呢

  2. zxc
    1年前
    2023-4-24 20:12:11

    我的程序跟你没有太大差别,
    Store store = session.getStore(protocol);
    store.connect(IMAP_SERVER_ADDR, USER_NAME, token);、
    链接时一直报 A1 NO AUTHENTICATE failed.
    javax.mail.AuthenticationFailedException: AUTHENTICATE failed.
    新版api权限已经不能手动添加:https://outlook.office365.com/IMAP.AccessAsUser.All 权限,请问有遇到这个问题吗?

    • gary
      博主
      zxc
      1年前
      2023-4-24 21:45:43

      权限问题一般找管理员开

      • zxc
        gary
        1年前
        2023-4-24 22:38:24

        2.Install ExchangeOnlineManagement Install-Module -Name ExchangeOnlineManagement -allowprerelease Import-module ExchangeOnlineManagement Connect-ExchangeOnline -Organization
        3.Register Service Principal in Exchange: 1.New-ServicePrincipal -AppId -ServiceId [-Organization ] Make sure to use ObjectId from enterprise applications rather than object id of application registration.
        For the same application you registered in Application Registration. A corresponding application has been created in Enterprise Application as well. You need to pass object id from there while registering service principal in Exchange: User’s image
        2.Get-ServicePrincipal | fl
        3.Add-MailboxPermission -Identity “[john.smith@contoso.com]” -User -AccessRights FullAccess
        我在操作这些命令时powershell一直报错,你有遇到过类似问题吗?或者管理员应该怎么操作呢我的客户如果开通这些权限应该怎么操作这几个命令呢?

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇