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,我是没用使用成功的。感兴趣的伙伴,可以自行尝试看看,有问题欢迎来交流,就分享到这里啦~~~