微软图形集成中常见的错误以及如何解决这些错误(第一部分)
本文记录了微软图形集成到商业应用中可能出现的常见集成错误,以及处理这些错误的方法。
随着2007年Exchange Server的发布,微软也引入了Exchange Web服务(EWS)。这些基于SOAP的API允许开发者通过互联网访问微软Exchange产品,如日历、联系人等。作为Office 365产品发布的一部分,微软还在2015年发布了新的基于REST的Office 365统一API,后来被称为MS Graph APIs。微软在2018年宣布,将不再积极开发EWS APIs。Exchange网络服务不再符合当今的安全和维护要求。出于这个原因,各种EWS APIs将从2022年开始逐步关闭,并由MS Graph APIs所取代。关于已经可以通过MS Graph APIs访问的服务的概述,可以在微软当前的文档中找到。由于切换到MS Graph,各公司必须调整他们的数字产品。
在本系列文章中,我们将讨论在使用MS Graph Java SDK向Microsoft Graph REST API v1.0过渡过程中的一些经验教训。
在Outlook事件中附加大文件(受众声称值无效)
根据微软的文档,只有大小不超过150MB的附件可以被上传和附加到事件中。大小不超过3MB的附件可以通过一个 POST呼叫。对于3MB和150MB之间的附件,会产生一个上传会话。在会话中,附件是通过多个PUT 调用而逐件上传的。对于第二种情况,请考虑下面的例子。
Java
final String primaryUser = "office365userEmail";
final String eventID = "eventID";
final GraphServiceClient<Request> graphClient = GraphServiceClient
.builder()
.authenticationProvider(getAuthenticationProvider())
.buildClient();
final File file = File.createTempFile("testFile", "txt");
FileUtils.writeByteArrayToFile(file, "testContent".getBytes());
InputStream fileStream = new FileInputStream(file);
final AttachmentItem attachmentItem = new AttachmentItem();
attachmentItem.attachmentType = AttachmentType.FILE;
attachmentItem.name = file.getName();
attachmentItem.size = file.getTotalSpace();
final AttachmentCreateUploadSessionParameterSet attachmentCreateUploadSessionParameterSet = AttachmentCreateUploadSessionParameterSet
.newBuilder()
.withAttachmentItem(attachmentItem)
.build();
final UploadSession uploadSession = graphClient
.users(primaryUser)
.events(evendID)
.attachments()
.createUploadSession(attachmentCreateUploadSessionParameterSet)
.buildRequest()
.post();
// Called after each slice of the file is uploaded
final IProgressCallback callback = (current, max) -> System.out.println("Uploaded "+ current + " bytes of " + max + " total bytes");
final LargeFileUploadTask<AttachmentItem> uploadTask = new LargeFileUploadTask<>(uploadSession, graphClient, fileStream, file.length(), AttachmentItem.class);
// upload (default: chunkSize is 5 MB)
uploadTask.upload(0, null, callback);
在这个例子中,你可以看到,首先,一个uploadSession ,通过一个POST 请求创建。对POST 请求的可能的响应可能是这样的。
HTML
HTTP/1.1 201 Created Content-type: application/json
{ "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#microsoft.graph.uploadSession",
"uploadUrl": "https://outlook.office.com/api/gv1.0/users('a8e8e219-4931-95c1-b73d-62626fd79c32@72aa88bf-76f0-494f-91ab-2d7cd730db47')/events('AAMkAGUwNjQ4ZjIxLTQ3Y2YtNDViMi1iZjc4LTMA=')/AttachmentSessions('AAMkAGUwNjQ4ZjIxLTAAA=')?authtoken=eyJhbGciOiJSUzI1NiIsImtpZCI6IjFTeXQ1bXdXYVh5UFJ",
"expirationDateTime": "2021-12-27T14:20:12.9708933Z",
"nextExpectedRanges": [ "0-" ] }
作为对POST 请求的响应,你会得到一个上传链接uploadURL ,它可以用来上传附件。这里变得很清楚的是,附件的上传是通过Outlook API完成的,而不是通过MS Graph API!
如果你开始上传 (uploadTask.upload(0, null, callback)),那么你可能会出现以下错误。
纺织品
401 : Unauthorized upon request. com.microsoft.graph.core.ClientException:
Error code: InvalidAudienceForResource Error message:
The audience claim value is invalid for current resource.
Audience claim is 'https://graph.microsoft.com/', request url is
'https://outlook.office.com/api/v2.0/Users...'.
为了更详细地了解这个错误,请考虑以下MS Graph Java SDK的AuthenticationHandler 的代码片断。
Java
CompletableFuture<String> future = this.authProvider.getAuthorizationTokenAsync(originalRequest.url().url());
String accessToken = (String)future.get();
return accessToken == null
? chain.proceed(originalRequest)
: chain.proceed(originalRequest.newBuilder().addHeader("Authorization", "Bearer " + accessToken).build());
对于每个通过graphClient 的请求,都会附加一个带有MS Graph API的访问令牌的授权头。此外,对于每个通过上传链接发送的PUT 请求,uploadURL
被发送到Outlook API。这导致了授权问题,因为认证凭证对Outlook API来说是无效的(返回代码401)。
一个可能的解决方案是定制认证提供者,以避免受众要求值的问题。要做到这一点,考虑这个例子:对于客户端graphClient ,getAuthenticationProvider() 方法定义了提供者。这个方法的以下实现产生了一个提供者,它首先检查请求的主机是否是MS Graph API。如果不是,那么就会返回一个带有null 值的CompletableFuture 。
爪哇
private IAuthenticationProvider getAuthenticationProvider() {
return new IAuthenticationProvider() {
private String hostNameToCheck = "graph";
@Override
public CompletableFuture<String> getAuthorizationTokenAsync(URL requestUrl) {
CompletableFuture<String> future = new CompletableFuture<>();
if(requestUrl.getHost().toLowerCase().contains(hostNameToCheck)){
future.complete(getToken());
} else {
future.complete(null);
}
return future;
}
};
}
由于返回的是null 值,所以对MS Graph API的每个请求都附加了一个授权头,对Outlook API的每个请求都保持不变。由于这个原因,InvalidAudienceForResource 的错误可以避免。
从这个例子中我们可以看出,这个问题不是由服务库引起的:是由授权库引起的。这个问题是已知的,而且预计在不久的将来也不会被修复。由于这个原因,这个变通方法成为一个永久性的解决方案。
阅读有两个传真号码的Outlook联系人 (缺少SingleValueLegacyExtendedProperty)。
根据联系人的设计,没有预见到使用Microsoft Graph REST API v1.0可以为联系人创建专业或私人传真号码。
然而,MS Graph API允许用自定义数据字段扩展资源运行实例的数据模型的可能性。考虑下面的例子,一个联系人被生成并存储有专业和个人的传真号码。
Java
public class FaxNumbersSample implements SampleClass {
private static final String PRIVATE_FAX_NUMBER = "privateFaxNumber";
private static final String BUSINESS_FAX_NUMBER = "businessFaxNumber";
private static final String PUBLIC_GUID_STRING = "00020329-0000-0000-c000-000000000046";
private static final String NAME = "} Name ";
@Override
public void run() {
final String primaryUser = "office365userEmail";
final String REQUEST_URI = "https://graph.microsoft.com/v1.0/users/"
+ primaryUser
+ "/contacts"
+ "/";
final GraphServiceClient<okhttp3.Request> graphClient =
GraphServiceClient
.builder()
.authenticationProvider(getAuthenticationProvider())
.buildClient();
Contact contact = new Contact();
List<SingleValueLegacyExtendedProperty> pageContents = new ArrayList<>();
pageContents.add(setProperty(BUSINESS_FAX_NUMBER, "001"));
pageContents.add(setProperty(PRIVATE_FAX_NUMBER, "002"));
if (!pageContents.isEmpty()) {
contact.singleValueExtendedProperties =
new SingleValueLegacyExtendedPropertyCollectionPage(pageContents,
new SingleValueLegacyExtendedPropertyCollectionRequestBuilder(
REQUEST_URI, graphClient, new ArrayList<>()));
}
contact.givenName = "Pavel";
contact.surname = "Bansky";
LinkedList<EmailAddress> emailAddressesList = new LinkedList<EmailAddress>();
EmailAddress emailAddresses = new EmailAddress();
emailAddresses.address = "pavelb@fabrikam.onmicrosoft.com";
emailAddresses.name = "Pavel Bansky";
emailAddressesList.add(emailAddresses);
contact.emailAddresses = emailAddressesList;
LinkedList<String> businessPhonesList = new LinkedList<String>();
businessPhonesList.add("+1 732 555 0102");
contact.businessPhones = businessPhonesList;
//response to the POST request doesn't contain the extra properties
contact = graphClient
.users(primaryUser)
.contacts()
.buildRequest()
.post(contact);
//to get all properties we need to expand the GET request
contact = graphClient
.users(primaryUser)
.contacts(contact.id)
.buildRequest()
.expand(new StringBuilder()
.append("singleValueExtendedProperties($filter=id eq 'String {")
.append(PUBLIC_GUID_STRING)
.append(NAME)
.append(PRIVATE_FAX_NUMBER)
.append("'")
.append(" or id eq 'String {")
.append(PUBLIC_GUID_STRING)
.append(NAME)
.append(BUSINESS_FAX_NUMBER)
.append("'")
.append(" or id eq 'String {")
.append(PUBLIC_GUID_STRING)
.append(NAME)
.append(ACADEMIC_TITLE)
.append("')").toString())
.get();
}
private SingleValueLegacyExtendedProperty setProperty(String propertyName, String propertyValue) {
final SingleValueLegacyExtendedProperty property = new SingleValueLegacyExtendedProperty();
property.id = new StringBuilder()
.append("String {")
.append(PUBLIC_GUID_STRING)
.append(NAME)
.append(propertyName).toString();
property.value = propertyValue;
return property;
}
@Override
public IAuthenticationProvider getAuthenticationProvider() {
return new IAuthenticationProvider() {
private String hostNameToCheck = "graph";
@Override
public CompletableFuture<String> getAuthorizationTokenAsync(URL requestUrl) {
CompletableFuture<String> future = new CompletableFuture<>();
if(requestUrl.getHost().toLowerCase().contains(hostNameToCheck)){
future.complete(getToken());
} else{
future.complete(null);
}
return future;
}
};
}
@Override
public String getToken() {
return null;
}
}
这个例子表明,标准的数据模型可以通过额外的属性(SingleValueLegacyExtendedProperty )来扩展。当创建额外的数据字段时,必须遵守ID的固定格式。这可以看起来如下。
纯文本
String {00020329-0000-0000-c000-000000000046} Name faxNumber
因此,00020329-0000-0000-c000-0000000046 ,称为GUID,可以用于一般的所有字符。请注意:POST 请求的返回值不包含额外的字段。如果你想读取所有字段,那么你必须通过一个额外的扩展GET 请求来实现。 在上面的例子中,GET 请求被以下过滤器所扩展。
纯文本
singleValueExtendedProperties($filter=id eq 'String {00020329-0000-0000-c000-000000000046} Name businessFaxNumber' or id eq 'String {00020329-0000-0000-c000-000000000046} Name privateFaxNumber'
虽然默认情况下,传真号码不能通过Microsoft GraphREST APIv1.0创建,但SingleValueLegacyExtendedProperty 类允许存储传真号码。这种解决路径的缺点是传真号码不会自动显示在Outlook中。然而,微软计划在未来将传真号码纳入MS Graph的默认模型中,这不仅会简化传真号码的存储,而且传真号码也会自动显示在Outlook中。