Создание функций Azure с помощью microsoft Graph
В этом руководстве рассказывается о создании функции Azure, которая использует API microsoft Graph для получения сведений о календаре для пользователя.
Совет
Если вы предпочитаете просто скачать завершенный учебник, вы можете скачать или клонировать GitHub репозиторий. Инструкции по настройке приложения с помощью ID-приложения и секрета см. в файле README в демо-папке.
Предварительные требования
Перед началом этого учебного пособия на компьютере разработки должны быть установлены следующие средства.
Вы также должны иметь учетную запись Microsoft work или school с доступом к глобальной учетной записи администратора в той же организации. Если у вас нет учетной записи Майкрософт, вы можете зарегистрироваться в программе Microsoft 365 разработчика, чтобы получить бесплатную Office 365 подписку.
Примечание
Этот учебник был написан со следующими версиями вышеуказанных инструментов. Действия в этом руководстве могут работать с другими версиями, но они не были проверены.
- .NET Core SDK 5.0.203
- Основные средства Azure Functions 3.0.3442
- Azure CLI 2.23.0
- ngrok 2.3.40
Отзывы
Обратите внимание на этот учебник в репозитории GitHub.
Создание проекта Azure Functions
В этом руководстве будет создаваться простая функция Azure, которая реализует триггерные функции HTTP, которые вызывают Microsoft Graph. Эти функции будут охватывать следующие сценарии:
- Реализует API для доступа к почтовому ящику пользователя с помощью проверки подлинности потока от имени потока.
- Реализует API для подписки и отписки уведомлений в почтовом ящике пользователя с помощью проверки подлинности потока предоставления клиентских учетных данных.
- Реализует веб-сайт для получения уведомлений об изменениях от Microsoft Graph доступа к данным с помощью потока предоставления клиентских учетных данных.
Вы также создайте простое одно страница JavaScript-приложение (SPA), чтобы вызвать API, реализованные в Функции Azure.
Создание проекта Azure Functions
Откройте интерфейс командной строки (CLI) в каталоге, в котором необходимо создать проект. Выполните следующую команду.
func init GraphTutorial --worker-runtime dotnetisolated
Измените текущий каталог в CLI на каталог GraphTutorial и запустите следующие команды, чтобы создать три функции в проекте.
func new --name GetMyNewestMessage --template "HTTP trigger" func new --name SetSubscription --template "HTTP trigger" func new --name Notify --template "HTTP trigger"
Откройте local.settings.json и добавьте в файл следующее, чтобы разрешить CORS
http://localhost:8080
из URL-адреса тестового приложения."Host": { "CORS": "http://localhost:8080" }
Запустите следующую команду для локального запуска проекта.
func start
Если все работает, вы увидите следующий вывод:
Functions: GetMyNewestMessage: [GET,POST] http://localhost:7071/api/GetMyNewestMessage Notify: [GET,POST] http://localhost:7071/api/Notify SetSubscription: [GET,POST] http://localhost:7071/api/SetSubscription
Убедитесь, что функции работают правильно, открыв браузер и просматривая URL-адреса функций, показанные на выходе. Вы должны увидеть следующее сообщение в браузере:
Welcome to Azure Functions!
.
Создание одно-страницного приложения
Откройте CLI в каталоге, в котором необходимо создать проект. Создайте каталог с именем TestClient для удержания файлов HTML и JavaScript.
Создайте новый файл сindex.html в каталоге TestClient и добавьте следующий код.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no"> <title>Azure Functions Graph Tutorial Test Client</title> <link rel="shortcut icon" href="g-raph.png"> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous"> <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.12.1/css/all.css" crossorigin="anonymous"> <link href="style.css" rel="stylesheet" type="text/css" /> </head> <body> <nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark"> <div class="container"> <a href="/" class="navbar-brand">Azure Functions Graph Test Client</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarCollapse"> <ul id="authenticated-nav" class="navbar-nav mr-auto"></ul> <ul class="navbar-nav justify-content-end"> <li class="nav-item"> <a class="nav-link" href="https://developer.microsoft.com/graph/docs/concepts/overview" target="_blank"> <i class="fas fa-external-link-alt mr-1"></i>Docs </a> </li> <li id="account-nav" class="nav-item"></li> </ul> </div> </div> </nav> <main id="main-container" role="main" class="container"> </main> <!-- Bootstrap/jQuery --> <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script> <!-- MSAL --> <script src="https://alcdn.msauth.net/browser/2.0.0/js/msal-browser.min.js" integrity="sha384-n3aacu1eFuIAfS3ZY4WGIZiQG/skqpT+cbeqIwLddpmMWcxWZwYdt+F0PgKyw+m9" crossorigin="anonymous"></script> <script src="config.js"></script> <script src="ui.js"></script> <script src="auth.js"></script> <script src="azurefunctions.js"></script> </body> </html>
Это определяет основные макеты приложения, в том числе панели навигации. Кроме того, добавляется следующее:
- Bootstrap и поддерживаемый JavaScript
- FontAwesome
- Библиотека проверки подлинности Майкрософт для JavaScript (MSAL.js) 2.0
Совет
Страница включает в себя favicon, (
<link rel="shortcut icon" href="g-raph.png">
). Эту строку можно удалить или скачать g-raph.pngиз GitHub.Создайте новый файл style.css в каталоге TestClient и добавьте следующий код.
body { padding-top: 70px; }
Создайте новый файл сui.js в каталоге TestClient и добавьте следующий код.
// Select DOM elements to work with const authenticatedNav = document.getElementById('authenticated-nav'); const accountNav = document.getElementById('account-nav'); const mainContainer = document.getElementById('main-container'); const Views = { error: 1, home: 2, message: 3, subscriptions: 4 }; // Helper function to create an element, set class, and add text function createElement(type, className, text) { const element = document.createElement(type); element.className = className; if (text) { const textNode = document.createTextNode(text); element.appendChild(textNode); } return element; } // Show the navigation items that should only show if // the user is signed in function showAuthenticatedNav(user, view) { authenticatedNav.innerHTML = ''; if (user) { // Add message link const messageNav = createElement('li', 'nav-item'); const messageLink = createElement('button', `btn btn-link nav-link${view === Views.message ? ' active' : '' }`, 'Latest Message'); messageLink.setAttribute('onclick', 'getLatestMessage();'); messageNav.appendChild(messageLink); authenticatedNav.appendChild(messageNav); // Add subscriptions link const subscriptionNav = createElement('li', 'nav-item'); const subscriptionLink = createElement('button', `btn btn-link nav-link${view === Views.message ? ' active' : '' }`, 'Subscriptions'); subscriptionLink.setAttribute('onclick', `updatePage(${Views.subscriptions});`); subscriptionNav.appendChild(subscriptionLink); authenticatedNav.appendChild(subscriptionNav); } } // Show the sign in button or the dropdown to sign-out function showAccountNav(user) { accountNav.innerHTML = ''; if (user) { // Show the "signed-in" nav accountNav.className = 'nav-item dropdown'; const dropdown = createElement('a', 'nav-link dropdown-toggle'); dropdown.setAttribute('data-toggle', 'dropdown'); dropdown.setAttribute('role', 'button'); accountNav.appendChild(dropdown); const userIcon = createElement('i', 'far fa-user-circle fa-lg rounded-circle align-self-center'); userIcon.style.width = '32px'; dropdown.appendChild(userIcon); const menu = createElement('div', 'dropdown-menu dropdown-menu-right'); dropdown.appendChild(menu); const userName = createElement('h5', 'dropdown-item-text mb-0', user); menu.appendChild(userName); const divider = createElement('div', 'dropdown-divider'); menu.appendChild(divider); const signOutButton = createElement('button', 'dropdown-item', 'Sign out'); signOutButton.setAttribute('onclick', 'signOut();'); menu.appendChild(signOutButton); } else { // Show a "sign in" button accountNav.className = 'nav-item'; const signInButton = createElement('button', 'btn btn-link nav-link', 'Sign in'); signInButton.setAttribute('onclick', 'signIn();'); accountNav.appendChild(signInButton); } } // Renders the home view function showWelcomeMessage(user) { // Create jumbotron const jumbotron = createElement('div', 'jumbotron'); const heading = createElement('h1', null, 'Azure Functions Graph Tutorial Test Client'); jumbotron.appendChild(heading); const lead = createElement('p', 'lead', 'This sample app is used to test the Azure Functions in the Azure Functions Graph Tutorial'); jumbotron.appendChild(lead); if (user) { // Welcome the user by name const welcomeMessage = createElement('h4', null, `Welcome ${user}!`); jumbotron.appendChild(welcomeMessage); const callToAction = createElement('p', null, 'Use the navigation bar at the top of the page to get started.'); jumbotron.appendChild(callToAction); } else { // Show a sign in button in the jumbotron const signInButton = createElement('button', 'btn btn-primary btn-large', 'Click here to sign in'); signInButton.setAttribute('onclick', 'signIn();') jumbotron.appendChild(signInButton); } mainContainer.innerHTML = ''; mainContainer.appendChild(jumbotron); } // Renders an email message function showLatestMessage(message) { // Show message const messageCard = createElement('div', 'card'); const cardBody = createElement('div', 'card-body'); messageCard.appendChild(cardBody); const subject = createElement('h1', 'card-title', `${message.subject || '(No subject)'}`); cardBody.appendChild(subject); const fromLine = createElement('div', 'd-flex'); cardBody.appendChild(fromLine); const fromLabel = createElement('div', 'mr-3'); fromLabel.appendChild(createElement('strong', '', 'From:')); fromLine.appendChild(fromLabel); fromLine.appendChild(createElement('div', '', message.from.emailAddress.name)); const receivedLine = createElement('div', 'd-flex'); cardBody.appendChild(receivedLine); const receivedLabel = createElement('div', 'mr-3'); receivedLabel.appendChild(createElement('strong', '', 'Received:')); receivedLine.appendChild(receivedLabel); receivedLine.appendChild(createElement('div', '', message.receivedDateTime)); mainContainer.innerHTML = ''; mainContainer.appendChild(messageCard); } // Renders current subscriptions from the session, and allows the user // to add new subscriptions function showSubscriptions() { const subscriptions = JSON.parse(sessionStorage.getItem('graph-subscriptions')); // Show new subscription form const form = createElement('form', 'form-inline mb-3'); const userInput = createElement('input', 'form-control mb-2 mr-2 flex-grow-1'); userInput.setAttribute('id', 'subscribe-user'); userInput.setAttribute('type', 'text'); userInput.setAttribute('placeholder', 'User to subscribe to (user ID or UPN)'); form.appendChild(userInput); const subscribeButton = createElement('button', 'btn btn-primary mb-2', 'Subscribe'); subscribeButton.setAttribute('type', 'button'); subscribeButton.setAttribute('onclick', 'createSubscription();'); form.appendChild(subscribeButton); const card = createElement('div', 'card'); const cardBody = createElement('div', 'card-body'); card.appendChild(cardBody); cardBody.appendChild(createElement('h2', 'card-title mb-4', 'Existing subscriptions')); const subscriptionTable = createElement('table', 'table'); cardBody.appendChild(subscriptionTable); const thead = createElement('thead', ''); subscriptionTable.appendChild(thead); const theadRow = createElement('tr', ''); thead.appendChild(theadRow); theadRow.appendChild(createElement('th', '')); theadRow.appendChild(createElement('th', '', 'User')); theadRow.appendChild(createElement('th', '', 'Subscription ID')) if (subscriptions) { // List subscriptions for (const subscription of subscriptions) { const row = createElement('tr', ''); subscriptionTable.appendChild(row); const deleteButtonCell = createElement('td', ''); row.appendChild(deleteButtonCell); const deleteButton = createElement('button', 'btn btn-sm btn-primary', 'Delete'); deleteButton.setAttribute('onclick', `deleteSubscription("${subscription.subscriptionId}");`); deleteButtonCell.appendChild(deleteButton); row.appendChild(createElement('td', '', subscription.userId)); row.appendChild(createElement('td', '', subscription.subscriptionId)); } } mainContainer.innerHTML = ''; mainContainer.appendChild(form); mainContainer.appendChild(card); } // Renders an error function showError(error) { const alert = createElement('div', 'alert alert-danger'); const message = createElement('p', 'mb-3', error.message); alert.appendChild(message); if (error.debug) { const pre = createElement('pre', 'alert-pre border bg-light p-2'); alert.appendChild(pre); const code = createElement('code', 'text-break text-wrap', JSON.stringify(error.debug, null, 2)); pre.appendChild(code); } mainContainer.innerHTML = ''; mainContainer.appendChild(alert); } // Re-renders the page with the selected view function updatePage(view, data) { if (!view) { view = Views.home; } // Get the user name from the session const user = sessionStorage.getItem('msal-userName'); if (!user && view !== Views.error) { view = Views.home; } showAccountNav(user); showAuthenticatedNav(user, view); switch (view) { case Views.error: showError(data); break; case Views.home: showWelcomeMessage(user); break; case Views.message: showLatestMessage(data); break; case Views.subscriptions: showSubscriptions(); break; } } updatePage(Views.home);
Этот код использует JavaScript для отображения текущей страницы на основе выбранного представления.
Тестирование одно-страницного приложения
Примечание
В этом разделе содержатся инструкции по использованию dotnet-serve для запуска простого сервера HTTP-тестирования на компьютере разработки. Использование этого конкретного средства не требуется. Вы можете использовать любой сервер тестирования, который вы предпочитаете обслуживать каталог TestClient .
Запустите следующую команду в CLI для установки dotnet-serve.
dotnet tool install --global dotnet-serve
Измените текущий каталог в CLI на каталог TestClient и запустите следующую команду, чтобы запустить http-сервер.
dotnet serve -h "Cache-Control: no-cache, no-store, must-revalidate" -p 8080
Откройте браузер и перейдите по адресу
http://localhost:8080
. Страница должна отрисовка, но ни одна из кнопок в настоящее время не работает.
Добавление пакетов NuGet
Прежде чем двигаться дальше, установите дополнительные NuGet пакеты, которые вы будете использовать позже.
- Microsoft.Azure.Functions.Extensions для впрыскивания зависимостей в проекте Azure Functions.
- Microsoft.Extensions.Configuration.UserSecrets для чтения конфигурации приложений из секретного магазина разработки .NET.
- Microsoft.Graph для вызовов Microsoft Graph.
- Microsoft.Identity.Client для проверки подлинности и управления маркерами.
- Microsoft.IdentityModel.Protocols.OpenIdConnect для восстановления конфигурации OpenID для проверки маркеров.
- System.IdentityModel.Tokens.Jwt для проверки маркеров, отправленных в веб-API.
Измените текущий каталог в CLI на каталог GraphTutorial и запустите следующие команды.
dotnet add package Microsoft.Azure.Functions.Extensions --version 1.1.0 dotnet add package Microsoft.Extensions.Configuration.UserSecrets --version 5.0.0 dotnet add package Microsoft.Graph --version 4.0.0 dotnet add package Microsoft.Identity.Client --version 4.35.1 dotnet add package Microsoft.IdentityModel.Protocols.OpenIdConnect --version 6.12.0 dotnet add package System.IdentityModel.Tokens.Jwt --version 6.12.0
Регистрация приложений на портале
В этом упражнении вы создадим три новых приложения Azure AD с Azure Active Directory центра администрирования:
- Регистрация приложения для одно-страничекого приложения, чтобы оно могли войти в пользователей и получить маркеры, позволяющие приложению вызывать функцию Azure.
- Регистрация приложения для функции Azure, которая позволяет использовать поток от имени для обмена маркера, отправленного SPA, на маркер, который позволит ему вызывать Microsoft Graph.
- Регистрация приложения для веб-пользователя Azure Function, которая позволяет использовать поток учетных данных клиента для вызова microsoft Graph без пользователя.
Примечание
В этом примере требуется три регистрации приложений, так как он реализует поток от имени и поток учетных данных клиента. Если в вашей функции Azure используется только один из этих потоков, вам потребуется создать только регистрации приложений, соответствующие этому потоку.
Откройте браузер и перейдите в центр администрирования Azure Active Directory и войдите с помощью администратора Microsoft 365 клиента.
Выберите Azure Active Directory на панели навигации слева, затем выберите Регистрация приложений в разделе Управление.
Регистрация приложения для одно-страницного приложения
Выберите Новая регистрация. На странице Зарегистрировать приложение задайте необходимые значения следующим образом.
- Введите имя
Graph Azure Function Test App
. - Установите поддерживаемые типы учетных записей только для учетных записей в этом организационном каталоге.
- В статье Перенаправление URI измените отсев на одно-страницу приложения (SPA) и установите значение
http://localhost:8080
.
- Введите имя
Нажмите Зарегистрировать. На странице Graph Azure Function Test App скопируйте значения ID приложения (клиента) и ID каталога (клиента) и сохраните их, чтобы сохранить их на последующих действиях.
Регистрация приложения для функции Azure
Вернись к регистрациям приложений и выберите новую регистрацию. На странице Зарегистрировать приложение задайте необходимые значения следующим образом.
- Введите имя
Graph Azure Function
. - Установите поддерживаемые типы учетных записей только для учетных записей в этом организационном каталоге.
- Оставьте URI перенаправления пустым.
- Введите имя
Нажмите Зарегистрировать. На странице Graph Azure Function скопируйте значение ID приложения (клиента) и сохраните его, оно потребуется на следующем шаге.
Выберите Сертификаты и секреты в разделе Управление. Нажмите кнопку Новый секрет клиента. Введите значение в поле Описание, выберите один из параметров Срок действия и нажмите Добавить.
Прежде чем покинуть страницу, скопируйте значение секрета клиента. Оно вам понадобится на следующем шаге.
Важно!
Система никогда не покажет секрет клиента повторно, поэтому убедитесь, что вы скопировали его.
Выберите разрешения API в статье Управление. Выберите Добавить разрешение.
Выберите microsoft Graph, а затем делегирование разрешений. Добавьте Mail.Read и выберите Добавить разрешения.
Выберите Expose aPI в статье Управление, а затем добавьте область.
Примите URI приложения по умолчанию и выберите Сохранить и продолжить.
Заполните форму Добавить область следующим образом:
- Имя области: Mail.Read
- Кто может согласиться?: Администраторы и пользователи
- Имя отображения согласия администратора: Чтение почтовых ящиков всех пользователей
- Описание согласия администратора: Позволяет приложению читать почтовые ящики всех пользователей
- Имя отображения согласия пользователя: Чтение почтового ящика
- Описание согласия пользователя: Позволяет приложению читать ваш почтовый ящик
- Состояние: включено
Нажмите Добавить область.
Скопируйте новую область, она потребуется в последующих действиях.
Выберите Манифест в статье Управление.
Найдите
knownClientApplications
в манифесте иTEST_APP_ID
[]
[TEST_APP_ID]
замените его текущим значением , где находится ID приложения Graph azure Function Test App. Нажмите Сохранить.
Примечание
Добавление ID knownClientApplications
приложения тестового приложения к свойству в манифесте Azure Function позволяет тестовой приложению запускать комбинированный поток согласия. Это необходимо для работы потока от имени.
Добавление области Azure Function для тестирования регистрации приложений
Вернись к регистрации Graph Azure Function Test App и выберите API Permissions under Manage. Выберите Добавить разрешение.
Выберите мои API, а затем выберите Нагрузка больше. Выберите Graph Azure Function.
Выберите разрешение Mail.Read , а затем добавьте разрешения.
В настроенных разрешениях удалите разрешение User.Read в microsoft Graph, выбрав ... справа от разрешения и выбрав удаление разрешения. Выберите Да, удалите , чтобы подтвердить.
Регистрация приложения для веб-сайта Azure Function
Вернись к регистрациям приложений и выберите новую регистрацию. На странице Зарегистрировать приложение задайте необходимые значения следующим образом.
- Введите имя
Graph Azure Function Webhook
. - Установите поддерживаемые типы учетных записей только для учетных записей в этом организационном каталоге.
- Оставьте URI перенаправления пустым.
- Введите имя
Нажмите Зарегистрировать. На странице веб Graph Azure Function скопируйте значение ID приложения (клиента) и сохраните его, оно потребуется на следующем шаге.
Выберите Сертификаты и секреты в разделе Управление. Нажмите кнопку Новый секрет клиента. Введите значение в поле Описание, выберите один из параметров Срок действия и нажмите Добавить.
Скопируйте значение секрета клиента, а затем покиньте эту страницу. Оно вам понадобится на следующем шаге.
Выберите разрешения API в статье Управление. Выберите Добавить разрешение.
Выберите microsoft Graph, а затем разрешения приложений. Добавьте User.Read.All и Mail.Read, а затем выберите Добавить разрешения.
В настроенных разрешениях удалите делегированную разрешения User.Read в microsoft Graph, выбрав ... справа от разрешения и выбрав удаление разрешения. Выберите Да, удалите , чтобы подтвердить.
Выберите разрешение администратора гранта для... кнопки, а затем выберите да , чтобы предоставить согласие администратора для настроенных разрешений приложения. Столбец Status в таблице Настраиваемых разрешений изменяется на Granted for ....
Реализация API с помощью проверки подлинности от имени
В этом упражнении вы завершите реализацию функции Azure и GetMyNewestMessage
обновим тестовый клиент, чтобы вызвать эту функцию.
Функция Azure использует поток от имени. Основной порядок событий в этом потоке:
- Тестовая программа использует интерактивный поток auth, чтобы позволить пользователю войти и предоставить согласие. Он возвращает маркер, который является областью для функции Azure. Маркер не содержит никаких областей Graph Microsoft.
- Тестовая приложение вызывает функцию Azure, отправляя маркер доступа в заглавном
Authorization
загонах. - Функция Azure проверяет маркер, а затем обменивает его на второй маркер доступа, содержащий области Microsoft Graph.
- Функция Azure вызывает microsoft Graph от имени пользователя с помощью второго маркера доступа.
Важно!
Чтобы не хранить код приложения и секрет в источнике, для хранения этих значений используется диспетчер секрета .NET . Секретный менеджер предназначен только для целей разработки, для хранения секретов в производственных приложениях должен использовать доверенный секретный менеджер.
Добавление проверки подлинности в приложение для одной страницы
Начните с добавления проверки подлинности в SPA. Это позволит приложению получить доступ к маркеру доступа для вызова функции Azure. Так как это SPA, он будет использовать поток кода авторизации с помощью PKCE.
Создайте новый файл в каталоге TestClient с именем config.jsи добавьте следующий код.
const msalConfig = { auth: { clientId: 'YOUR_TEST_APP_APP_ID_HERE', authority: 'https://login.microsoftonline.com/YOUR_TENANT_ID_HERE' } }; const msalRequest = { // Scope of the Azure Function scopes: [ 'YOUR_AZURE_FUNCTION_APP_ID_HERE/.default' ] }
Замените
YOUR_TEST_APP_APP_ID_HERE
с помощью ID приложения, созданного на портале Azure для Graph тестирования функций Azure. ЗаменитеYOUR_TENANT_ID_HERE
значение ID Directory (tenant), скопированные с портала Azure. ЗаменитеYOUR_AZURE_FUNCTION_APP_ID_HERE
с помощью ID приложения для Graph Azure Function.Важно!
Если вы используете источник управления, например git, сейчас самое время исключить файлconfig.jsиз источника управления, чтобы не допустить случайной утечки кодов приложений и ID клиента.
Создайте новый файл в каталоге TestClient с именем auth.jsи добавьте следующий код.
// Create the main MSAL instance // configuration parameters are located in config.js const msalClient = new msal.PublicClientApplication(msalConfig); async function signIn() { // Login try { // Use MSAL to login const authResult = await msalClient.loginPopup(msalRequest); // Save the account username, needed for token acquisition sessionStorage.setItem('msal-userName', authResult.account.username); // Refresh home page updatePage(Views.home); } catch (error) { console.log(error); updatePage(Views.error, { message: 'Error logging in', debug: error }); } } function signOut() { account = null; sessionStorage.removeItem('msal-userName'); msalClient.logout(); }
Рассмотрим, что делает этот код.
- Инициализирует использование
PublicClientApplication
значений, хранимых вconfig.js. - Он использует
loginPopup
для регистрации пользователя, используя область разрешений для функции Azure. - В сеансе сохраняется имя пользователя.
Важно!
Так как приложение использует
loginPopup
, возможно, потребуется изменить всплывающее блокатор браузера, чтобы разрешить всплывающие окна отhttp://localhost:8080
.- Инициализирует использование
Обновление страницы и вход. Страница должна обновиться с именем пользователя, указав, что вход был успешным.
Добавление проверки подлинности в функцию Azure
В этом разделе будет GetMyNewestMessage
реализован поток от имени в функции Azure, чтобы получить маркер доступа, совместимый с Microsoft Graph.
Инициализировать секретный магазин разработки .NET, открыв CLI в каталоге, содержаном GraphTutorial.csproj и запуская следующую команду.
dotnet user-secrets init
Добавьте свой ID приложения, секретный и клиентский ID в секретный магазин с помощью следующих команд. Замените
YOUR_API_FUNCTION_APP_ID_HERE
с помощью ID приложения для Graph Azure Function. ЗаменитеYOUR_API_FUNCTION_APP_SECRET_HERE
секрет приложения, созданный на портале Azure для Graph Azure Function. ЗаменитеYOUR_TENANT_ID_HERE
значение ID Directory (tenant), скопированные с портала Azure.dotnet user-secrets set apiFunctionId "YOUR_API_FUNCTION_APP_ID_HERE" dotnet user-secrets set apiFunctionSecret "YOUR_API_FUNCTION_APP_SECRET_HERE" dotnet user-secrets set tenantId "YOUR_TENANT_ID_HERE"
Обработка маркера входящих носителей
В этом разделе будет реализован класс для проверки и обработки маркера-носитера, отправленного из SPA в функцию Azure.
Создайте новый каталог в каталоге GraphTutorial с именем Authentication.
Создайте новый файл с именем TokenValidationResult.cs в папке ./GraphTutorial/Authentication и добавьте следующий код.
namespace GraphTutorial.Authentication { public class TokenValidationResult { // MSAL account ID - used to access the token // cache public string MsalAccountId { get; private set; } // The extracted token - used to build user assertion // for OBO flow public string Token { get; private set; } public TokenValidationResult(string msalAccountId, string token) { MsalAccountId = msalAccountId; Token = token; } } }
Создайте новый файл с именем TokenValidation.cs в папке ./GraphTutorial/Authentication и добавьте следующий код.
using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; using System; using System.Security.Claims; using System.IdentityModel.Tokens.Jwt; using System.Net.Http.Headers; using System.Threading.Tasks; namespace GraphTutorial.Authentication { public static class TokenValidation { private static TokenValidationParameters _validationParameters = null; public static async Task<TokenValidationResult> ValidateAuthorizationHeader( HttpRequest request, string tenantId, string expectedAudience, ILogger log) { // Check for Authorization header if (request.Headers.ContainsKey("authorization")) { var authHeader = AuthenticationHeaderValue.Parse(request.Headers["authorization"]); if (authHeader != null && authHeader.Scheme.ToLower() == "bearer" && !string.IsNullOrEmpty(authHeader.Parameter)) { if (_validationParameters == null) { // Load the tenant-specific OpenID config from Azure var configManager = new ConfigurationManager<OpenIdConnectConfiguration>( $"https://login.microsoftonline.com/{tenantId}/.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever()); var config = await configManager.GetConfigurationAsync(); _validationParameters = new TokenValidationParameters { // Use signing keys retrieved from Azure IssuerSigningKeys = config.SigningKeys, ValidateAudience = true, // Audience MUST be the app ID for the Web API ValidAudience = expectedAudience, ValidateIssuer = true, // Use the issuer retrieved from Azure ValidIssuer = config.Issuer, ValidateLifetime = true }; } var tokenHandler = new JwtSecurityTokenHandler(); SecurityToken jwtToken; try { // Validate the token var result = tokenHandler.ValidateToken(authHeader.Parameter, _validationParameters, out jwtToken); // If ValidateToken did not throw an exception, token is valid. return new TokenValidationResult(GetMsalAccountId(result), authHeader.Parameter); } catch (Exception exception) { log.LogError(exception, "Error validating bearer token"); } } } return null; } // Helper function to construct an MSAL account ID from the // claims in the token. MSAL uses an ID in the format // oid.tid, where oid is the object ID of the user, and tid is // the tenant ID. private static string GetMsalAccountId(ClaimsPrincipal principal) { var objectId = principal?.FindFirst("oid"); if (objectId == null) { objectId = principal?.FindFirst( "http://schemas.microsoft.com/identity/claims/objectidentifier"); } var tenantId = principal?.FindFirst("tid"); if (tenantId == null) { tenantId = principal?.FindFirst( "http://schemas.microsoft.com/identity/claims/tenantid"); } if (objectId != null && tenantId != null) { return $"{objectId.Value}.{tenantId.Value}"; } return null; } } }
Рассмотрим, что делает этот код.
- Оно гарантирует, что в загонах имеется маркер носитела
Authorization
. - Он проверяет подпись и эмитент из опубликованной конфигурации OpenID в Azure.
- В нем проверяется, что аудитория (
aud
утверждение) соответствует ID приложения Azure Function. - Он разбирает маркер и создает ID учетной записи MSAL, который будет необходим для использования кэшинга маркеров.
Создание поставщика проверки подлинности от имени
Создайте новый файл в каталоге проверки подлинности с именем OnBehalfOfAuthProvider.cs и добавьте в этот файл следующий код.
using Microsoft.Extensions.Logging; using Microsoft.Graph; using Microsoft.Identity.Client; using System; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; namespace GraphTutorial.Authentication { public class OnBehalfOfAuthProvider : IAuthenticationProvider { private IConfidentialClientApplication _msalClient; private TokenValidationResult _tokenResult; private string[] _scopes; private ILogger _logger; public OnBehalfOfAuthProvider( IConfidentialClientApplication msalClient, TokenValidationResult tokenResult, string[] scopes, ILogger logger) { _scopes = scopes; _logger = logger; _tokenResult = tokenResult; _msalClient = msalClient; } public async Task<string> GetAccessToken() { try { // First attempt to get token from the cache for this user // Check for a matching account in the cache var account = await _msalClient.GetAccountAsync(_tokenResult.MsalAccountId); if (account != null) { // Make a "silent" request for a token. This will // return the cached token if still valid, and will handle // refreshing the token if needed var cacheResult = await _msalClient .AcquireTokenSilent(_scopes, account) .ExecuteAsync(); _logger.LogInformation($"User access token: {cacheResult.AccessToken}"); return cacheResult.AccessToken; } } catch (MsalUiRequiredException) { // This exception indicates that a new token // can only be obtained by invoking the on-behalf-of // flow. "UiRequired" isn't really accurate since the OBO // flow doesn't involve UI. // Catching the exception so code will continue to the // AcquireTokenOnBehalfOf call below. } catch (Exception exception) { _logger.LogError(exception, "Error getting access token via on-behalf-of flow"); return null; } try { _logger.LogInformation("Token not found in cache, attempting OBO flow"); // Use the token sent by the calling client as a // user assertion var userAssertion = new UserAssertion(_tokenResult.Token); // Invoke on-behalf-of flow var result = await _msalClient .AcquireTokenOnBehalfOf(_scopes, userAssertion) .ExecuteAsync(); _logger.LogInformation($"User access token: {result.AccessToken}"); return result.AccessToken; } catch (Exception exception) { _logger.LogError(exception, "Error getting access token from cache"); return null; } } // This is the delegate called by the GraphServiceClient on each // request. public async Task AuthenticateRequestAsync(HttpRequestMessage requestMessage) { // Get the current access token var token = await GetAccessToken(); // Add the token in the Authorization header requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); } } }
Подумайте о том, что делает код в OnBehalfOfAuthProvider.cs .
- В функции
GetAccessToken
сначала пытается получить маркер пользователя из кэша маркера с помощьюAcquireTokenSilent
. Если это не удается, он использует маркер-носителер, отправленный тест-приложением в функцию Azure для создания утверждения пользователя. Затем он использует это утверждение пользователя, чтобы получить Graph совместимый маркер с помощьюAcquireTokenOnBehalfOf
. - Он реализует интерфейс
Microsoft.Graph.IAuthenticationProvider
, позволяяGraphServiceClient
передавать этот класс в конструкторе исходяющих запросов для проверки подлинности.
Реализация службы Graph клиента
В этом разделе вы реализуете службу, которая может быть зарегистрирована для впрыска зависимостей. Служба будет использоваться для получения проверки подлинности Graph клиента.
Создание нового каталога в каталоге GraphTutorial с именем Services.
Создайте новый файл в каталоге Служб с именем IGraphClientService.cs и добавьте в этот файл следующий код.
using GraphTutorial.Authentication; using Microsoft.Extensions.Logging; using Microsoft.Graph; namespace GraphTutorial.Services { public interface IGraphClientService { GraphServiceClient GetUserGraphClient( TokenValidationResult validation, string[] scopes, ILogger logger); GraphServiceClient GetAppGraphClient(ILogger logger); } }
Создайте новый файл в каталоге Служб с именем GraphClientService.cs и добавьте в этот файл следующий код.
using GraphTutorial.Authentication; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Identity.Client; using Microsoft.Graph; namespace GraphTutorial.Services { // Service added via dependency injection // Used to get an authenticated Graph client public class GraphClientService : IGraphClientService { } }
Добавьте в класс следующие
GraphClientService
свойства.// Configuration private IConfiguration _config; // Single MSAL client object used for all user-related // requests. Making this a "singleton" here because the sample // uses the default in-memory token cache. private IConfidentialClientApplication _userMsalClient;
Добавьте в класс следующие
GraphClientService
функции.public GraphClientService(IConfiguration config) { _config = config; } public GraphServiceClient GetUserGraphClient(TokenValidationResult validation, string[] scopes, ILogger logger) { // Only create the MSAL client once if (_userMsalClient == null) { _userMsalClient = ConfidentialClientApplicationBuilder .Create(_config["apiFunctionId"]) .WithAuthority(AadAuthorityAudience.AzureAdMyOrg, true) .WithTenantId(_config["tenantId"]) .WithClientSecret(_config["apiFunctionSecret"]) .Build(); } // Create a new OBO auth provider for the specific user var authProvider = new OnBehalfOfAuthProvider(_userMsalClient, validation, scopes, logger); // Return a GraphServiceClient initialized with the auth provider return new GraphServiceClient(authProvider); }
Добавьте реализацию задатки для функции
GetAppGraphClient
. Это будет реализовано в последующих разделах.public GraphServiceClient GetAppGraphClient() { throw new System.NotImplementedException(); }
Функция
GetUserGraphClient
принимает результаты проверки маркеров и создает проверкуGraphServiceClient
подлинности для пользователя.Откройте ./GraphTutorial/Program.cs и замените его содержимое следующим.
Этот код добавит секреты пользователей в конфигурацию и встроит инъекцию зависимостей в функции Azure, обнажая службу.
GraphClientService
Реализация функции GetMyNewestMessage
Откройте ./GraphTutorial/GetMyNewestMessage.cs и замените все содержимое на следующее.
using GraphTutorial.Authentication; using GraphTutorial.Services; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Azure.WebJobs; using Microsoft.Azure.WebJobs.Extensions.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using System.Threading.Tasks; using System.Web.Http; namespace GraphTutorial { public class GetMyNewestMessage { private IConfiguration _config; private IGraphClientService _clientService; public GetMyNewestMessage(IConfiguration config, IGraphClientService clientService) { _config = config; _clientService = clientService; } [FunctionName("GetMyNewestMessage")] public async Task<IActionResult> Run( [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req, ILogger log) { // Check configuration if (string.IsNullOrEmpty(_config["apiFunctionId"]) || string.IsNullOrEmpty(_config["apiFunctionSecret"]) || string.IsNullOrEmpty(_config["tenantId"])) { log.LogError("Invalid app settings configured"); return new InternalServerErrorResult(); } // Validate the bearer token var validationResult = await TokenValidation.ValidateAuthorizationHeader( req, _config["tenantId"], _config["apiFunctionId"], log); // If token wasn't returned it isn't valid if (validationResult == null) { return new UnauthorizedResult(); } // Initialize a Graph client for this user var graphClient = _clientService.GetUserGraphClient(validationResult, new[] { "https://graph.microsoft.com/.default" }, log); // Get the user's newest message in inbox // GET /me/mailfolders/inbox/messages var messagePage = await graphClient.Me .MailFolders .Inbox .Messages .Request() // Limit the fields returned .Select(m => new { m.From, m.ReceivedDateTime, m.Subject }) // Sort by received time, newest on top .OrderBy("receivedDateTime DESC") // Only get back one message .Top(1) .GetAsync(); if (messagePage.CurrentPage.Count < 1) { return new OkObjectResult(null); } // Return the message in the response return new OkObjectResult(messagePage.CurrentPage[0]); } } }
Просмотрите код в GetMyNewestMessage.cs
Подумайте, что делает код в GetMyNewestMessage.cs .
- В конструкторе он сохраняет объекты
IConfiguration
IGraphClientService
, переданные через впрыски зависимостей. - В функции
Run
она делает следующее:- Проверяет необходимые значения конфигурации, присутствующие в объекте
IConfiguration
. - Проверяет маркер носителей и возвращает код
401
состояния, если маркер является недействительным. - Получает Graph клиента от
GraphClientService
пользователя, который сделал этот запрос. - Использует SDK Graph Microsoft для получения самого нового сообщения из почтового ящика пользователя и возвращает его в качестве тела JSON в ответе.
- Проверяет необходимые значения конфигурации, присутствующие в объекте
Вызов функции Azure из тестового приложения
Откройте auth.js и добавьте следующую функцию, чтобы получить маркер доступа.
async function getToken() { let account = sessionStorage.getItem('msal-userName'); if (!account){ throw new Error( 'User account missing from session. Please sign out and sign in again.'); } try { // First, attempt to get the token silently const silentRequest = { scopes: msalRequest.scopes, account: msalClient.getAccountByUsername(account) }; const silentResult = await msalClient.acquireTokenSilent(silentRequest); return silentResult.accessToken; } catch (silentError) { // If silent requests fails with InteractionRequiredAuthError, // attempt to get the token interactively if (silentError instanceof msal.InteractionRequiredAuthError) { const interactiveResult = await msalClient.acquireTokenPopup(msalRequest); return interactiveResult.accessToken; } else { throw silentError; } } }
Рассмотрим, что делает этот код.
- Сначала он пытается получить маркер доступа без взаимодействия с пользователем. Так как пользователь уже должен быть подписан, msAL должен иметь маркеры для пользователя в кэше.
- Если сбой с ошибкой, которая указывает на то, что пользователю необходимо взаимодействовать, он пытается получить маркер интерактивно.
Совет
Вы можете разопросить https://jwt.ms
aud
маркер доступа и подтвердить, что утверждение является ID приложения для функции Azure иscp
что в утверждении содержится область разрешений Azure Function, а не Microsoft Graph.Создайте новый файл в каталоге TestClient с именем azurefunctions.jsи добавьте следующий код.
async function getLatestMessage() { const token = await getToken(); if (!token) { updatePage(Views.error, { message: 'Could not retrieve token for user' }); return; } try { const response = await fetch('http://localhost:7071/api/GetMyNewestMessage', { headers: { Authorization: `Bearer ${token}` } }); const message = await response.json(); updatePage(Views.message, message); } catch (error) { updatePage(Views.error, { message: 'Error getting message', debug: error }); } }
Измените текущий каталог в CLI на каталог ./GraphTutorial и запустите следующую команду, чтобы запустить функцию Azure локально.
func start
Если еще не обслуживается SPA, откройте второе окно CLI и измените текущий каталог на каталог ./TestClient . Запустите следующую команду для запуска тестового приложения.
dotnet serve -h "Cache-Control: no-cache, no-store, must-revalidate"
Откройте браузер и перейдите по адресу
http://localhost:8080
. Впишите и выберите элемент навигации "Последнее сообщение". Приложение отображает сведения о самом новом сообщении в почтовом ящике пользователя.
Реализация веб-сайта с помощью проверки подлинности учетных данных клиента
В этом упражнении вы завершите реализацию функций Azure и SetSubscription
Notify
обновим тестового приложения, чтобы подписаться и отписаться от изменений в почтовом ящике пользователя.
- Функция
SetSubscription
будет выступать в качестве API, позволяя тестовой приложению создавать или удалять подписку на изменения в почтовом ящике пользователя. - Функция
Notify
будет выступать в качестве веб-сайта, который получает уведомления об изменении, созданные подпиской.
Обе функции будут использовать поток клиентских учетных данных для получения маркера только для приложения для вызова Microsoft Graph. Так как администратор предоставил администратору согласие на необходимые области разрешений, для получения маркера не потребуется взаимодействие с пользователем.
Добавление проверки подлинности учетных данных клиентов в проект Azure Functions
В этом разделе вы реализуете поток клиентских учетных данных в проекте Azure Functions, чтобы получить маркер доступа, совместимый с Microsoft Graph.
Откройте CLI в каталоге, который содержит GraphTutorial.csproj.
Добавьте свой ИД приложения webhook и секрет в секретный магазин с помощью следующих команд. Замените
YOUR_WEBHOOK_APP_ID_HERE
iD приложения для веб-Graph Azure Function. ЗаменитеYOUR_WEBHOOK_APP_SECRET_HERE
секрет приложения, созданный на портале Azure для веб-Graph Azure Function.dotnet user-secrets set webHookId "YOUR_WEBHOOK_APP_ID_HERE" dotnet user-secrets set webHookSecret "YOUR_WEBHOOK_APP_SECRET_HERE"
Создание поставщика проверки подлинности учетных данных клиента
Создайте новый файл в каталоге ./GraphTutorial/Authentication с именем ClientCredentialsAuthProvider.cs и добавьте следующий код.
using Microsoft.Extensions.Logging; using Microsoft.Graph; using Microsoft.Identity.Client; using System; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; namespace GraphTutorial.Authentication { public class ClientCredentialsAuthProvider : IAuthenticationProvider { private IConfidentialClientApplication _msalClient; private string[] _scopes; private ILogger _logger; public ClientCredentialsAuthProvider( string appId, string clientSecret, string tenantId, string[] scopes, ILogger logger) { _scopes = scopes; _logger = logger; _msalClient = ConfidentialClientApplicationBuilder .Create(appId) .WithAuthority(AadAuthorityAudience.AzureAdMyOrg, true) .WithTenantId(tenantId) .WithClientSecret(clientSecret) .Build(); } public async Task<string> GetAccessToken() { try { // Invoke client credentials flow // NOTE: This will return a cached token if a valid one // exists var result = await _msalClient .AcquireTokenForClient(_scopes) .ExecuteAsync(); _logger.LogInformation($"App-only access token: {result.AccessToken}"); return result.AccessToken; } catch (Exception exception) { _logger.LogError(exception, "Error getting access token via client credentials flow"); return null; } } // This is the delegate called by the GraphServiceClient on each // request. public async Task AuthenticateRequestAsync(HttpRequestMessage requestMessage) { // Get the current access token var token = await GetAccessToken(); // Add the token in the Authorization header requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); } } }
Удумайте, что делает код в ClientCredentialsAuthProvider.cs .
- В конструкторе инициализируется конфиденциальнаяclientApplication из пакета
Microsoft.Identity.Client
. Он использует функцииWithAuthority(AadAuthorityAudience.AzureAdMyOrg, true)
и.WithTenantId(tenantId)
функции, чтобы ограничить аудиторию входа только указанной Microsoft 365 организации. - В функции
GetAccessToken
она вызывает получениеAcquireTokenForClient
маркера для приложения. Поток маркеров учетных данных клиента всегда не является интерактивным. - Он реализует интерфейс
Microsoft.Graph.IAuthenticationProvider
, позволяяGraphServiceClient
передавать этот класс в конструкторе исходяющих запросов для проверки подлинности.
Обновление GraphClientService
Откройте GraphClientService.cs и добавьте следующее свойство в класс.
private GraphServiceClient _appGraphClient;
Замените имеющуюся функцию
GetAppGraphClient
указанным ниже кодом.public GraphServiceClient GetAppGraphClient(ILogger logger) { if (_appGraphClient == null) { // Create a client credentials auth provider var authProvider = new ClientCredentialsAuthProvider( _config["webHookId"], _config["webHookSecret"], _config["tenantId"], // The https://graph.microsoft.com/.default scope // is required for client credentials. It requests // all of the permissions that are explicitly set on // the app registration new[] { "https://graph.microsoft.com/.default" }, logger); _appGraphClient = new GraphServiceClient(authProvider); } return _appGraphClient; }
Реализация функции Notify
В этом разделе будет реализована функция Notify
, которая будет использоваться в качестве URL-адреса уведомлений об изменениях.
Создайте новый каталог в каталоге GraphTutorials с именем Models.
Создайте новый файл в каталоге Models с именем ResourceData.cs и добавьте следующий код.
namespace GraphTutorial.Models { // Class to represent the resourceData object // inside a change notification public class ResourceData { public string Id { get;set; } } }
Создайте новый файл в каталоге Models с именем ChangeNotificationPayload.cs и добавьте следующий код.
Создайте новый файл в каталоге Моделей с именем NotificationList.cs и добавьте следующий код.
namespace GraphTutorial.Models { // Class representing an array of notifications // in a notification payload public class NotificationList { public ChangeNotification[] Value { get;set; } } }
Откройте ./GraphTutorial/Notify.cs и замените все содержимое на следующее.
using GraphTutorial.Models; using GraphTutorial.Services; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Azure.WebJobs; using Microsoft.Azure.WebJobs.Extensions.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Graph; using System.IO; using System.Text.Json; using System.Threading.Tasks; using System.Web.Http; namespace GraphTutorial { public class Notify { public static readonly string ClientState = "GraphTutorialState"; private IConfiguration _config; private IGraphClientService _clientService; public Notify(IConfiguration config, IGraphClientService clientService) { _config = config; _clientService = clientService; } [FunctionName("Notify")] public async Task<IActionResult> Run( [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequest req, ILogger log) { // Check configuration if (string.IsNullOrEmpty(_config["webHookId"]) || string.IsNullOrEmpty(_config["webHookSecret"]) || string.IsNullOrEmpty(_config["tenantId"])) { log.LogError("Invalid app settings configured"); return new InternalServerErrorResult(); } // Is this a validation request? // https://docs.microsoft.com/graph/webhooks#notification-endpoint-validation string validationToken = req.Query["validationToken"]; if (!string.IsNullOrEmpty(validationToken)) { // Because validationToken is a string, OkObjectResult // will return a text/plain response body, which is // required for validation return new OkObjectResult(validationToken); } // Not a validation request, process the body var requestBody = await new StreamReader(req.Body).ReadToEndAsync(); log.LogInformation($"Change notification payload: {requestBody}"); var jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; // Deserialize the JSON payload into a list of ChangeNotification // objects var notifications = JsonSerializer.Deserialize<NotificationList>(requestBody, jsonOptions); foreach (var notification in notifications.Value) { if (notification.ClientState == ClientState) { // Process each notification await ProcessNotification(notification, log); } else { log.LogInformation($"Notification received with unexpected client state: {notification.ClientState}"); } } // Return 202 per docs return new AcceptedResult(); } private async Task ProcessNotification(ChangeNotification notification, ILogger log) { var graphClient = _clientService.GetAppGraphClient(log); // The resource field in the notification has the URL to the // message, including the user ID and message ID. Since we // have the URL, use a MessageRequestBuilder instead of the fluent // API var msgRequestBuilder = new MessageRequestBuilder( $"https://graph.microsoft.com/v1.0/{notification.Resource}", graphClient); var message = await msgRequestBuilder.Request() .Select(m => new { m.Subject }) .GetAsync(); log.LogInformation($"The following message was {notification.ChangeType}:"); log.LogInformation($"Subject: {message.Subject}, ID: {message.Id}"); } } }
Удумайте, что делает код в Notify.cs .
- Функция
Run
проверяет наличие параметраvalidationToken
запроса. Если этот параметр присутствует, он обрабатывает запрос в качестве запроса проверки и отвечает соответствующим образом. - Если запрос не является запросом на проверку, полезной нагрузкой JSON является deserialized в
ChangeNotificationCollection
. - Каждое уведомление в списке проверяется на ожидаемое значение состояния клиента и обрабатывается.
- Сообщение, которое вызвало уведомление, извлекалось с помощью Microsoft Graph.
Реализация функции SetSubscription
В этом разделе будет реализована функция SetSubscription. Эта функция будет выступать в качестве API, который вызван тест-приложением для создания или удаления подписки в почтовом ящике пользователя.
Создайте новый файл в каталоге Models с именем SetSubscriptionPayload.cs и добавьте следующий код.
namespace GraphTutorial.Models { // Class to represent the payload sent to the // SetSubscription function public class SetSubscriptionPayload { // "subscribe" or "unsubscribe" public string RequestType { get;set; } // If unsubscribing, the subscription to delete public string SubscriptionId { get;set; } // If subscribing, the user ID to subscribe to // Can be object ID of user, or userPrincipalName public string UserId { get;set; } } }
Откройте ./GraphTutorial/SetSubscription.cs и замените все содержимое следующим образом.
using GraphTutorial.Authentication; using GraphTutorial.Models; using GraphTutorial.Services; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Azure.WebJobs; using Microsoft.Azure.WebJobs.Extensions.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Graph; using System; using System.IO; using System.Text.Json; using System.Threading.Tasks; using System.Web.Http; namespace GraphTutorial { public class SetSubscription { private IConfiguration _config; private IGraphClientService _clientService; public SetSubscription(IConfiguration config, IGraphClientService clientService) { _config = config; _clientService = clientService; } [FunctionName("SetSubscription")] public async Task<IActionResult> Run( [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequest req, ILogger log) { // Check configuration if (string.IsNullOrEmpty(_config["webHookId"]) || string.IsNullOrEmpty(_config["webHookSecret"]) || string.IsNullOrEmpty(_config["tenantId"]) || string.IsNullOrEmpty(_config["apiFunctionId"])) { log.LogError("Invalid app settings configured"); return new InternalServerErrorResult(); } var notificationHost = _config["ngrokUrl"]; if (string.IsNullOrEmpty(notificationHost)) { notificationHost = req.Host.Value; } // Validate the bearer token var validationResult = await TokenValidation.ValidateAuthorizationHeader( req, _config["tenantId"], _config["apiFunctionId"], log); // If token wasn't returned it isn't valid if (validationResult == null) { return new UnauthorizedResult(); } var requestBody = await new StreamReader(req.Body).ReadToEndAsync(); var jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; // Deserialize the JSON payload into a SetSubscriptionPayload object var payload = JsonSerializer.Deserialize<SetSubscriptionPayload>(requestBody, jsonOptions); if (payload == null) { return new BadRequestErrorMessageResult("Invalid request payload"); } // Initialize Graph client var graphClient = _clientService.GetAppGraphClient(log); if (payload.RequestType.ToLower() == "subscribe") { if (string.IsNullOrEmpty(payload.UserId)) { return new BadRequestErrorMessageResult("Required fields in payload missing"); } // Create a new subscription object var subscription = new Subscription { ChangeType = "created,updated", NotificationUrl = $"{notificationHost}/api/Notify", Resource = $"/users/{payload.UserId}/mailfolders/inbox/messages", ExpirationDateTime = DateTimeOffset.UtcNow.AddDays(2), ClientState = Notify.ClientState }; // POST /subscriptions var createdSubscription = await graphClient.Subscriptions .Request() .AddAsync(subscription); return new OkObjectResult(createdSubscription); } else { if (string.IsNullOrEmpty(payload.SubscriptionId)) { return new BadRequestErrorMessageResult("Subscription ID missing in payload"); } // DELETE /subscriptions/subscriptionId await graphClient.Subscriptions[payload.SubscriptionId] .Request() .DeleteAsync(); return new AcceptedResult(); } } } }
Задумайтесь о том, что делает код в SetSubscription.cs .
- Функция
Run
считывает полезной нагрузки JSON, отправленной в запрос POST, чтобы определить тип запроса (подписаться или отписаться), пользовательский ID для подписки и подписку на подписку, чтобы отписаться. - Если запрос является запросом на подписку, он использует SDK microsoft Graph для создания новой подписки в почтовом ящике указанного пользователя. Подписка будет уведомлять о том, когда сообщения создаются или обновляются. Новая подписка возвращается в полезной нагрузке JSON ответа.
- Если запрос является отписаным, для удаления указанной подписки используется Graph microsoft Graph SDK.
Вызов SetSubscription из тестового приложения
В этом разделе будут реализованы функции создания и удаления подписок в тестовом приложении.
Откройте ./TestClient/azurefunctions.js и добавьте следующую функцию.
async function createSubscription() { // Get the user to subscribe for const userId = document.getElementById('subscribe-user').value; if (!userId) { updatePage(Views.error, { message: 'Please provide a user ID or userPrincipalName' }); return; } const token = await getToken(); if (!token) { updatePage(Views.error, { message: 'Could not retrieve token for user' }); return; } // Build the JSON payload for the subscribe request const payload = { requestType: 'subscribe', userId: userId }; const response = await fetch('http://localhost:7071/api/SetSubscription', { method: 'POST', headers: { Authorization: `Bearer ${token}` }, body: JSON.stringify(payload) }); if (response.ok) { // Get the new subscription from the response const subscription = await response.json(); // Add the new subscription to the array of subscriptions // in the session let existingSubscriptions = JSON.parse(sessionStorage.getItem('graph-subscriptions')) || []; existingSubscriptions.push({ userId: userId, subscriptionId: subscription.id }); sessionStorage.setItem('graph-subscriptions', JSON.stringify(existingSubscriptions)); // Refresh the subscriptions page to display the new // subscription updatePage(Views.subscriptions); return; } updatePage(Views.error, { message: `Call to SetSubscription returned ${response.status}`, debug: response.statusText }); }
Этот код вызывает функцию
SetSubscription
Azure для подписки и добавляет новую подписку в массив подписок в сеансе.Добавьте следующую функцию вazurefunctions.js.
async function deleteSubscription(subscriptionId) { const token = await getToken(); if (!token) { updatePage(Views.error, { message: 'Could not retrieve token for user' }); return; } // Build the JSON payload for the unsubscribe request const payload = { requestType: 'unsubscribe', subscriptionId: subscriptionId }; const response = await fetch('http://localhost:7071/api/SetSubscription', { method: 'POST', headers: { Authorization: `Bearer ${token}` }, body: JSON.stringify(payload) }); if (response.ok) { // Remove the subscription from the array let existingSubscriptions = JSON.parse(sessionStorage.getItem('graph-subscriptions')) || []; const subscriptionIndex = existingSubscriptions.findIndex((item) => { return item.subscriptionId === subscriptionId; }); existingSubscriptions.splice(subscriptionIndex, 1); sessionStorage.setItem('graph-subscriptions', JSON.stringify(existingSubscriptions)); // Refresh the subscriptions page updatePage(Views.subscriptions); return; } updatePage(Views.error, { message: `Call to SetSubscription returned ${response.status}`, debug: response.statusText }); }
Этот код вызывает функцию
SetSubscription
Azure для отписки и удаляет подписку из массива подписок в сеансе.Если у вас нет запуска ngrok, запустите ngrok (
ngrok http 7071
) и скопируйте URL-адрес переададки HTTPS.Добавьте URL-адрес ngrok в хранилище секретов пользователей, выстроив следующую команду.
dotnet user-secrets set ngrokUrl "YOUR_NGROK_URL_HERE"
Важно!
Если вы перезапустите ngrok, вам потребуется повторить эту команду, чтобы обновить URL-адрес ngrok.
Измените текущий каталог в CLI на каталог ./GraphTutorial и запустите следующую команду, чтобы запустить функцию Azure локально.
func start
Обновите SPA и выберите элемент Подписки nav. Введите пользовательский ИД для пользователя в Microsoft 365 организации с Exchange Online почтовым ящиком. Это может быть
id
пользователь (от Microsoft Graph) или пользовательuserPrincipalName
. Нажмите Кнопку Подписка.Страница обновляет отображение новой подписки в таблице.
Отправка электронной почты пользователю. Через некоторое время функция
Notify
должна быть вызвана. Проверить это можно в веб-интерфейсе ngrok (http://localhost:4040
) или в отлаговке проекта Azure Function.... [7/8/2020 7:33:57 PM] The following message was created: [7/8/2020 7:33:57 PM] Subject: Hi Megan!, ID: AAMkAGUyN2I4N2RlLTEzMTAtNDBmYy1hODdlLTY2NTQwODE2MGEwZgBGAAAAAAA2J9QH-DvMRK3pBt_8rA6nBwCuPIFjbMEkToHcVnQirM5qAAAAAAEMAACuPIFjbMEkToHcVnQirM5qAACHmpAsAAA= [7/8/2020 7:33:57 PM] Executed 'Notify' (Succeeded, Id=9c40af0b-e082-4418-aa3a-aee624f30e7a) ...
В тестовом приложении нажмите кнопку Удалить в строке таблицы для подписки. Страница обновляется, а подписка больше не находится в таблице.
Подготовка к публикации в Azure
В этом упражнении вы узнаете, какие изменения необходимы примеру Azure Function для подготовки к публикации в приложении Azure Functions.
Обновление кода
Конфигурация читается из секретного магазина пользователя, который применяется только к вашей машине разработки. Перед публикацией в Azure необходимо изменить место хранения конфигурации и соответствующим образом обновить код в Program.cs .
Секреты приложений должны храниться в безопасном хранилище, например в хранилище Azure Key Vault.
Обновление параметра CORS для функции Azure
В этом примере мы настроили CORS в local.settings.json , чтобы разрешить тестовому приложению вызывать функцию. Вам потребуется настроить опубликованную функцию, чтобы разрешить любые spa-приложения, которые будут ее вызывать.
Обновление регистраций приложений
Свойство knownClientApplications
в манифесте для регистрации Graph Azure Function должно быть обновлено с помощью ID приложений любых приложений, которые будут вызывать функцию Azure.
Воссоздание существующих подписок
Любые подписки, созданные с помощью URL-адреса веб-страницы на локальном компьютере или ngrok, должны быть воссозданы с использованием производственного URL-адреса Notify
функции Azure.
Поздравляем!
Вы завершили учебный курс Azure Functions Microsoft Graph. Теперь, когда у вас есть рабочее приложение, которое вызывает Microsoft Graph, вы можете экспериментировать и добавлять новые функции. В обзоре microsoft Graph все данные, к которые можно получить доступ с помощью Microsoft Graph.
Отзывы
Обратите внимание на этот учебник в репозитории GitHub.
Возникла проблема с этим разделом? Если это так, отправьте нам отзыв, чтобы мы исправили этот раздел.