You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

213 lines
8.2 KiB
C#

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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
{
/// <summary>
/// 浏览器认证处理器:基于接口实现
/// 可实现子接口 IAuthenticationRequestHandler, 以控制后续中间件是否执行.
/// </summary>
public class BrowserAuthenticationBaseHandler :
IAuthenticationHandler,
IAuthenticationSignInHandler,
IAuthenticationSignOutHandler
{
public string DefaultSchemeName = BrowserAuthenticationDefault.SchemeName;
public HttpContext? CurrentHttpContext;
public BrowserAuthenticationOptions Options;
public BrowserAuthenticationBaseHandler(BrowserAuthenticationOptions option)
{
Options = option;
}
/// <summary>
/// 认证
/// </summary>
public Task<AuthenticateResult> AuthenticateAsync()
{
//认证结果
AuthenticateResult result;
//认证属性
var properties = new AuthenticationProperties();
properties.Items.Add("AuthenticationBrowser", "浏览器认证属性");
//获取请求浏览器信息,如果请头重复则以后面的为准
var userAgent = CurrentHttpContext?.Request.Headers["User-Agent"].ToString();
if (string.IsNullOrWhiteSpace(userAgent))
{
properties.UpdateTokenValue("AuthenticationBrowser", "失败:获取不到浏览器信息");
result = AuthenticateResult.Fail($"失败:获取不到浏览器信息", properties);
return Task.FromResult(result);
}
ClientInfo clientInfo = Parser.GetDefault().Parse(userAgent);
//移动设备认证
if (!Options.AllowMobile && 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 (!Options.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<Claim>
{
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);
}
/// <summary>
/// 无认证:服务端向客户端(浏览器)发质询(要求提供一个新票据),质询体现为 http请求的响应。
/// </summary>
public async Task ChallengeAsync(AuthenticationProperties? properties)
{
properties?.Parameters.Add("x-item", "无效的认证");
CurrentHttpContext!.Response.StatusCode = 401;
if (CurrentHttpContext?.Response.Body.CanWrite ?? false)
{
var msg = Encoding.UTF8.GetBytes("认证无效");
await CurrentHttpContext!.Response.Body.WriteAsync(msg);
}
CurrentHttpContext?.Items.Add("认证结束时间", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
//return Task.CompletedTask;
}
/// <summary>
/// 无权限:服务端向客户端(浏览器)发质询(要求提供一个新票据),质询体现为 http请求的响应。
/// </summary>
public async Task ForbidAsync(AuthenticationProperties? properties)
{
CurrentHttpContext!.Response.StatusCode = 403;
if (CurrentHttpContext?.Response.Body.CanWrite ?? false)
{
var msg = Encoding.UTF8.GetBytes("无权访问");
await CurrentHttpContext!.Response.Body.WriteAsync(msg);
}
//return Task.CompletedTask;
}
/// <summary>
/// IAuthenticationRequestHandler
/// 返回true,立即反回,不执行后续中间件
/// </summary>
public Task<bool> HandleRequestAsync()
{
return Task.FromResult(false);
}
/// <summary>
/// 初始化
/// </summary>
public async Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
{
//初始化工作,传递给认证方法和授权中间件
CurrentHttpContext = context;
context.Items.Add("认证初始时间", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
await Task.CompletedTask;
}
/// <summary>
/// 登陆方法
/// 写入Cookie和Session认证信息持久化等
/// </summary>
public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties? properties)
{
//导航到主页
//CurrentHttpContext?.Response.Redirect("/api/app/index");
return Task.CompletedTask;
}
/// <summary>
/// 退出方法: 反操作登陆方法
/// 清除Cookie和Session删除认证信息的持久化作废票据等
/// </summary>
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;
}
}
}