Bot Builder:如何在SharePoint中使用OAuth 2隐式授权流创建机器人
我刚好做过类似的.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
相关产品推荐
相关产品推荐

