微软图形集成中常见的错误以及如何解决这些错误(第一部分)

413 阅读3分钟

微软图形集成中常见的错误以及如何解决这些错误(第一部分)

本文记录了微软图形集成到商业应用中可能出现的常见集成错误,以及处理这些错误的方法。

随着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)。

一个可能的解决方案是定制认证提供者,以避免受众要求值的问题。要做到这一点,考虑这个例子:对于客户端graphClientgetAuthenticationProvider() 方法定义了提供者。这个方法的以下实现产生了一个提供者,它首先检查请求的主机是否是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中。