完整案例——配置前端和后端API应用的安全认证——基于Azure实现

这篇文章记录了我的一些实践。官方文档是 https://docs.microsoft.com/en-us/azure/app-service/tutorial-auth-aad?pivots=platform-linux

案例场景

  1. 我有一个API 服务,用dotnet core 编写的
  2. 我有一个前端网站,用React 编写的
  3. 我希望这个前端网站,可以安全地访问到API服务
  4. 我不希望其他人在没有经过登录的情况下,直接访问到这个API服务

关键技术

  1. 两个应用都是需要启用身份认证的。这个官方文档采用的方案是利用最新的Azure的功能,叫做Easy Auth , 官方文档在这里 https://docs.microsoft.com/en-us/azure/app-service/overview-authentication-authorization。 我们采用Azure Active Directory来做认证,所以会创建对应的两个Azure AD application。
  2. 配置前端应用对应的Azure AD application, 让他可以访问后端API应用。

 

  1. 配置API 应用对应的Azure AD application, 让他自动授权,信任前端应用对应的Azure AD application。(这一步是官方文档中没有的,但这样添加了更加方便,因为不会弹出一个让用户额外授权的提示框)。这里比较有意思的还有,就是可以添加一个或多个scope,这个可以在后续的代码中验证,实现类似于Microsoft Graph的效果。

     

     

     

  2. 配置前端应用在做身份认证时,顺带就把访问后端API服务的id_token取过来。这一步很关键。需要访问 https://resources.azure.com 这个网站进行修改authSettings.

     

    "additionalLoginParams": [

    "response_type=code id_token",

    "resource=ee8a72b8-81f1-4a2f-b98c-aa394559f487"

    ],

     

  3. 为了让前端应用(React)可以访问到这个后端API 服务,还需要设置后端API服务的CORS

     

    请注意,如果不想做CORS的控制,则可以取消 "Enable Access-Control-Allow-Credentials" 这个复选框,然后在Allowed Origins 中删除所有的地址,输入一个 * 就可以了。

     

  4. 这样配置完后,当用户尝试去打开前端这个React 应用时,会自动弹出Azure AD 的身份认证的窗口,并且自动完成认证。那么如何在React中得到对应的ID_TOKEN呢?有意思的是,这里只要访问 /.auth/me 这个地址即可获得。然后就可以用这个access_token去继续访问后端的API服务了

     

    fetch("/.auth/me")

    .then(res => {

    return res.json()

    })

    .then(data => {

    const token = data[0].access_token;

    /* 读取天气数据 */

    let remote_url = "https://weatherservice-ares.azurewebsites.net/WeatherForecast";

     

    fetch(remote_url, {

    headers: {

    'Authorization': 'bearer ' + token

    }

    })

    .then(res => {

    return res.json();

    })

    .then(items => {

    setLoaded(true);

    setItems(items)

    });

    })

     

     

    题外话:如果前端这个应用,不是用React写的静态网页,而也是一个服务器技术开发的网页,例如ASP.NET Core,可以使用下面的方式进行access_token的传递。也就是说,Azure 提供的Easy Auth 会自动地把用户登录后得到的token,在每个请求的header中,通过 X-MS-TOKEN-AAD-ACCESS-TOKEN 这个传递过来。

     

    public override void OnActionExecuting(ActionExecutingContext context)

    {

    base.OnActionExecuting(context);

     

    _client.DefaultRequestHeaders.Accept.Clear();

    _client.DefaultRequestHeaders.Authorization =

    new AuthenticationHeaderValue("Bearer", Request.Headers["X-MS-TOKEN-AAD-ACCESS-TOKEN"]);

    }

     

  5. 如何在API服务端判断用户的身份,包括租户信息,账号信息呢。下面几行几行代码即可

 

var user = HttpContext.User.Identity.Name;

var provider = HttpContext.User.FindFirst("http://schemas.microsoft.com/identity/claims/identityprovider")?.Value;

var tid = HttpContext.User.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid")?.Value;

var oid = HttpContext.User.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier")?.Value;

var scp = HttpContext.User.FindFirst("http://schemas.microsoft.com/identity/claims/scope")?.Value;

 

    但是这里会有一个问题,默认情况下,你上面获取到的信息都是空白的。这是一个已知的问题,需要通过一个第三方库来解决。 https://github.com/MaximRouiller/MaximeRouiller.Azure.AppService.EasyAuth

 

    具体的做法就是,添加这个package : MaximeRouiller.Azure.AppService.EasyAuth,然后注入服务

    services.AddAuthentication().AddEasyAuthAuthentication((o) => { });

 

    然后在具体的Controller或者Action上面添加

    [Authorize(AuthenticationSchemes = "EasyAuth")]

 

  1. 如何判断当前的token是否具有指定的scope,以确定哪些用户能访问什么服务。

     

    这个我们可以通过封装一个类来检测

     

    using Microsoft.AspNetCore.Http;

    using Microsoft.AspNetCore.Mvc;

    using Microsoft.AspNetCore.Mvc.Filters;

    using System;

    using System.Collections.Generic;

    using System.Linq;

    using System.Threading.Tasks;

     

    namespace webapisample

    {

    public static class HttpContextExtension

    {

    public static bool VerifyUserHasAnyAcceptedScope(this HttpContext ctx, string[] scopes)

    {

    var scp = ctx.User.FindFirst("http://schemas.microsoft.com/identity/claims/scope")?.Value;

    if (string.IsNullOrEmpty(scp))

    return false;

     

    return scp.Split(' ').Intersect(scopes).Count() == scopes.Count();

    }

    }

    public class ScopeFilterAttribute : Attribute, IActionFilter

    {

    public string[] Scopes { get; set; }

     

    public void OnActionExecuted(ActionExecutedContext context)

    {

     

    }

     

    public void OnActionExecuting(ActionExecutingContext context)

    {

    if (!context.HttpContext.VerifyUserHasAnyAcceptedScope(Scopes))

    context.Result = new UnauthorizedResult();

    }

    }

    }

     

    这个ScopeFilter使用起来也很简单,如下所示

     

    [ScopeFilter(Scopes = new string[] { "Files.Read" })]

posted @ 2020-11-25 15:17  陈希章  阅读(734)  评论(0编辑  收藏  举报