欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

Java 编程系列] 使用 Java 访问 Microsoft Graph 并实现发送电子邮件的功能。

最编程 2024-10-02 07:18:06
...

1、前言

        微软与2022年10月1号,开始停止了部分服务的 basic auth (账密登录)功能,需要改用 oauth2.0 协议接入相应服务。邮件方面主要在于IMAP和pop协议。并且与2023年1月1日时,正式全面停止账密登录使用去接入上述服务功能。而对于smtp方面,目前还没有指出,还是可以继续使用账密登录的,而具体的不可用截止日期,目前并未明确指出。但官方还是建议,将smtp也尽早接入使用oauth2.0。

官网最新更新信息:


        基于以上原因,未雨绸缪的想法,担心不久的将来还是要改造,所以不如现在就先了解和实现如何接入oauth2.0去发送邮件吧。
        因此,今天我将记录下,我通过微软的Microsoft Graph API来实现邮件发送的服务功能,该api也是使用到了oauth2.0的接入,所以直接实现即可。

参考文档:

Basic Authentication and Exchange Online – September 2021 Update - Microsoft Community Hub

Basic Authentication Deprecation in Exchange Online – September 2022 Update - Microsoft Community Hub

弃用 Exchange Online 中的基本身份验证 | Microsoft Learn


2、Microsoft Graph接入实践

2.1 参考API

参考链接:

user: sendMail - Microsoft Graph v1.0 | Microsoft Learn

根据文档,会带你跳转进到依赖包的引入页面和身份认证示例代码的界面,如图:

以上,就是官方文档推荐的封装SDK API调用方式的流程。。。。

但是但是,,这一套操作下来,程序根本运行不起来,各种依赖包缺失或是ClassNotFound。

(试过多遍之后,实在无力吐槽,百度其他帖子,也有遇到这种情况)

所以,下面我所介绍的方式,是另外一种形式,通过Http的方式请求Graph Api!!!

另外一些前置准备工作,请按官方推荐步骤来:


2.2 引入依赖

<!-- 发邮件 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-mail</artifactId>
</dependency>

<dependency>
    <groupId>com.microsoft.azure</groupId>
    <artifactId>msal4j</artifactId>
    <version>1.10.1</version>
</dependency>

2.3 实现代码

package xxx.xxx.mail.helper;

import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import com.microsoft.aad.msal4j.ClientCredentialFactory;
import com.microsoft.aad.msal4j.ClientCredentialParameters;
import com.microsoft.aad.msal4j.ConfidentialClientApplication;
import com.microsoft.aad.msal4j.IAuthenticationResult;
import xxx.xxx.constants.DateConstant;
import xxx.xxx.constants.PunctuationMarkConstant;
import xxx.xxx.util.DateUtils;
import xxx.xxx.util.StringUtils;
import xxx.xxx.mail.bean.po.MicrosoftMailConfig;
import xxx.xxx.mail.dao.MicrosoftMailConfigMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.mail.internet.MimeMessage;
import java.io.ByteArrayOutputStream;
import java.util.Base64;
import java.util.Collections;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;


@Slf4j
@Component
public class Oauth2GraphHelper {

    private ConcurrentHashMap<String,IAuthenticationResult> tokenMap = new ConcurrentHashMap<>();

    @Autowired
    private MicrosoftMailConfigMapper microsoftMailConfigMapper;

    @Value("${graph.authUrl:https://login.microsoftonline.com/%s/oauth2/v2.0/token}")
    private String authUrl;

    @Value("${graph.sendUrl:https://graph.microsoft.com/v1.0/users/%s/sendMail}")
    private String sendRequestUrl;

    //获取oauth2所需token
    private String getToken(String from) throws Exception {

        //根据发件邮箱查询对应的配置
        MicrosoftMailConfig mailConfig = microsoftMailMapper.selectConfigBySendMail(from);
        if(Objects.isNull(mailConfig)){
            return null;
        }

        //通过配置获取对应邮箱的token
        synchronized (this) {
            String tenantId = mailConfig.getTenantId();
            String key = String.join(PunctuationMarkConstant.PUNCTUATION_VERTICAL_LINE,tenantId,mailConfig.getClientId());
            IAuthenticationResult authenticationResult = tokenMap.get(key);

            if (authenticationResult != null && StringUtils.isNotEmpty(authenticationResult.accessToken())
                    && !isExpires(authenticationResult)) {
                return authenticationResult.accessToken();
            }

            String clientAuthUrl = String.format(authUrl,tenantId);
            ConfidentialClientApplication app = ConfidentialClientApplication.builder(mailConfig.getClientId(), ClientCredentialFactory.createFromSecret(mailConfig.getClientSecret()))
                    .authority(clientAuthUrl).build();
            ClientCredentialParameters clientCredentialParam = ClientCredentialParameters.builder(Collections.singleton("https://graph.microsoft.com/.default")).build();
            CompletableFuture<IAuthenticationResult> future = app.acquireToken(clientCredentialParam);
            IAuthenticationResult iAuthenticationResult = future.get();

            if(Objects.nonNull(iAuthenticationResult)){
                tokenMap.put(key,iAuthenticationResult);
                log.info("MircoSoft Graph Token expiresOnDate {}  {} ",tenantId, DateUtils.formatDate(iAuthenticationResult.expiresOnDate(), DateConstant.DATE_FORMAT_YMD_HMS));
                return iAuthenticationResult.accessToken();
            }

        }

        return null;
    }


    //通过Graph api发送邮件
    public void sendMailByGraphApi(String from,MimeMessage mimeMessage){

        boolean succ = true;
        try {
            String token = getToken(from);
            if(StringUtils.isEmpty(token)){
                log.warn("Graph api obtain token failed !!! By {}",from);
                return;
            }

            String msgBase64 = convertMessageToBase64Str(mimeMessage);
            String reqUrl = String.format(sendRequestUrl,from);
            HttpResponse response = HttpUtil.createPost(reqUrl)
                    .header("Content-Type","text/plain")
                    .header("Authorization","Bearer "+token)
                    .body(msgBase64)
                    .execute();

            if(response.isOk()){
                log.info("Graph api send email by {} , status : {}",from,response.getStatus());
            }else{
                succ = false;
            }

        } catch (Exception e) {
            log.error("",e);
            succ = false;
        }finally {
            if(!succ){
                //可以发送告警信息
                RobotUtil.sendWarnMessage("Graph API failed to send email !!! By {} ",from);
            }
        }

    }

    private boolean isExpires(IAuthenticationResult authenticationResult) {
        long currentTimeMillis = System.currentTimeMillis();
        long time = authenticationResult.expiresOnDate().getTime();
        return time < currentTimeMillis + 1000 * 60 * 10;
    }

    private String convertMessageToBase64Str(MimeMessage mimeMessage) throws Exception {
        final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        mimeMessage.writeTo(outputStream);
        final byte[] bytes = outputStream.toByteArray();

        String msgBase64 = Base64.getEncoder().encodeToString(bytes);
        return msgBase64;
    }

}

        简单说明下,authUrl中的占位符需要替换成上述自己申请的 tenantId ,而sendRequestUrl中的占位符需要替换成 发件邮箱  ,如此就可以通过graph api发送邮件啦。。


3、总结

        目前整体流程上,个人觉得比较坑的就是官方的SDK封装的API,我是没用使用成功的。感兴趣的伙伴,可以自行尝试看看,有问题欢迎来交流,就分享到这里啦~~~

推荐阅读