diff --git a/AuthStudy.Authentication.Browser/BrowserAuthenticationDefault.cs b/AuthStudy.Authentication.Browser/BrowserAuthenticationDefault.cs index 901d31c..8edcf44 100644 --- a/AuthStudy.Authentication.Browser/BrowserAuthenticationDefault.cs +++ b/AuthStudy.Authentication.Browser/BrowserAuthenticationDefault.cs @@ -12,8 +12,8 @@ namespace AuthStudy.Authentication.Browser public const string DispayName = "浏览器方案"; - public static List AllowBrowsers = new List() { "Chrome", "Edge", "Firefox" }; + public static List AllowBrowsers { get; set; } = new() { "Chrome", "Edge", "Firefox" }; - public static BrowserAuthenticationOptions DefaultOptions = new BrowserAuthenticationOptions(); + public static BrowserAuthenticationOptions DefaultOptions = new(); } } diff --git a/AuthStudy.Authentication.Browser/BrowserAuthenticationExtensions.cs b/AuthStudy.Authentication.Browser/BrowserAuthenticationExtensions.cs index d0e5a72..f5ca336 100644 --- a/AuthStudy.Authentication.Browser/BrowserAuthenticationExtensions.cs +++ b/AuthStudy.Authentication.Browser/BrowserAuthenticationExtensions.cs @@ -1,45 +1,62 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.DependencyInjection; namespace AuthStudy.Authentication.Browser { public static class BrowserAuthenticationExtensions { - public static AuthenticationBuilder AddBrowser(this AuthenticationBuilder builder) + public static IServiceCollection AddBrowserAuthentication + ( + this IServiceCollection builder, + string AuthenticationSchemeName, + string AuthenticationDispalyName, + BrowserAuthenticationOptions Option + ) { - return builder.AddBrowser(BrowserAuthenticationDefault.SchemeName); - } + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + builder.AddService(Option.DefaultAuthenticateScheme, Option); - public static AuthenticationBuilder AddBrowser(this AuthenticationBuilder builder, string authenticationScheme) - { - return builder.AddBrowser(authenticationScheme, configureOptions: null); - } + builder.AddAuthentication(options => + { + options.DefaultScheme = AuthenticationSchemeName; + options.DefaultAuthenticateScheme = AuthenticationSchemeName; + options.DefaultChallengeScheme = AuthenticationSchemeName; + options.DefaultForbidScheme = AuthenticationSchemeName; + options.DefaultSignInScheme = AuthenticationSchemeName; + options.DefaultSignOutScheme = AuthenticationSchemeName; + options.AddScheme(AuthenticationSchemeName, AuthenticationDispalyName); + }); - public static AuthenticationBuilder AddBrowser(this AuthenticationBuilder builder, Action configureOptions) - { - return builder.AddBrowser(BrowserAuthenticationDefault.SchemeName, configureOptions); + return builder; } - public static AuthenticationBuilder AddBrowser - ( - this AuthenticationBuilder builder, - string authenticationScheme, - Action configureOptions - ) + + private static IServiceCollection AddService(this IServiceCollection builder, string defaultSchemeName, BrowserAuthenticationOptions option) { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } + defaultSchemeName = defaultSchemeName ?? BrowserAuthenticationDefault.SchemeName; + + BrowserAuthenticationOptions defaultOption = option ?? new(); + defaultOption.DefaultScheme ??= defaultSchemeName; + defaultOption.DefaultAuthenticateScheme ??= defaultSchemeName; + defaultOption.DefaultChallengeScheme ??= defaultSchemeName; + defaultOption.DefaultForbidScheme ??= defaultSchemeName; + + builder.AddSingleton(defaultOption); + builder.AddSingleton(); + return builder; - //return builder.AddScheme>(authenticationScheme, "", configureOptions); } } } diff --git a/AuthStudy.Authentication.Browser/BrowserAuthenticationHandler.cs b/AuthStudy.Authentication.Browser/BrowserAuthenticationHandler.cs index 41b9acf..5186b6a 100644 --- a/AuthStudy.Authentication.Browser/BrowserAuthenticationHandler.cs +++ b/AuthStudy.Authentication.Browser/BrowserAuthenticationHandler.cs @@ -20,24 +20,22 @@ namespace AuthStudy.Authentication.Browser /// 浏览器认证 处理器 /// 可实现子接口 IAuthenticationRequestHandler, 以控制后续中间件是否执行. /// - public class BrowserAuthenticationHandler : - IAuthenticationHandler, IAuthenticationRequestHandler, + public class BrowserAuthenticationHandler : + IAuthenticationHandler, + IAuthenticationRequestHandler, IAuthenticationSignInHandler, IAuthenticationSignOutHandler - where TOptions : AuthenticationSchemeOptions, new() { public string DefaultSchemeName = BrowserAuthenticationDefault.SchemeName; public HttpContext? CurrentHttpContext; - public List AllowBrowsers = BrowserAuthenticationDefault.AllowBrowsers; + public BrowserAuthenticationOptions Options; - public BrowserAuthenticationOptions Options = BrowserAuthenticationDefault.DefaultOptions; - - public BrowserAuthenticationHandler() + public BrowserAuthenticationHandler(BrowserAuthenticationOptions option) { - + Options = option; } /// @@ -46,7 +44,7 @@ namespace AuthStudy.Authentication.Browser public Task AuthenticateAsync() { //认证结果 - AuthenticateResult result = AuthenticateResult.NoResult(); + AuthenticateResult result; //属性 var properties = new AuthenticationProperties(); @@ -80,7 +78,7 @@ namespace AuthStudy.Authentication.Browser } //浏览器类型认证 - if (!AllowBrowsers.Contains(clientInfo.UA.Family)) + if (!Options.AllowBrowsers.Contains(clientInfo.UA.Family)) { properties.UpdateTokenValue("AuthenticationBrowser", "失败:不支持的浏览器"); result = AuthenticateResult.Fail($"不支持的浏览器:{clientInfo.UA.Family}", properties); @@ -90,14 +88,16 @@ namespace AuthStudy.Authentication.Browser //声明(身份项) var browser = new Claim("Browser", clientInfo.UA.ToString()); //浏览器 - var os = new Claim("OS", clientInfo.OS.ToString()); //操作系统 - var device = new Claim("Device", clientInfo.Device.ToString()); //设备 //设备 + var os = new Claim("OS", clientInfo.OS.ToString()); //操作系统 + var device = new Claim("Device", clientInfo.Device.ToString()); //设备 //设备 //声明集合 - var Claims = new List(); - Claims.Add(browser); - Claims.Add(os); - Claims.Add(device); + var Claims = new List + { + browser, + os, + device + }; //身份:包含声明集合,是声明集合的包装类,一个身份对应多个声明 var claimsIdentity = new ClaimsIdentity(Claims, DefaultSchemeName); @@ -120,7 +120,7 @@ namespace AuthStudy.Authentication.Browser /// /// 无认证:服务端向客户端(浏览器)发质询(要求提供一个新票据),质询体现为 htpp请求的响应。 /// - public Task ChallengeAsync(AuthenticationProperties? properties) + public async Task ChallengeAsync(AuthenticationProperties? properties) { properties?.Parameters.Add("x-itme", "无效的认证"); @@ -128,24 +128,24 @@ namespace AuthStudy.Authentication.Browser if (CurrentHttpContext?.Response.Body.CanWrite ?? false) { var msg = UTF8Encoding.UTF8.GetBytes("认证无效"); - CurrentHttpContext!.Response.Body.WriteAsync(msg); + await CurrentHttpContext!.Response.Body.WriteAsync(msg); } CurrentHttpContext?.Items.Add("认证结束时间", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); - return Task.CompletedTask; + //return Task.CompletedTask; } /// /// 无权限:服务端向客户端(浏览器)发质询(要求提供一个新票据),质询体现为 htpp请求的响应。 /// - public Task ForbidAsync(AuthenticationProperties? properties) + public async Task ForbidAsync(AuthenticationProperties? properties) { CurrentHttpContext!.Response.StatusCode = 403; if (CurrentHttpContext?.Response.Body.CanWrite ?? false) { var msg = UTF8Encoding.UTF8.GetBytes("无权访问"); - CurrentHttpContext!.Response.Body.WriteAsync(msg); + await CurrentHttpContext!.Response.Body.WriteAsync(msg); } - return Task.CompletedTask; + //return Task.CompletedTask; } /// @@ -193,7 +193,7 @@ namespace AuthStudy.Authentication.Browser return Task.CompletedTask; } - private bool IsMobile(string deviceInfo) + private static bool IsMobile(string deviceInfo) { bool isMobile = false; @@ -202,7 +202,7 @@ namespace AuthStudy.Authentication.Browser return isMobile; } - Regex phoneRegex = new Regex(@"(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino", RegexOptions.IgnoreCase | RegexOptions.Multiline); + Regex phoneRegex = new(@"(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino", RegexOptions.IgnoreCase | RegexOptions.Multiline); if (phoneRegex.IsMatch(deviceInfo)) { isMobile = true; diff --git a/AuthStudy.Authentication.Browser/BrowserAuthenticationHandler2.cs b/AuthStudy.Authentication.Browser/BrowserAuthenticationHandler2.cs new file mode 100644 index 0000000..e54e55f --- /dev/null +++ b/AuthStudy.Authentication.Browser/BrowserAuthenticationHandler2.cs @@ -0,0 +1,217 @@ +using System.Security; +using System.Security.Claims; +using System.Security.Policy; +using System.Security.Principal; +using System.Text; +using System.Text.RegularExpressions; +using System.Text.Unicode; +using System.Threading.Tasks.Sources; + +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; + +using UAParser; + +namespace AuthStudy.Authentication.Browser +{ + /// + /// 浏览器认证 处理器 + /// 可实现子接口 IAuthenticationRequestHandler, 以控制后续中间件是否执行. + /// + public class BrowserAuthenticationHandler2 : + IAuthenticationHandler, + IAuthenticationRequestHandler, + IAuthenticationSignInHandler, + IAuthenticationSignOutHandler + where TOptions : AuthenticationSchemeOptions, new() + { + public string DefaultSchemeName = BrowserAuthenticationDefault.SchemeName; + + public HttpContext? CurrentHttpContext; + + public List AllowBrowsers = BrowserAuthenticationDefault.AllowBrowsers; + + public BrowserAuthenticationOptions Options = BrowserAuthenticationDefault.DefaultOptions; + + + public BrowserAuthenticationHandler2() + { + + } + + /// + /// 认证 + /// + public Task AuthenticateAsync() + { + //认证结果 + AuthenticateResult result; + + //属性 + var properties = new AuthenticationProperties(); + properties.Items.Add("AuthenticationBrowser", "浏览器认证属性"); + + //获取请求浏览器信息,如果请头重复则以后面的为准 + var userAgent = CurrentHttpContext?.Request.Headers["User-Agent"].LastOrDefault(); + if (userAgent == null) + { + properties.UpdateTokenValue("AuthenticationBrowser", "失败:获取不到浏览器信息"); + result = AuthenticateResult.Fail($"失败:获取不到浏览器信息", properties); + return Task.FromResult(result); + } + + ClientInfo clientInfo = Parser.GetDefault().Parse(userAgent); + + //移动设备认证 + if (!Options.AllowMobile && BrowserAuthenticationHandler2.IsMobile(clientInfo.UA.Family)) + { + properties.UpdateTokenValue("AuthenticationBrowser", "失败:不被允许的可移动设备"); + result = AuthenticateResult.Fail($"不被允许的可移动设备:{clientInfo.UA.Family}", properties); + return Task.FromResult(result); + } + + //爬虫认证 + if (!Options.AllowSpider && clientInfo.Device.IsSpider) + { + properties.UpdateTokenValue("AuthenticationBrowser", "失败:不允许爬虫"); + result = AuthenticateResult.Fail($"不允许爬虫", properties); + return Task.FromResult(result); + } + + //浏览器类型认证 + if (!AllowBrowsers.Contains(clientInfo.UA.Family)) + { + properties.UpdateTokenValue("AuthenticationBrowser", "失败:不支持的浏览器"); + result = AuthenticateResult.Fail($"不支持的浏览器:{clientInfo.UA.Family}", properties); + + return Task.FromResult(result); + } + + //声明(身份项) + var browser = new Claim("Browser", clientInfo.UA.ToString()); //浏览器 + var os = new Claim("OS", clientInfo.OS.ToString()); //操作系统 + var device = new Claim("Device", clientInfo.Device.ToString()); //设备 //设备 + + //声明集合 + var Claims = new List + { + browser, + os, + device + }; + + //身份:包含声明集合,是声明集合的包装类,一个身份对应多个声明 + var claimsIdentity = new ClaimsIdentity(Claims, DefaultSchemeName); + + //当事人/主角:是身份Identity的包装,对应多个身份 + var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); + + //票据:对Principal的包装,一对一 + var ticket = new AuthenticationTicket(claimsPrincipal, DefaultSchemeName); + + //认证结果:认证信息会写入 当前请求的 User属性中,供下一个授权中间件使用 + result = AuthenticateResult.Success(ticket); + + //调用登陆 + //CurrentHttpContext?.SignInAsync(BrowserAuthentication.DefaultAuthenticationScheme, claimsPrincipal, properties); + + return Task.FromResult(result); + } + + /// + /// 无认证:服务端向客户端(浏览器)发质询(要求提供一个新票据),质询体现为 htpp请求的响应。 + /// + public async Task ChallengeAsync(AuthenticationProperties? properties) + { + properties?.Parameters.Add("x-itme", "无效的认证"); + + CurrentHttpContext!.Response.StatusCode = 401; + if (CurrentHttpContext?.Response.Body.CanWrite ?? false) + { + var msg = UTF8Encoding.UTF8.GetBytes("认证无效"); + await CurrentHttpContext!.Response.Body.WriteAsync(msg); + } + CurrentHttpContext?.Items.Add("认证结束时间", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + //return Task.CompletedTask; + } + + /// + /// 无权限:服务端向客户端(浏览器)发质询(要求提供一个新票据),质询体现为 htpp请求的响应。 + /// + public async Task ForbidAsync(AuthenticationProperties? properties) + { + CurrentHttpContext!.Response.StatusCode = 403; + if (CurrentHttpContext?.Response.Body.CanWrite ?? false) + { + var msg = UTF8Encoding.UTF8.GetBytes("无权访问"); + await CurrentHttpContext!.Response.Body.WriteAsync(msg); + } + //return Task.CompletedTask; + } + + /// + /// IAuthenticationRequestHandler + /// 返回true,立即反回,不执行后续中间件 + /// + public Task HandleRequestAsync() + { + return Task.FromResult(false); + } + + /// + /// 初始化 + /// + public async Task InitializeAsync(AuthenticationScheme scheme, Microsoft.AspNetCore.Http.HttpContext context) + { + //初始化工作,传递给认证方法和授权中间件 + CurrentHttpContext = context; + + context.Items.Add("认证初始时间", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + + await Task.CompletedTask; + } + + /// + /// 登陆方法 + /// 写入Cookie和Session,认证信息持久化等 + /// + public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties? properties) + { + //导航到主页 + //CurrentHttpContext?.Response.Redirect("/api/app/index"); + + return Task.CompletedTask; + } + + /// + /// 退出方法: 反操作登陆方法 + /// 清除Cookie和Session,删除认证信息的持久化,作废票据等 + /// + public Task SignOutAsync(AuthenticationProperties? properties) + { + //导航到登陆页 + CurrentHttpContext?.Response.Redirect("/api/auth/login"); + return Task.CompletedTask; + } + + private static bool IsMobile(string deviceInfo) + { + bool isMobile = false; + + if (string.IsNullOrWhiteSpace(deviceInfo)) + { + return isMobile; + } + + Regex phoneRegex = new(@"(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino", RegexOptions.IgnoreCase | RegexOptions.Multiline); + if (phoneRegex.IsMatch(deviceInfo)) + { + isMobile = true; + } + + return isMobile; + } + } +} \ No newline at end of file diff --git a/AuthStudy.Authentication.Browser/BrowserAuthenticationOptions.cs b/AuthStudy.Authentication.Browser/BrowserAuthenticationOptions.cs index 66f8937..2a9f618 100644 --- a/AuthStudy.Authentication.Browser/BrowserAuthenticationOptions.cs +++ b/AuthStudy.Authentication.Browser/BrowserAuthenticationOptions.cs @@ -8,7 +8,7 @@ using Microsoft.AspNetCore.Authentication; namespace AuthStudy.Authentication.Browser { - public class BrowserAuthenticationOptions : AuthenticationSchemeOptions + public class BrowserAuthenticationOptions : AuthenticationOptions { /// /// 允许的浏览器 diff --git a/AuthStudy.WebApp/Controllers/AccountsController.cs b/AuthStudy.WebApp/Controllers/AccountsController.cs index f44b9cd..f63c2d2 100644 --- a/AuthStudy.WebApp/Controllers/AccountsController.cs +++ b/AuthStudy.WebApp/Controllers/AccountsController.cs @@ -1,4 +1,4 @@ -using AuthStudy.WebApp.VModels; +using AuthStudy.WebApp.VModels; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; @@ -10,7 +10,10 @@ namespace AuthStudy.WebApp.Controllers [ApiController] public class AccountsController : ControllerBase { - public AccountsController() { } + public AccountsController() + { + + } [Authorize] [HttpGet] @@ -22,7 +25,7 @@ namespace AuthStudy.WebApp.Controllers Console.WriteLine($"{claim.Type}={claim.Value}"); } - List accounts = new List() + List accounts = new() { new AccountVM(){ Name="张三", Email="zhangsan@qq.com", Password="123456"}, new AccountVM(){ Name="小明", Email="xiaoming@qq.com", Password="123456"}, @@ -33,9 +36,9 @@ namespace AuthStudy.WebApp.Controllers } [HttpPost] - public IActionResult Login(string loginName, string loginPassword) + public IActionResult Login(string LoginName, string LoginPassword) { - var info = new { Name = "", Roles = "Admin" }; + var info = new { Name = LoginName, Roles = "Admin" }; return new JsonResult(info); } diff --git a/AuthStudy.WebApp/Program.cs b/AuthStudy.WebApp/Program.cs index 600798d..5d4e8f8 100644 --- a/AuthStudy.WebApp/Program.cs +++ b/AuthStudy.WebApp/Program.cs @@ -7,7 +7,7 @@ namespace AuthStudy.WebApp { public static void Main(string[] args) { - var builder = WebApplication.CreateBuilder(args); + WebApplicationBuilder? builder = WebApplication.CreateBuilder(args); // Add services to the container. @@ -17,25 +17,30 @@ namespace AuthStudy.WebApp builder.Services.AddSwaggerGen(); #region 认证注册 - builder.Services.AddAuthentication(configOption => - { - configOption.AddScheme>(BrowserAuthenticationDefault.SchemeName, BrowserAuthenticationDefault.DispayName); - }); + //builder.Services.AddAuthentication(configOption => + //{ + // configOption.AddScheme>(BrowserAuthenticationDefault.SchemeName, BrowserAuthenticationDefault.DispayName); + //}); + builder.Services.AddBrowserAuthentication + ( + BrowserAuthenticationDefault.SchemeName, + BrowserAuthenticationDefault.DispayName, + new BrowserAuthenticationOptions() + { + AllowBrowsers = new List() { "Edge" } + } + ); #endregion - var app = builder.Build(); + WebApplication? app = builder.Build(); // Configure the HTTP request pipeline. - if (app.Environment.IsDevelopment()) - { - app.UseSwagger(); - app.UseSwaggerUI(); - } + app.UseSwagger(); + app.UseSwaggerUI(); app.UseAuthentication(); app.UseAuthorization(); - app.MapControllers(); app.Run(); diff --git a/AuthStudy.WebApp/VModels/AccountVM.cs b/AuthStudy.WebApp/VModels/AccountVM.cs index 695fb02..49d350a 100644 --- a/AuthStudy.WebApp/VModels/AccountVM.cs +++ b/AuthStudy.WebApp/VModels/AccountVM.cs @@ -2,10 +2,10 @@ { public class AccountVM { - public string Name { get; set; } + public string? Name { get; set; } - public string Email { get; set; } + public string? Email { get; set; } - public string Password { get; set; } + public string? Password { get; set; } } }