开发框架Furion之WebApi+SqlSugar (二)
限速(Rate-Limiting)系统可以控制网络接口发送和接受流量的速率。对于Web API来说,
目录
目录
关于项目框架的基础搭建参见开发框架Furion之WebApi+SqlSugar (一)
1.JSON序列化之long 长整型数据精度丢失设置
有时候我们需要将 long 类型序列化时转为 string 类型,防止 JavaScript 出现精度溢出问题
MyFurion.Start项目中的Startup.cs ConfigureServices中配置如下代码
System.Text.Json 方式
services.AddControllersWithViews().AddJsonOptions(options =>
{
//long类型数据防止精度丢失设置
options.JsonSerializerOptions.Converters.AddLongTypeConverters();
});
Newtonsoft.Json 方式
services.AddControllersWithViews().AddNewtonsoftJson(options =>
{
//统一日期类型返回
options.SerializerSettings.DateFormatString = "yyyy-MM-dd HH:mm:ss";
//long数据类型精度丢失问题解决
options.SerializerSettings.Converters.AddLongTypeConverters();
});
关于更多的JSON序列化配置参见 23. JSON 序列化 | Furion
2.API接口请求速率限制配置
什么是限速
限速(Rate-Limiting)系统可以控制网络接口发送和接受流量的速率。对于Web API来说,限速系统被用来控制一段时间内某个程序或客户端允许调用某个API的次数,超过该次数的流量会被拒绝。例如Github的API只允许开发者每小时发送5000次请求。
限速策略
在对API限速之前,您首先要考虑好限速的策略,通常一个好的限速策略有以下这两个特性:
- 易于理解,易于解释,易于使用
- 针对特殊情况,对开发者可以不限速
限速策略考虑还需考虑的问题:
- 颗粒速率限制还是全局速率限制
- 针对比较简单的系统,很多开发者采用的是全局速率限制,但是如果某个API消费了大量的资源,你可能需要为每个API单独进行速率限制。所以颗粒的速率限制会保护你的基础架构不被任何耗资巨大的API节点所引起的无理流量尖峰造成严重影响。
- 测量每个用户、应用、客户端
- 你想要进行速率限制的资源还依赖于您的API的身份认证方式。需要用户身份认证的API通常可以按照用户进行速率限制,而需要应用身份认证的API通常是基于每个应用来进行速率限制。对于未认证的API,我能想到的就是按照IP地址进行速率限制。
- 是否支持突发流量
- 有些API,尤其是企业内部的系统,需要支持超过速率限制的突发流量。这种情况下,可以采用令牌桶算法(Token Bucket)来实现速率限制。
- 是否允许例外
- 有时候,对于应用的开发者来说,一个限速策略或一组限速策略可能都不太适用。对于你信任的开发者,如果他们的请求超出了配额,可能需要给他们一些例外的允许,但是在此之前,你要做这些工作:
- 保证每个开发者的用例对客户来说都是合理有益的。
- 要确认确实没有其它不超出约束的办法来达到相同的目的。
- 确保你的基础架构确实可以支撑想要请求的速率。
针对ASP.NET Core Web API项目,如果不采用网关的话考虑使用AspNetCoreRateLimit
限速实现
在MyFurion.Start项目中,通过Nuget添加 AspNetCoreRateLimit,然后在Handlers文件夹中创建IPRateExtension
using AspNetCoreRateLimit;
namespace MyFurion.Start
{
public static class IPRateExtension
{
public static void AddIPRate(this IServiceCollection services)
{
if (services == null) throw new ArgumentNullException(nameof(services));
//从appsettings.json中加载常规配置,IpRateLimiting与配置文件中节点对应
services.Configure<IpRateLimitOptions>(App.Configuration.GetSection("IpRateLimiting"));
//从appsettings.json中加载Ip规则
services.Configure<IpRateLimitPolicies>(App.Configuration.GetSection("IpRateLimitPolicies"));
//注入计数器和规则存储
services.AddSingleton<IIpPolicyStore, MemoryCacheIpPolicyStore>();
services.AddSingleton<IRateLimitCounterStore, MemoryCacheRateLimitCounterStore>();
//配置(解析器、计数器密钥生成器)
services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();
services.AddSingleton<IProcessingStrategy, AsyncKeyLockProcessingStrategy>();
}
}
}
Startup中的ConfigureServices中添加代码
services.AddIPRate();//ip请求频率限制
Startup中的Configure中添加代码
app.UseIpRateLimiting();//启用客户端IP限制速率
appsettings.json配置文件中配置限制参数
//接口请求IP速率限制
"IpRateLimiting": {
//例如设置了5次每分钟访问限流。当False时:项目中每个接口都加入计数,不管你访问哪个接口,只要在一分钟内累计够5次,将禁止访问。
//True:当一分钟请求了5次GetData接口,则该接口将在时间段内禁止访问,但是还可以访问PostData()5次,总得来说是每个接口都有5次在这一分钟,互不干扰。
"EnableEndpointRateLimiting": true,
//false,拒绝的API调用不会添加到调用次数计数器上;如 客户端每秒发出3个请求并且您设置了每秒一个调用的限制,则每分钟或每天计数器等其他限制将仅记录第一个调用,即成功的API调用。如果您希望被拒绝的API调用计入其他时间的显示(分钟,小时等)
//则必须设置StackBlockedRequests为true。
"StackBlockedRequests": false,
"RealIpHeader": "X-Real-IP",
//取白名单的客户端ID。如果此标头中存在客户端ID并且与ClientWhitelist中指定的值匹配,则不应用速率限制。
"ClientIdHeader": "X-ClientId",
"HttpStatusCode": 429, //返回状态码
//端点白名单
"EndpointWhitelist": [], //"post:/api/sysAuth/login","*:/logout"
//返回消息内容
"QuotaExceededResponse": {
"Content": "{{\"success\":false,\"code\":429,\"message\":\"访问过于频繁,请稍后重试\",\"data\":null}}",
"ContentType": "application/json",
"StatusCode": 429
},
//通用规则,api规则,结尾一定要带*
"GeneralRules": [
{
"Endpoint": "*",
"Period": "3s", //时间段,格式:{数字}{单位};可使用单位:s, m, h, d
"Limit": 50//限制访问的次数
}
],
"IpRateLimitPolicies": {
//ip规则
"IpRules": [
]
}
}
3.JWT Token 认证
3.1登录验证码
MyFurion.Unility项目,通过Nuget添加ZKWeb.System.Drawing,创建Generic文件夹
然后在Generic文件夹下创建CaptchaHelper.cs类文件,实现登录验证码的创建
using System.ComponentModel;
using System.DrawingCore;
using System.DrawingCore.Imaging;
namespace MyFurion.Unility.Generic
{
/// <summary>
/// 验证码
/// </summary>
public class CaptchaHelper
{
/// <summary>
/// 获取验证码
/// </summary>
/// <param name="n">验证码数</param>
/// <param name="type">类型 0:数字 1:字符</param>
/// <returns></returns>
public static VerifyCode CreateVerifyCode(int n, VerifyCodeType type)
{
int codeW = 170;//宽度
int codeH = 50;//高度
int fontSize = 32;//字体大小
//初始化验证码
string charCode = string.Empty;
string resultCode = "";
switch (type.ToString())
{
case "NUM":
charCode = CreateNumCode(n);
break;
case "ARITH":
charCode = CreateArithCode(out resultCode);
n = charCode.Length;
break;
default:
charCode = CreateCharCode(n);
break;
}
//颜色列表
Color[] colors = { Color.Black, Color.Red, Color.Blue, Color.Green, Color.Orange, Color.Brown, Color.DarkBlue };
//字体列表
string[] fonts = { "Times New Roman", "Verdana", "Arial", "Gungsuh" };
//创建画布
Bitmap bitmap = new Bitmap(codeW, codeH);
Graphics graphics = Graphics.FromImage(bitmap);
graphics.Clear(Color.White);
Random random = new Random();
//画躁线
for (int i = 0; i < n; i++)
{
int x1 = random.Next(codeW);
int y1 = random.Next(codeH);
int x2 = random.Next(codeW);
int y2 = random.Next(codeH);
Color color = colors[random.Next(colors.Length)];
Pen pen = new Pen(color);
graphics.DrawLine(pen, x1, y1, x2, y2);
}
//画噪点
for (int i = 0; i < 100; i++)
{
int x = random.Next(codeW);
int y = random.Next(codeH);
Color color = colors[random.Next(colors.Length)];
bitmap.SetPixel(x, y, color);
}
//画验证码
for (int i = 0; i < n; i++)
{
string fontStr = fonts[random.Next(fonts.Length)];
Font font = new Font(fontStr, fontSize);
Color color = colors[random.Next(colors.Length)];
graphics.DrawString(charCode[i].ToString(), font, new SolidBrush(color), (float)i * 30 + 5, (float)0);
}
//写入内存流
try
{
MemoryStream stream = new MemoryStream();
bitmap.Save(stream, ImageFormat.Jpeg);
VerifyCode verifyCode = new VerifyCode()
{
Code = type.ToString() == "ARITH" ? resultCode : charCode,
Image = stream.ToArray()
};
return verifyCode;
}
//释放资源
finally
{
graphics.Dispose();
bitmap.Dispose();
}
}
/// <summary>
/// 获取数字验证码
/// </summary>
/// <param name="n">验证码数</param>
/// <returns></returns>
public static string CreateNumCode(int n)
{
char[] numChar = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
string charCode = string.Empty;
Random random = new Random();
for (int i = 0; i < n; i++)
{
charCode += numChar[random.Next(numChar.Length)];
}
return charCode;
}
/// <summary>
/// 获取字符验证码
/// </summary>
/// <param name="n">验证码数</param>
/// <returns></returns>
public static string CreateCharCode(int n)
{
char[] strChar = { 'a', 'b','c','d','e','f','g','h','i','j','k','l','m',
'n','o','p','q','r','s','t','u','v','w','x','y','z','0','1','2','3',
'4','5','6','7','8','9','A','B','C','D','E','F','G','H','I','J','K',
'L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'};
string charCode = string.Empty;
Random random = new Random();
for (int i = 0; i < n; i++)
{
charCode += strChar[random.Next(strChar.Length)];
}
return charCode;
}
/// <summary>
/// 获取运算符验证码
/// </summary>
/// <returns></returns>
public static string CreateArithCode(out string resultCode)
{
string checkCode = "";
Random random = new Random();
int intFirst = random.Next(1, 20);//生成第一个数字
int intSec = random.Next(1, 20);//生成第二个数字
int intTemp = 0;
switch (random.Next(1, 3).ToString())
{
case "2":
if (intFirst < intSec)
{
intTemp = intFirst;
intFirst = intSec;
intSec = intTemp;
}
checkCode = intFirst + "-" + intSec + "=";
resultCode = (intFirst - intSec).ToString();
break;
default:
checkCode = intFirst + "+" + intSec + "=";
resultCode = (intFirst + intSec).ToString();
break;
}
return checkCode;
}
}
/// <summary>
/// 验证码信息
/// </summary>
public class VerifyCode
{
/// <summary>
/// 验证码
/// </summary>
public string Code { get; set; }
/// <summary>
/// 验证码数据流
/// </summary>
public byte[] Image { get; set; }
/// <summary>
/// base64
/// </summary>
public string Base64Str { get { return Convert.ToBase64String(Image); } }
}
/// <summary>
/// 验证码类型
/// </summary>
public enum VerifyCodeType
{
[Description("纯数字验证码")]
NUM = 0,
[Description("数字加字母验证码")]
CHAR = 1,
[Description("数字运算验证码")]
ARITH = 2,
}
}
3.2用户登录实现
redis缓存配置
MyFurion.Unility项目,通过Nuget 添加Microsoft.Extensions.Caching.StackExchangeRedis
Generic文件加下创建CacheHelper类文件
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Distributed;
namespace MyFurion.Unility.Generic
{
/// <summary>
/// Microsoft.Extensions.Caching.redis 缓存
/// </summary>
public class CacheHelper
{
private readonly IDistributedCache _cache;
public CacheHelper(IDistributedCache cache)
{
_cache = cache;
}
/// <summary>
/// 设置缓存
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
/// <param name="time">过期时间 单位秒</param>
public void SetRedisCache(string key,string value,int time)
{
_cache.Set(key, Encoding.UTF8.GetBytes(value), new DistributedCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromSeconds(time)));
}
/// <summary>
/// 获取缓存
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public string GetReidsCache(string key)
{
byte[]?valueByte=_cache.Get(key);
return valueByte != null ? Encoding.Default.GetString(valueByte) : null;
}
/// <summary>
/// 删除缓存
/// </summary>
/// <param name="key"></param>
public void DelRedisCache(string key)
{
_cache.Remove(key);
}
}
}
appsettings.json配置文件中配置Redis数据库连接
"RedisConnection": "127.0.0.1:6379,defaultDatabase=0,ssl=false,writeBuffer=10240"
MyFurion.Start项目,ConfigureServices中,配置redis连接
//redis 缓存配置
services.AddStackExchangeRedisCache(options =>
{
options.Configuration = App.GetConfig<string>("RedisConnection");// 连接字符串
options.InstanceName = "furion_"; // 键名前缀
});
登录接口实现
MyFurion.Model中创建UserInfo实体对象,在MyFurion.Application项目创建用户仓储及用户登录接口
MyFurion.Application项目, 通过Nuget 添加Furion.Extras.Authentication.JwtBearer
用户实体对象
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MyFurion.Model
{
/// <summary>
/// 用户信息
/// </summary>
[SugarTable("Sys_User")]
[Tenant(0)]
public class UserInfo:BaseEntity
{
/// <summary>
/// 用户姓名
/// </summary>
[SugarColumn(IsNullable =true,ColumnDescription = "用户姓名")]
public string? UserName { get; set; }
/// <summary>
/// 用户昵称
/// </summary>
[SugarColumn(IsNullable = true, ColumnDescription = "用户昵称")]
public string? NickName { get; set; }
/// <summary>
/// 登录账户
/// </summary>
[SugarColumn(IsNullable = false, ColumnDescription = "登录账户")]
public string? Account { get; set; }
/// <summary>
/// 登录密码
/// </summary>
[SugarColumn(IsNullable = false, ColumnDescription = "登录密码")]
public string? LoginPwd { get; set; }
/// <summary>
/// 联系电话
/// </summary>
[SugarColumn(IsNullable = true, ColumnDescription = "联系电话")]
public string? Tel { get; set; }
/// <summary>
/// 电子邮箱
/// </summary>
[SugarColumn(IsNullable = true, ColumnDescription = "电子邮箱")]
public string? Email { get; set; }
}
}
用户仓储
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MyFurion.Application
{
/// <summary>
/// 用户信息服务仓储
/// </summary>
public class UserRepository:BaseRepository<UserInfo>,ITransient
{
//TODO
}
}
appsettings.json配置JWT秘钥、加密方法等
"JWTSettings": {
"IssuerSigningKey": "Furion@#PassWord!@2022", // 密钥,string 类型,必须是复杂密钥,长度大于16
"ValidIssuer": "Furion@2022", // 签发方,string 类型
"ValidAudience": "Furion@Client", // 签收方,string 类型
"ExpiredTime": 30 // 过期时间,long 类型,单位分钟,默认20分钟5秒
//"Algorithm": "HS256" // 加密算法,string 类型,默认 HS256
}
登录接口
using Microsoft.Extensions.Caching.Distributed;
namespace MyFurion.Application.Controller
{
/// <summary>
/// Login
/// </summary>
[ApiDescriptionSettings(Name = "Login", Order = 2)]
[Route("api/sysLogin")]
public class LoginController:IDynamicApiController
{
private readonly UserRepository _userRepository;
private readonly IDistributedCache _cache;
private readonly IHttpContextAccessor _httpContextAccessor;
public LoginController(UserRepository userRepository, IDistributedCache cache, IHttpContextAccessor httpContextAccessor)
{
_userRepository = userRepository;
_cache = cache;
_httpContextAccessor = httpContextAccessor;
}
/// <summary>
/// 获取验证码
/// </summary>
/// <returns></returns>
[AllowAnonymous]
[HttpGet("getCaptcha")]
public object GetCaptcha()
{
string uuid = Guid.NewGuid().ToString().Replace("-", "");
CacheHelper cacheHelper = new CacheHelper(_cache);
var verifyCode = CaptchaHelper.CreateVerifyCode(4, VerifyCodeType.CHAR);
cacheHelper.SetRedisCache(uuid, verifyCode.Code, 300);
return new { uuid, img = verifyCode.Base64Str };
}
/// <summary>
/// 登录
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[AllowAnonymous]
[HttpPost("login")]
public async Task<string> Login(LoginInput input)
{
//验证码校验
CacheHelper cacheHelper = new CacheHelper(_cache);
string verifyCode = cacheHelper.GetReidsCache(input.CaptchaId);
if (!string.IsNullOrWhiteSpace(verifyCode))
{
if (!verifyCode.ToLower().Equals(input.Captcha.ToLower()))
{
throw Oops.Oh("验证码错误");
}
else
{
cacheHelper.DelRedisCache(input.CaptchaId);
}
}
else
{
throw Oops.Oh("验证码已失效");
}
//登录账号校验
string md5Pwd= MD5Encryption.Encrypt(input.Password);
var userInfo = await _userRepository.GetFirstAsync(it=>it.Account==input.LoginName&&it.LoginPwd==md5Pwd);
if (userInfo == null)
{
throw Oops.Oh("用户名或密码错误");
}
var accessToken = JWTEncryption.Encrypt(new Dictionary<string, object>
{
{"UserId", userInfo.Id},
{"UserName", userInfo.UserName},
{"NickName", userInfo.NickName}
});
// 设置Swagger自动登录
_httpContextAccessor.HttpContext.SigninToSwagger(accessToken);
// 生成刷新Token令牌
var refreshToken = JWTEncryption.GenerateRefreshToken(accessToken);
// 设置刷新Token令牌
_httpContextAccessor.HttpContext.Response.Headers["x-access-token"] = refreshToken;
return accessToken;
}
}
}
3.3JWT身份认证实现
MyFurion.Start项目中,Handlers文件夹创建JwtHandler类文件
using Furion.Authorization;
using Furion.DataEncryption;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
namespace MyFurion.Start
{
/// <summary>
/// JWT身份认证
/// </summary>
public class JwtHandler : AppAuthorizeHandler
{
/// <summary>
/// 管道请求
/// </summary>
/// <param name="context"></param>
/// <param name="httpContext"></param>
/// <returns></returns>
public override Task<bool> PipelineAsync(AuthorizationHandlerContext context, DefaultHttpContext httpContext)
{
// 这里写您的授权判断逻辑,授权通过返回 true,否则返回 false
return Task.FromResult(true);
}
/// <summary>
/// 重写 Handler 添加自动刷新
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public override async Task HandleAsync(AuthorizationHandlerContext context)
{
// 自动刷新Token
if (JWTEncryption.AutoRefreshToken(context, context.GetCurrentHttpContext(),App.GetOptions<JWTSettingsOptions>().ExpiredTime))
{
await AuthorizeHandleAsync(context);
}
else
{
context.Fail(); // 授权失败
DefaultHttpContext currentHttpContext = context.GetCurrentHttpContext();
if (currentHttpContext == null)
{
return;
}
currentHttpContext.SignoutToSwagger();
}
}
}
}
Startup中的ConfigureServices启用JWT身份认证
services.AddJwt<JwtHandler>(enableGlobalAuthorize: true);//启用Jwt 身份验证 全局权限
Configure中启用身份认证
app.UseAuthentication();
app.UseAuthorization();
4.日志
关于日志,使用IEventSubscriber时间订阅依赖接口实现,将日志存储到数据库中
4.1错误日志
MyFurion.Model项目,创建ErrorLog类文件,MyFurion.Application项目创建ErrorLog仓储
namespace MyFurion.Model
{
/// <summary>
/// 错误日志
/// </summary>
[SugarTable("Sys_ErrorLog")]
[Tenant(0)]
public class ErrorLog:BaseEntity
{
/// <summary>
/// 类名
/// </summary>
[SugarColumn(Length = 100, IsNullable = true, ColumnDescription = "类名")]
public string ClassName { get; set; }
/// <summary>
/// 方法名
/// </summary>
[SugarColumn(Length = 100, IsNullable = true, ColumnDescription = "方法名")]
public string MethodName { get; set; }
/// <summary>
/// 异常名称
/// </summary>
[SugarColumn(IsNullable = true, ColumnDataType = CommonConst.DB_STRING_MAX, ColumnDescription = "异常名称")]
public string ExceptionName { get; set; }
/// <summary>
/// 异常信息
/// </summary>
[SugarColumn(IsNullable = true, ColumnDataType = CommonConst.DB_STRING_MAX, ColumnDescription = "异常信息")]
public string ExceptionMsg { get; set; }
/// <summary>
/// 异常源
/// </summary>
[SugarColumn(IsNullable = true, ColumnDataType = CommonConst.DB_STRING_MAX, ColumnDescription = "异常源")]
public string ExceptionSource { get; set; }
/// <summary>
/// 堆栈信息
/// </summary>
[SugarColumn(IsNullable = true, ColumnDataType = CommonConst.DB_STRING_MAX, ColumnDescription = "堆栈信息")]
public string StackTrace { get; set; }
/// <summary>
/// 参数对象
/// </summary>
[SugarColumn(IsNullable = true, ColumnDataType = CommonConst.DB_STRING_MAX, ColumnDescription = "参数对象")]
public string ParamsObj { get; set; }
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MyFurion.Application
{
/// <summary>
/// 错误日志服务仓储
/// </summary>
public class ErrorLogRepository : BaseRepository<ErrorLog>, ITransient
{
//TODO
}
}
4.2请求审计日志
MyFurion.Model项目,创建RequestLog类文件,MyFurion.Application项目创建RequestLog仓储
namespace MyFurion.Model
{
/// <summary>
/// 请求审计日志
/// </summary>
[SugarTable("Sys_RequestLog")]
[Tenant(0)]
public class RequestLog:BaseEntity
{
/// <summary>
/// 是否执行成功
/// </summary>
[SugarColumn(ColumnDescription = "是否执行成功")]
public bool Success { get; set; }
/// <summary>
/// 具体消息
/// </summary>
[SugarColumn(IsNullable = true, ColumnDataType = CommonConst.DB_STRING_MAX, ColumnDescription = "具体消息")]
public string Message { get; set; }
/// <summary>
/// IP
/// </summary>
[SugarColumn(Length = 100, IsNullable = true, ColumnDescription = "ip地址")]
public string Ip { get; set; }
/// <summary>
/// 地址
/// </summary>
[SugarColumn(Length = 1024, IsNullable = true, ColumnDescription = "地址")]
public string Location { get; set; }
/// <summary>
/// 浏览器
/// </summary>
[SugarColumn(Length = 100, IsNullable = true, ColumnDescription = "浏览器")]
public string Browser { get; set; }
/// <summary>
/// 操作系统
/// </summary>
[SugarColumn(Length = 100, IsNullable = true, ColumnDescription = "操作系统")]
public string OsSystem { get; set; }
/// <summary>
/// 请求地址
/// </summary>
[SugarColumn(Length = 100, IsNullable = true, ColumnDescription = "请求地址")]
public string Url { get; set; }
/// <summary>
/// 类名称
/// </summary>
[SugarColumn(Length = 100, IsNullable = true, ColumnDescription = "类名称")]
public string ClassName { get; set; }
/// <summary>
/// 方法名称
/// </summary>
[SugarColumn(Length = 100, IsNullable = true, ColumnDescription = "方法名称")]
public string MethodName { get; set; }
/// <summary>
/// 请求方式(GET POST PUT DELETE)
/// </summary>
[SugarColumn(Length = 100, IsNullable = true, ColumnDescription = "请求方式")]
public string ReqMethod { get; set; }
/// <summary>
/// 请求参数
/// </summary>
[SugarColumn(IsNullable = true, ColumnDataType = CommonConst.DB_STRING_MAX, ColumnDescription = "请求参数")]
public string Param { get; set; }
/// <summary>
/// 返回结果
/// </summary>
[SugarColumn(IsNullable = true, ColumnDataType = CommonConst.DB_STRING_MAX, ColumnDescription = "返回结果")]
public string Result { get; set; }
/// <summary>
/// 耗时(毫秒)
/// </summary>
[SugarColumn(ColumnDescription = " 耗时(毫秒)")]
public long ElapsedTime { get; set; }
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MyFurion.Application
{
/// <summary>
/// 请求审计日志服务仓储
/// </summary>
public class RequestLogRepository : BaseRepository<RequestLog>, ITransient
{
//TODO
}
}
4.3日志订阅
MyFurion.Unility项目,Nuget添加Microsoft.AspNetCore.Http.Abstractions
MyFurion.Unility项目,创建获取IP地址类IPHelper
using Microsoft.AspNetCore.Http;
namespace MyFurion.Unility.Generic
{
public static class IPHelper
{
/// <summary>
/// 获取请求的ip4
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public static string GetRequestIPv4(this HttpContext context)
{
string ip = string.Empty;
if (context.Connection.RemoteIpAddress != null)
{
if (context.Request.Headers.ContainsKey("X-Real-IP"))
{
ip = context.Request.Headers["X-Real-IP"].FirstOrDefault();
}
if (context.Request.Headers.ContainsKey("X-Forwarded-For"))
{
ip = context.Request.Headers["X-Forwarded-For"].FirstOrDefault();
}
if (string.IsNullOrEmpty(ip))
{
ip = context.Connection.RemoteIpAddress?.MapToIPv4()?.ToString();
}
}
return ip;
}
}
}
MyFurion.Start项目,Nuget添加UAParser
MyFurion.Start项目,创建LogEventSubscriber日志事件订阅类文件
using MyFurion.Application;
using Furion.EventBus;
namespace MyFurion.Start
{
/// <summary>
/// 日志事件订阅依赖接口
/// </summary>
public class LogEventSubscriber : IEventSubscriber
{
/// <summary>
/// 请求日志
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
[EventSubscribe("Add:RequestLog")]
public async Task AddWorkLog(EventHandlerExecutingContext context)
{
var log = (RequestLog)context.Source.Payload;
await App.GetService<RequestLogRepository>().Add(log);
}
/// <summary>
/// 错误日志
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
[EventSubscribe("Add:ErrorLog")]
public async Task AddErrorLog(EventHandlerExecutingContext context)
{
var log = (ErrorLog)context.Source.Payload;
await App.GetService<ErrorLogRepository>().Add(log);
}
}
}
MyFurion.Start项目,创建全局异常处理类ErrorLogFilter
using Furion.DependencyInjection;
using Furion.EventBus;
using Furion.FriendlyException;
using Microsoft.AspNetCore.Mvc.Filters;
namespace MyFurion.Start
{
/// <summary>
/// 全局异常处理
/// </summary>
public class ErrorLogFilter : IGlobalExceptionHandler, ISingleton
{
private readonly IEventPublisher _eventPublisher;
/// <summary>
/// 全局异常处理
/// </summary>
/// <param name="eventPublisher"></param>
public ErrorLogFilter(IEventPublisher eventPublisher)
{
_eventPublisher = eventPublisher;
}
/// <summary>
/// 异常处理
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public async Task OnExceptionAsync(ExceptionContext context)
{
//var userContext = App.User;
await _eventPublisher.PublishAsync(new ChannelEventSource("Add:ErrorLog",
new ErrorLog
{
//Account = userContext?.FindFirstValue(ClaimConst.ClAIM_ACCOUNT) ?? String.Empty,
//ErrorName = userContext?.FindFirstValue(ClaimConst.ClAIM_NAME) ?? String.Empty,
ClassName = context.Exception.TargetSite?.DeclaringType?.FullName,
MethodName = context.Exception.TargetSite?.Name ?? String.Empty,
ExceptionName = context.Exception.Message,
ExceptionMsg = context.Exception.Message,
ExceptionSource = context.Exception.Source,
StackTrace = context.Exception.StackTrace,
ParamsObj = context.Exception.TargetSite?.GetParameters().ToString()
}));
// 写日志文件
Log.Error(context.Exception.ToString());
}
}
}
MyFurion.Start项目,创建请求日志处理类 RequestLogFilter
using Furion.EventBus;
using Furion.JsonSerialization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using System.Diagnostics;
using UAParser;
using MyFurion.Unility.Generic;
namespace MyFurion.Start
{
/// <summary>
/// 请求日志
/// </summary>
public class RequestLogFilter : IAsyncActionFilter
{
private readonly IEventPublisher _eventPublisher;
/// <summary>
/// 请求日志拦截
/// </summary>
/// <param name="eventPublisher"></param>
public RequestLogFilter(IEventPublisher eventPublisher)
{
_eventPublisher = eventPublisher;
}
/// <summary>
/// 请求处理(执行操作前后)
/// </summary>
/// <param name="context"></param>
/// <param name="next"></param>
/// <returns></returns>
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var httpContext = context.HttpContext;
var httpRequest = httpContext.Request;
var sw = new Stopwatch();
sw.Start();
var actionContext = await next();
sw.Stop();
// 判断是否请求成功(没有异常就是请求成功)
var isRequestSucceed = actionContext.Exception == null;
var headers = httpRequest.Headers;
var clientInfo = headers.ContainsKey("User-Agent") ? Parser.GetDefault().Parse(headers["User-Agent"]) : null;
var actionDescriptor = context.ActionDescriptor as ControllerActionDescriptor;
var ip = httpContext.GetRequestIPv4();
await _eventPublisher.PublishAsync(new ChannelEventSource("Add:RequestLog",
new RequestLog
{
Success=isRequestSucceed,
Ip = ip,
Location = httpRequest.GetRequestUrlAddress(),
Browser = clientInfo?.UA.Family + clientInfo?.UA.Major,
OsSystem = clientInfo?.OS.Family + clientInfo?.OS.Major,
Url = httpRequest.Path,
ClassName = context.Controller.ToString() ?? String.Empty,
MethodName = actionDescriptor?.ActionName ?? String.Empty,
ReqMethod = httpRequest.Method,
Param = context.ActionArguments.Count < 1 ? string.Empty : JSON.Serialize(context.ActionArguments),
Result = actionContext.Result?.GetType() == typeof(JsonResult) ? JSON.Serialize(actionContext.Result) : string.Empty,
ElapsedTime = sw.ElapsedMilliseconds
}));
}
}
}
MyFurion.Start项目,Startup ConfigureServices注册日志订阅事件及请求日志
// 注册EventBus服务
services.AddEventBus(builder =>
{
// 注册 Log 日志订阅者
builder.AddSubscriber<LogEventSubscriber>();
});
//全局注册请求日志
services.Configure<MvcOptions>(options =>
{
options.Filters.Add<RequestLogFilter>();
});
5.用户权限配置
一般我们用到的权限配置是菜单权限、按钮操作权限、数据域权限,此处不做详细说明了,大家可根据实际情况自行设计开发
6.附录
主要最终版源码的可以至MyFurion: 项目使用Furion开源框架、SqlSugar开源多库架构ORM框架、.Net6,用户登录、角色、权限、部门、菜单、数据字典、日志、多租户等基础功能的实现
下载
更多推荐



所有评论(0)