Cube/NewLife.CubeNC/WebMiddleware/RunTimeMiddleware.cs

244 lines
8.0 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Diagnostics;
using System.Web;
using Microsoft.AspNetCore.Http.Features;
using NewLife.Common;
using NewLife.Cube.Entity;
using NewLife.Cube.Services;
using NewLife.Cube.ViewModels;
using NewLife.Cube.Web;
using NewLife.Log;
using NewLife.Security;
using NewLife.Web;
using XCode.DataAccessLayer;
using XCode.Membership;
using HttpContext = Microsoft.AspNetCore.Http.HttpContext;
namespace NewLife.Cube.WebMiddleware;
/// <summary>运行时中间件。页面查询执行时间、异常拦截</summary>
public class RunTimeMiddleware
{
private readonly RequestDelegate _next;
private readonly UserService _userService;
private readonly AccessService _accessService;
/// <summary>会话提供者</summary>
static readonly SessionProvider _sessionProvider = new();
/// <summary>实例化</summary>
/// <param name="next"></param>
/// <param name="userService"></param>
/// <param name="accessService"></param>
public RunTimeMiddleware(RequestDelegate next, UserService userService, AccessService accessService)
{
_next = next ?? throw new ArgumentNullException(nameof(next));
_userService = userService;
_accessService = accessService;
}
/// <summary>调用</summary>
/// <param name="ctx"></param>
/// <returns></returns>
public async Task Invoke(HttpContext ctx)
{
// 分析浏览器
var userAgent = ctx.Request.Headers.UserAgent + "";
var ua = new UserAgentParser();
ua.Parse(userAgent);
ctx.Items["UserAgent"] = ua;
// 识别拦截爬虫
if (!WebHelper.ValidRobot(ctx, ua)) return;
// 强制访问Https
if (MiddlewareHelper.CheckForceRedirect(ctx)) return;
// 创建Session集合。后续 ManageProvider.User 需要用到Session
var session = CreateSession(ctx);
var url = ctx.Request.GetRawUrl();
var ip = ctx.GetUserHost();
ManageProvider.UserHost = ip;
// 获取当前用户。先找Items再找Session2没有自动登录能力
var user = ManageProvider.User;
// 安全访问
var rule = _accessService.Valid(url + "", ua, ip, user, session);
if (rule != null && rule.ActionKind is AccessActionKinds.Block or AccessActionKinds.Limit)
{
if (rule.BlockCode == 302)
{
ctx.Response.Redirect(rule.BlockContent);
}
else if (rule.BlockCode > 0)
{
ctx.Response.StatusCode = rule.BlockCode;
ctx.Response.ContentType = "text/html; charset=utf-8";
await ctx.Response.WriteAsync(rule.BlockContent);
await ctx.Response.CompleteAsync();
}
else
{
ctx.Abort();
}
return;
}
var inf = new RunTimeInfo();
ctx.Items[nameof(RunTimeInfo)] = inf;
var query = DAL.QueryTimes;
var execute = DAL.ExecuteTimes;
var sw = Stopwatch.StartNew();
inf.QueryTimes = query;
inf.ExecuteTimes = execute;
inf.Watch = sw;
// 设计时收集执行的SQL语句
if (SysConfig.Current.Develop)
{
inf.Sqls = [];
DAL.LocalFilter = s => inf.Sqls.Add(s);
}
// 日志控制精确标注Web类型线程
WriteLogEventArgs.Current.IsWeb = true;
var online = session["Online"] as UserOnline;
try
{
var set = CubeSetting.Current;
var p = ctx.Request.Path + "";
if (!p.EndsWithIgnoreCase(ExcludeSuffixes) &&
(set.EnableUserOnline == 2 || set.EnableUserOnline == 1 && user != null))
{
// 浏览器设备标识作为会话标识
var deviceId = WebHelper.FillDeviceId(ctx);
//var sessionId = token?.MD5_16() ?? ip;
var sessionId = deviceId;
online = _userService.SetWebStatus(online, sessionId, deviceId, p, userAgent, ua, user, ip);
//FillDeviceId(ctx, olt);
session["Online"] = online;
ctx.Items["Cube_Online"] = online;
}
await _next.Invoke(ctx);
}
catch (Exception ex)
{
var uri = ctx.Request.GetRawUrl();
online?.SetError(ex.Message);
XTrace.Log.Error("[{0}]的错误[{1}] {2}", uri, ip, ctx.TraceIdentifier);
LogProvider.Provider?.WriteLog("访问", "错误", false, ex.Message + " " + uri + Environment.NewLine + ex.GetMessage(), user?.ID ?? 0, user + "", ip);
XTrace.WriteException(ex);
// 传递给异常处理页面
ctx.Items["Exception"] = new ErrorModel
{
RequestId = DefaultSpan.Current?.TraceId ?? Activity.Current?.Id ?? ctx.TraceIdentifier,
Uri = uri,
Exception = ex
};
throw;
}
finally
{
sw.Stop();
DAL.LocalFilter = null;
ManageProvider.UserHost = null;
// 日志控制
WriteLogEventArgs.Current.IsWeb = false;
}
}
/// <summary>执行时间字符串</summary>
public static String DbRunTimeFormat { get; set; } = "查询{0}次,执行{1}次,耗时{2:n0}毫秒";
/// <summary>获取执行时间和查询次数等信息</summary>
/// <param name="ctx"></param>
/// <returns></returns>
public static String GetInfo(HttpContext ctx)
{
if (ctx.Items[nameof(RunTimeInfo)] is not RunTimeInfo rtinf) return null;
var inf = String.Format(DbRunTimeFormat,
DAL.QueryTimes - rtinf.QueryTimes,
DAL.ExecuteTimes - rtinf.ExecuteTimes,
rtinf.Watch.ElapsedMilliseconds);
// 设计时收集执行的SQL语句
if (SysConfig.Current.Develop)
{
var list = rtinf.Sqls;
if (list != null && list.Count > 0) inf += "<br />" + list.Select(e => HttpUtility.HtmlEncode(e)).Join("<br />" + Environment.NewLine);
}
return inf;
}
private static IDictionary<String, Object> CreateSession(HttpContext ctx)
{
// 准备Session避免未启用Session时ctx.Session直接抛出异常
//var ss = ctx.Session;
var ss = ctx.Features.Get<ISessionFeature>()?.Session;
if (ss != null && !ss.IsAvailable) ss = null;
// 关键点在于确定一个唯一的SessionId没有Session和Cookie时使用令牌
var key = ".Cube.Session";
var sid = "";
if (sid.IsNullOrEmpty()) sid = ss?.GetString(key);
if (sid.IsNullOrEmpty()) sid = ctx.Request.Cookies[key];
if (sid.IsNullOrEmpty()) sid = ctx.Request.Cookies[".AspNetCore.Session"];
if (sid.IsNullOrEmpty()) sid = ctx.LoadToken();
if (sid.IsNullOrEmpty())
{
sid = Rand.NextString(16);
if (ss != null)
ss.SetString(key, sid);
else
ctx.Response.Cookies.Append(key, sid);
}
else
{
// 避免过长
if (sid.Length > 32) sid = sid.MD5();
ss?.SetString(key, sid);
}
var session = _sessionProvider.GetSession(sid);
ctx.Items["Session"] = session;
return session;
}
/// <summary>忽略的后缀</summary>
public static String[] ExcludeSuffixes { get; set; } = new[] {
".html", ".htm", ".js", ".css", ".map", ".png", ".jpg", ".gif", ".ico", // 脚本样式图片
".woff", ".woff2", ".svg", ".ttf", ".otf", ".eot" // 字体
};
}
/// <summary>运行时间信息</summary>
public class RunTimeInfo
{
/// <summary>查询次数</summary>
public Int32 QueryTimes { get; set; }
/// <summary>执行次数</summary>
public Int32 ExecuteTimes { get; set; }
/// <summary>执行耗时</summary>
public Stopwatch Watch { get; set; }
/// <summary>查询次数</summary>
public IList<String> Sqls { get; set; }
}