You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Bot Builder:如何在SharePoint中使用OAuth 2隐式授权流创建机器人

.NET实现SharePoint机器人OAuth 2.0隐式授权流获取用户信息

我刚好做过类似的.NET实现,给你一步步拆解如何用OAuth 2.0隐式授权流在SharePoint机器人中获取用户信息,下面是具体的代码和配置步骤:

一、前置准备(Azure AD应用注册)

首先得在Azure AD里注册一个应用,配置隐式授权:

  • 登录Azure门户,进入Azure Active Directory → 应用注册 → 新建注册
  • 填写名称,选择支持的账户类型(比如“任何组织目录中的账户”)
  • 注册完成后,进入认证页面,在“隐式授权和混合流”下勾选ID令牌和访问令牌
  • 添加重定向URI:比如https://localhost:5001/Callback(根据你的.NET应用端口调整)
  • 进入API权限页面,添加委托权限:
    • Microsoft Graph → User.Read(获取用户基本信息)
    • SharePoint → Sites.Read.All(读取SharePoint站点信息,根据你的机器人需求调整)
  • 如果是租户级应用,点击授予管理员同意(否则普通用户需要自己同意)

二、.NET项目配置(ASP.NET Core示例)

我用ASP.NET Core Razor Pages来演示,因为隐式流需要前端配合解析哈希参数,后端做验证和API调用:

1. 配置appsettings.json

在项目根目录的appsettings.json中添加Azure AD和SharePoint相关配置:

{
  "AzureAd": {
    "TenantId": "你的租户ID",
    "ClientId": "你的应用注册Client ID",
    "RedirectUri": "https://localhost:5001/Callback"
  },
  "SharePoint": {
    "SiteUrl": "你的SharePoint站点URL,比如https://contoso.sharepoint.com/sites/MySite"
  }
}

2. 配置Program.cs

添加Session和认证服务支持:

var builder = WebApplication.CreateBuilder(args);

// 添加Razor Pages支持
builder.Services.AddRazorPages();

// 配置Session用于存储state和nonce
builder.Services.AddSession(options =>
{
    options.IdleTimeout = TimeSpan.FromMinutes(30);
    options.Cookie.HttpOnly = true;
    options.Cookie.IsEssential = true;
});

// 添加JWT验证服务
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = $"https://login.microsoftonline.com/{builder.Configuration["AzureAd:TenantId"]}/v2.0";
        options.Audience = builder.Configuration["AzureAd:ClientId"];
    });

var app = builder.Build();

// 中间件顺序很重要
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseSession(); // 必须在UseAuthentication之前
app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages();

app.Run();

三、核心代码实现

1. 生成授权跳转链接(Index.cshtml.cs)

在首页添加登录按钮,点击后跳转到Azure AD授权页面:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace SharePointBot.Pages;

public class IndexModel : PageModel
{
    private readonly IConfiguration _configuration;

    public IndexModel(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    public IActionResult OnGetLogin()
    {
        var tenantId = _configuration["AzureAd:TenantId"];
        var clientId = _configuration["AzureAd:ClientId"];
        var redirectUri = _configuration["AzureAd:RedirectUri"];
        // 所需权限,根据你的需求调整
        var scope = "openid profile User.Read Sites.Read.All";
        // 生成随机state和nonce用于防CSRF和令牌验证
        var state = Guid.NewGuid().ToString();
        var nonce = Guid.NewGuid().ToString();

        // 保存到Session,后续验证用
        HttpContext.Session.SetString("AuthState", state);
        HttpContext.Session.SetString("AuthNonce", nonce);

        // 构造授权URL
        var authUrl = $"https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/authorize?" +
            $"client_id={clientId}" +
            $"&response_type=id_token token" + // 隐式流返回ID令牌和访问令牌
            $"&redirect_uri={Uri.EscapeDataString(redirectUri)}" +
            $"&scope={Uri.EscapeDataString(scope)}" +
            $"&state={state}" +
            $"&nonce={nonce}" +
            $"&response_mode=fragment"; // 参数放在哈希中,避免暴露在URL查询参数

        return Redirect(authUrl);
    }
}

对应的Index.cshtml添加登录按钮:

@page
@model IndexModel
@{
    ViewData["Title"] = "SharePoint机器人登录";
}

<h1>SharePoint机器人用户信息获取</h1>
<a asp-page-handler="Login" class="btn btn-primary">登录并获取用户信息</a>

2. 回调页面处理(Callback.cshtml)

因为隐式流的令牌参数在URL哈希中,后端无法直接读取,所以用前端JavaScript解析后发送到后端:

@page
@model SharePointBot.Pages.CallbackModel
@{
    ViewData["Title"] = "回调页面";
}

<h1>正在处理登录...</h1>
<pre id="userInfo"></pre>

<script>
window.onload = function() {
    // 解析URL哈希中的参数
    const hash = window.location.hash.substr(1);
    const params = new URLSearchParams(hash);
    const idToken = params.get('id_token');
    const accessToken = params.get('access_token');
    const state = params.get('state');

    if (!idToken || !accessToken || !state) {
        document.getElementById('userInfo').innerText = "登录失败:未获取到令牌参数";
        return;
    }

    // 发送令牌到后端验证并获取用户信息
    fetch('/Callback/ProcessTokens', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({
            IdToken: idToken,
            AccessToken: accessToken,
            State: state
        })
    })
    .then(response => {
        if (!response.ok) throw new Error('后端验证失败');
        return response.json();
    })
    .then(data => {
        document.getElementById('userInfo').innerText = "用户信息:\n" + JSON.stringify(data, null, 2);
    })
    .catch(error => {
        document.getElementById('userInfo').innerText = "错误:" + error.message;
    });
};
</script>

3. 后端令牌验证与用户信息获取(Callback.cshtml.cs)

编写后端接口验证令牌,并调用SharePoint REST API获取用户信息:

using System.IdentityModel.Tokens.Jwt;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.IdentityModel.Tokens;

namespace SharePointBot.Pages;

public class CallbackModel : PageModel
{
    private readonly IConfiguration _configuration;

    public CallbackModel(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    [HttpPost]
    public async Task<IActionResult> ProcessTokens([FromBody] TokenRequest model)
    {
        // 1. 验证state参数,防止CSRF攻击
        var savedState = HttpContext.Session.GetString("AuthState");
        if (model.State != savedState)
        {
            return BadRequest("无效的state参数,请求可能被篡改");
        }

        // 2. 验证ID令牌的合法性
        var tenantId = _configuration["AzureAd:TenantId"];
        var clientId = _configuration["AzureAd:ClientId"];
        var savedNonce = HttpContext.Session.GetString("AuthNonce");

        var validationParams = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = $"https://login.microsoftonline.com/{tenantId}/v2.0",
            ValidateAudience = true,
            ValidAudience = clientId,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            NonceValidator = (nonce, token, parameters) => nonce == savedNonce // 验证nonce,防止重放攻击
        };

        var tokenHandler = new JwtSecurityTokenHandler();
        ClaimsPrincipal principal;
        try
        {
            principal = tokenHandler.ValidateToken(model.IdToken, validationParams, out _);
        }
        catch (Exception ex)
        {
            return BadRequest($"ID令牌验证失败:{ex.Message}");
        }

        // 3. 从ID令牌中提取基本用户信息
        var basicUserInfo = new
        {
            姓名 = principal.FindFirst(ClaimTypes.Name)?.Value,
            邮箱 = principal.FindFirst(ClaimTypes.Email)?.Value,
            用户ID = principal.FindFirst("oid")?.Value
        };

        // 4. 用访问令牌调用SharePoint REST API获取站点内用户信息
        var siteUrl = _configuration["SharePoint:SiteUrl"];
        var spUserApiUrl = $"{siteUrl}/_api/web/currentuser?select=Title,LoginName,Email";
        using var httpClient = new HttpClient();
        httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", model.AccessToken);
        httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json;odata=nometadata"));

        var spResponse = await httpClient.GetAsync(spUserApiUrl);
        if (spResponse.IsSuccessStatusCode)
        {
            var spUser = await spResponse.Content.ReadFromJsonAsync<SharePointUser>();
            return Json(new
            {
                basicUserInfo.姓名,
                basicUserInfo.邮箱,
                basicUserInfo.用户ID,
                SharePoint职位 = spUser.Title,
                SharePoint登录名 = spUser.LoginName
            });
        }
        else
        {
            // 如果SharePoint API调用失败,返回基本用户信息
            return Json(basicUserInfo);
        }
    }
}

// 模型类定义
public class TokenRequest
{
    public string IdToken { get; set; } = string.Empty;
    public string AccessToken { get; set; } = string.Empty;
    public string State { get; set; } = string.Empty;
}

public class SharePointUser
{
    public string Title { get; set; } = string.Empty;
    public string LoginName { get; set; } = string.Empty;
    public string Email { get; set; } = string.Empty;
}

四、安全注意事项

  • 必须使用HTTPS:隐式流的令牌会暴露在前端,HTTPS可以防止令牌被窃听
  • 验证state和nonce:这两个参数是防止CSRF和重放攻击的关键,必须严格验证
  • 限制令牌有效期:隐式流的access_token默认有效期较短(通常1小时),不要长期存储
  • 避免在前端存储敏感数据:access_token不要存在localStorage中,尽量用内存存储,用完即弃
  • 权限最小化:只请求机器人需要的必要权限,不要过度申请

内容的提问来源于stack exchange,提问作者Soufien Hajji

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 07:29:00