歡迎光臨
每天分享高質量文章

ASP.NET Core 專案簡單實現身份驗證及鑒權

環境

  • VS 2017
  • ASP.NET Core 2.2

標的

  以相對簡單優雅的方式實現使用者身份驗證和鑒權,解決以下兩個問題:

  • 無狀態的身份驗證服務,使用請求頭附加訪問令牌,幾乎適用於手機、網頁、桌面應用等所有客戶端
  • 基於功能點的許可權訪問控制,可以將任意功能點許可權集合授予使用者或角色,無需硬編碼角色許可權,非常靈活

專案準備

  1. 建立一個ASP.NET Core Web應用程式

  • 使用ASP.NET Core 2.2
  • 模板選[空]
  • 不啟用HTTPS
  • 不進行身份驗證
  • 透過NuGet安裝Swashbuckle.AspNetCore程式包,併在Startup類中啟用Swagger支援

    因為這個示例專案不打算編寫前端網頁,所以直接使用Swagger來除錯,真的很方便。

  • 新增一個空的MVC控制器(HomeController)和一個空的API控制器(AuthController)

    HomeController.Index()方法中只寫一句簡單的跳轉程式碼即可:

    return new RedirectResult("~/swagger");

    AuthController類中隨便寫一兩個骨架方法,方便看效果。

  • 執行專案,會自動開啟瀏覽器並跳轉到Swagger頁面。

身份驗證

定義基本型別和介面

  1. ClaimTypes 定義一些常用的宣告型別常量

  2. IClaimsSession 表示當前會話資訊的介面

  3. ClaimsSession 會話資訊實現類
    根據宣告型別從ClaimsPrincipal.ClaimsIdentity屬性中讀取使用者ID、使用者名稱等資訊。

    實際專案中可從此類繼承或完全重新實現自己的Session類,以新增更多的會話資訊(例如工作部門)

  4. IToken 登入令牌介面
    包含訪問令牌、掃清令牌、令牌時效等令牌

  5. IIdentity 身份證明介面
    包含使用者基本資訊及令牌資訊

  6. IAuthenticationService 驗證服務介面
    抽象出來的驗證服務介面,僅規定了四個身份驗證相關的方法,如需擴充套件可定義由此介面派生的介面。

    Login(userName, password) IIdentity 根據使用者名稱及密碼驗證其身份,成功則傳回身份證明
    Logout() void 登出本次登入,即使未登入也不報錯
    RefreshToken(refreshToken) Token 掃清登入令牌,如果當前使用者未登入則報錯
    ValidateToken(accessToken) IIdentity 驗證訪問令牌,成功則傳回身份證明
  7. SimpleToken 登入令牌的簡化實現

    這個類提不提供都可以,實際專案中大家生成Token的演演算法肯定是各不相同的,提供簡單實現僅用於演示

編寫驗證處理器

  1. BearerDefaults 定義了一些與身份驗證相關的常量

    如:AuthenticationScheme

  2. BearerOptions 身份驗證選項類

    AuthenticationSchemeOptions繼承而來

  3. BearerValidatedContext 驗證結果背景關係

  4. BearerHandler 身份驗證處理器 <= 關鍵類

    改寫了HandleAuthenticateAsync()方法,實現自定義的身份驗證邏輯,簡述如下:

    1. 獲取訪問令牌。從請求頭中獲取authorization資訊,如果沒有則從請求的引數中獲取

    2. 如果訪問令牌為空,則終止驗證,但不報錯,直接傳回AuthenticateResult.NoResult()

    3. 呼叫從建構式註入的IAuthenticationService實體的ValidateToken()方法,驗證訪問令牌是否有效,如果該方法觸發異常(例如令牌過期)則捕獲後透過AuthenticateResult.Fail()傳回錯誤資訊,如果該方法傳回值為空(例如訪問令牌根本不存在)則傳回AuthenticateResult.NoResult(),不報錯。

    4. 到這一步說明身份驗證已經透過,而且拿到身份證明資訊,根據該資訊建立Claim陣列,然後再建立一個包含這些Claim資料的ClaimsPrincipal實體,並將Thread.CurrentPrincipal設定為該實體。

      重點:其實,HttpContext.User屬性的型別正是CurrentPrincipal,而其值應該就是來自於Thread.CurrentPrincipal

    5. 構造BearerValidatedContext實體,並將其Principal屬性賦值為上面建立的ClaimsPrincipal實體,然後呼叫Success()方法,表示驗證成功。最後傳回該實體的Result屬性值。

  5. BearerExtensions 包含一些擴充套件方法,提供使用便利

    重點在於AddBearer()方法內呼叫builder.AddScheme()泛型方法時,分別使用了前面編寫的BearerOptionsBearerHandler類作為泛型引數。

    public static AuthenticationBuilder AddBearer(...)
    {
        return builder.AddScheme(...);
    }

    如果想要自己實現BearerHandler類的驗證邏輯,可以拋棄此類,重新編寫使用新Handler類的擴充套件方法

實現使用者身份驗證

說明

  這部分是身份驗證的落地,實際專案中應該將上面兩步(定義基本型別和介面、編寫驗證處理器)的程式碼抽象出來,成為獨立可復用的軟體包,利用該軟體包進行身份驗證的實現邏輯可參照此示例程式碼。

實現步驟

  1. Identity 身份證明實現類

  2. SampleAuthenticationService 驗證服務的簡單實現

    出於演示方便,固化了三個使用者(admin/123456、user/123、tester/123)

  3. AuthController 透過HTTP向前端提供驗證服務的控制器類

    提供了使用者登入、令牌掃清、令牌驗證等方法。

  4. 還需要修改專案中Startup.cs檔案,新增依賴註入規則、身份驗證,並啟用身份驗證中介軟體。
    ConfigureServices方法內新增程式碼:

    
    services.AddScoped();
    services.AddScoped();
    
    services.AddAuthentication(options =>
    {
     options.DefaultAuthenticateScheme = BearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = BearerDefaults.AuthenticationScheme;
    }).AddBearer();

    Configure()方法內新增程式碼:

    
    app.UseAuthentication();

透過Swagger測試

  • 測試登入功能

    啟動專案,自動進入[Swagger UI]介面,點選/api/Auth/Login方法,不修改輸入框中的內容直接點選[Execute]按鈕,可以見到傳回401錯誤碼。

    在輸入框中輸入{"userName": "admin", "password": "123456"},然後點選[Execute]按鈕,系統驗證成功並傳回身份證明資訊。

記下訪問令牌2ad43df2c11d48a18a88441adbf4994a和掃清令牌9bbaf811ed8b4d29b638777d4f89238e

  • 測試掃清登入令牌

    點選/api/Auth/Refresh方法,在輸入框中輸入上面獲取到的掃清令牌9bbaf811ed8b4d29b638777d4f89238e,然後點選[Execute]按鈕,傳回401錯誤碼。原因是因為我們並未提供訪問令牌。

    點選方法名右側的[鎖]圖示,在彈出框中輸入之前獲取的訪問令牌2ad43df2c11d48a18a88441adbf4994a並點選[Authorize]按鈕後關閉對話方塊,重新點選[Execute]按鈕,成功獲取到新的登入令牌。

  • 測試驗證訪問令牌

    點選/api/Auth/Validate方法,在輸入框中輸入第一次獲取的到訪問令牌2ad43df2c11d48a18a88441adbf4994a,然後點選[Execute]按鈕,傳回400錯誤碼,表明發起的請求引數有誤。因為此方法是支援匿名訪問的,所以錯誤碼不會是401.

    將輸入框內容修改為新的訪問令牌f37542e162ed4855921ddf26b05c3f25,然後點選[Execute]按鈕,驗證成功,傳回了對應的使用者身份證明資訊。

許可權鑒定

  在ASP.NET Core專案中實現基於角色的授權很容易,在一些許可權管理並不複雜的專案中,採取這種方式來實現許可權鑒定簡單可行。有興趣可以參考這篇博文ASP.NET Core 認證與授權5:初識授權
但是,對於稍微複雜一些的專案,許可權劃分又細又多,如果採用這種方式,要改寫到各種各樣的許可權組合,需要在程式碼中定義相當多的角色,大大增加專案維護工作,並且很不靈活。
這裡借鑒ABP框架中許可權鑒定的一些思想,來實現基於功能點的許可權訪問控制。
非常感謝ASP.NET Core和ABP等諸多優秀的開源專案,向你們致敬!
不得不說ABP框架非常優秀,但是我並不喜歡使用它,因為我沒有能力和精力搞清楚它的詳細設計思路,而且很多功能我根本不需要。

思路

  ASP.NET Core提供了一個IAuthorizationFilter介面,如果在控制器類上新增[授權過濾]特性,相應的AuthorizationFilter類的OnAuthorization()方法會在控制器的Action之前執行,如果在該方法中設定AuthorizationFilterContext.Result為一個錯誤的response,Action將不會被呼叫。

基於這個思路,我們設計了以下方案:

  1. 編寫一個Attribute(特性)類,包含以下兩個屬性:

    Permissions:需要檢查的許可權陣列

    RequireAllPermissions:是否需要擁有陣列中全部許可權,如果為否則擁有任一許可權即可

  2. 定義一個IPermissionChecker介面,在介面中定義IsGrantedAsync()方法,用於執行許可權鑒定邏輯

  3. 編寫一個AuthorizationFilterAttribute特性類(應用標的為class),透過屬性註入IPermissionChecker實體。然後在OnAuthorization()方法內呼叫IPermissionChecker實體的IsGrantedAsync()方法,如果該方法傳回值為false,則傳回403錯誤,否則正常放行。

編寫過濾器類及相關介面

  1. ApiAuthorizeAttribute類

        [AttributeUsage(AttributeTargets.Method)]
        public class ApiAuthorizeAttribute : Attribute, IFilterMetadata
        {
            public string[] Permissions { get; }
    
            public bool RequireAllPermissions { get; set; }
    
            public ApiAuthorizeAttribute(params string[] permissions)
            {
                Permissions = permissions;
            }
        }
  2. IPermissionChecker介面定義

        public interface IPermissionChecker
        {
            Task<bool> IsGrantedAsync(string permissionName);
        }
  3. AuthorizationFilterAttribute類

        [AttributeUsage(AttributeTargets.Class)]
        public class AuthorizationFilterAttribute : Attribute, IAuthorizationFilter
        {
            [Injection] 
            public IPermissionChecker PermissionChecker { get; set; } = NullPermissionChecker.Instance;
    
            public void OnAuthorization(AuthorizationFilterContext context)
            {
                if(存在[AllowAnonymous]特性) return;
                var authorizeAttribute = 從context.Filters中析出ApiAuthorizeAttribute
                foreach (var permission in authorizeAttribute.Permissions)
                {
                 
                 var granted = PermissionChecker.IsGrantedAsync(permission).Result;
                }
                if(檢查未透過)
                 context.Result = new ObjectResult("未授權") { StatusCode = 403 };
            }
        }
  4. 配合屬性註入提供NullPermissionChecker類,在IsGrantedAsync()方法內直接傳回true。

實現屬性註入

  做好上面的準備,我們應該可以開始著手在專案內應用許可權鑒定功能了,不過ASP.NET Core內建的DI框架並不支援屬性註入,所以還得新增屬性註入的功能。

  1. 定義InjectionAttribute類,用於顯式宣告應用了此特性的屬性將使用依賴註入

    
    
    
    [AttributeUsage(AttributeTargets.Property)]
    public class InjectionAttribute : Attribute { }
  2. 新增一個PropertiesAutowiredFilterProvider類,從DefaultFilterProvider類派生

    public class PropertiesAutowiredFilterProvider : DefaultFilterProvider
    {
        private static IDictionary<string, IEnumerable> _publicPropertyCache = new Dictionary<string, IEnumerable>();
    
        public override void ProvideFilter(FilterProviderContext context, FilterItem filterItem)
        {
            base.ProvideFilter(context, filterItem); 
            var filterType = filterItem.Filter.GetType();
            if (!_publicPropertyCache.ContainsKey(filterType.FullName))
            {
                var ps=filterType.GetProperties(BindingFlags.Public|BindingFlags.Instance)
                    .Where(c => c.GetCustomAttribute() != null);
                _publicPropertyCache[filterType.FullName] = ps;
            }
    
            var injectionProperties = _publicPropertyCache[filterType.FullName];
            if (injectionProperties?.Count() == 0)
                return;
            
            var serviceProvider = context.ActionContext.HttpContext.RequestServices;
            foreach (var item in injectionProperties)
            {
                var service = serviceProvider.GetService(item.PropertyType);
                if (service == null)
                {
                    throw new InvalidOperationException($"Unable to resolve service for type '{item.PropertyType.FullName}' while attempting to activate '{filterType.FullName}'");
                }
                item.SetValue(filterItem.Filter, service);
            }
        }
    }
  3. 還有非常關鍵的一步,在Startup.ConfigureServices()中新增下麵的程式碼,替換IFilterProvider介面的實現類為上面編寫的PropertiesAutowiredFilterProvider

    services.Replace(ServiceDescriptor.Singleton<Microsoft.AspNetCore.Mvc.Filters.IFilterProvider, PropertiesAutowiredFilterProvider>());

實現使用者許可權鑒定

  終於,我們可以在專案內應用許可權鑒定功能了。

編碼

  1. 首先,我們定義一些功能點許可權常量

    public static class PermissionNames
    {
        public const string TestAdd = "Test.Add";
        public const string TestEdit = "Test.Edit";
        public const string TestDelete = "Test.Delete";
    }
  2. 接著,新增一個新的用於測試的控制器類

        [AuthorizationFilter]
        [Route("api/[controller]")]
        [ApiController]
        public class TestController : ControllerBase
        {
            [Injection]
            public IClaimsSession Session { get; set; }
    
            [HttpGet]
            [Route("[action]")]
            public IActionResult CurrentUser() => Ok(Session?.UserName);
    
            [ApiAuthorize]
            [HttpGet("{id}")]
            public IActionResult Get(int id)=> Ok(id);
    
            [ApiAuthorize(PermissionNames.TestAdd)]
            [HttpPost]
            [Route("[action]")]
            public IActionResult Create()=> Ok();
    
            [ApiAuthorize(PermissionNames.TestEdit, RequireAllPermissions = false)]
            [HttpPost]
            [Route("[action]")]
            public IActionResult Update()=> Ok();
    
            [ApiAuthorize(PermissionNames.TestAdd, PermissionNames.TestEdit, RequireAllPermissions = false)]
            [HttpPost]
            [Route("[action]")]
            public IActionResult Patch() => Ok();
    
            [ApiAuthorize(PermissionNames.TestDelete)]
            [HttpDelete("{id}")]
            public IActionResult Delete(int id) => Ok();
        }

    在控制器類上添加了[AuthorizationFilter]特性,除了CurrentUser()方法以外,都添加了[ApiAuthorize]特性,所需的許可權各不相同,為簡化測試所有的Action都直接傳回OkResult

  3. 實現一個用於演示的許可權檢查器類

    public class SamplePermissionChecker : IPermissionChecker
    {
        private readonly Dictionary<long, string[]> userPermissions = new Dictionary<long, string[]>
        {
            
            { 1, new[] { PermissionNames.TestAdd, PermissionNames.TestEdit, PermissionNames.TestDelete } },
            
            { 2, new[] { PermissionNames.TestEdit, PermissionNames.TestDelete } }
        };
    
        public IClaimsSession Session { get; }
    
        
        public SamplePermissionChecker(IClaimsSession session)
        {
            this.Session = session;
        }
    
        public Task<bool> IsGrantedAsync(string permissionName)
        {
            if(!userPermissions.Any(p => p.Key == Session.UserId))
                return Task.FromResult(false);
            var up = userPermissions.Where(p => p.Key == Session.UserId).First();
            var granted = up.Value.Any(permission => permission.Equals(permissionName, StringComparison.InvariantCultureIgnoreCase));
            return Task.FromResult(granted);
        }
    
    }
  4. 最後還需要修改專案中Startup.cs檔案,新增依賴註入規則

    services.AddSingleton<IPermissionChecker, SamplePermissionChecker>();

    因為SamplePermissionChecker類中並沒有需要行程間隔離的資料,所以使用單例樣式註冊就可以了。不過這樣一來,因為該類透過建構式註入了IClaimsSession介面實體,在構建Checker類實體時將觸發異常。考慮到CliamsSession類中只有方法沒有資料 ,改為單例也並無妨,於是將該介面也改為單例樣式註冊。

透過Swagger測試

  • 測試未登入時僅可訪問/api/Test/CurrentUser

  • 測試以使用者user登入,可以訪問/api/Test/CurrentUser和GET請求/api/Test/{id}

  • 測試以使用者admin登入,可以訪問除/api/Test/Add以外的介面

測試

編寫了命令列程式,用來測試前面實現的Web API服務。

測試不同使用者同時訪問時Session是否正確

  • 測試方法

    同時執行三個測試程式,都選擇[測試身份驗證],然後分別輸入不同的使用者身份序號,快速切換三個程式並按下回車鍵,三個測試程式會各自發起100次請求,每次請求間隔100毫秒。

    例如同時開啟三個命令列終端執行:dotnet .\CustomAuthorization.test.dll

  • 測試結果

    三個測試程式從後臺服務所獲取到的當前使用者資訊完成匹配。

測試以不同使用者身份訪問需要許可權的介面

  • 測試方法

    預設的許可權為:admin=>全部許可權,user=>除Test.Add以外許可權,tester=>無。

    分別以admin、user、tester三個使用者身份請求/api/test下的所有介面,並模擬令牌過期的場景。

  • 測試結果

    可以見到,以過期的令牌發起請求時,後臺傳回的狀態為Unauthorized,當使用者未獲得足夠的授權時後臺傳回的狀態為Forbidden。

    測試透過!

最後

原始碼託管在gitee.com :https://gitee.com/xant77/CustomAuthorization.WebApi

原文地址:https://www.cnblogs.com/wiseant/p/10515842.html

贊(0)

分享創造快樂