开发框架Furion之WebApi+SqlSugar (二)

目录

目录

1.JSON序列化之long 长整型数据精度丢失设置

2.API接口请求速率限制配置

3.JWT Token 认证

3.1登录验证码

3.2用户登录实现

3.3JWT身份认证实现

4.日志

4.1错误日志

4.2请求审计日志

 4.3日志订阅

5.用户权限配置

6.附录



关于项目框架的基础搭建参见开发框架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限速之前,您首先要考虑好限速的策略,通常一个好的限速策略有以下这两个特性:

  • 易于理解,易于解释,易于使用
  • 针对特殊情况,对开发者可以不限速

限速策略考虑还需考虑的问题

  1. 颗粒速率限制还是全局速率限制
  2. 针对比较简单的系统,很多开发者采用的是全局速率限制,但是如果某个API消费了大量的资源,你可能需要为每个API单独进行速率限制。所以颗粒的速率限制会保护你的基础架构不被任何耗资巨大的API节点所引起的无理流量尖峰造成严重影响。
  3. 测量每个用户、应用、客户端
  4. 你想要进行速率限制的资源还依赖于您的API的身份认证方式。需要用户身份认证的API通常可以按照用户进行速率限制,而需要应用身份认证的API通常是基于每个应用来进行速率限制。对于未认证的API,我能想到的就是按照IP地址进行速率限制。
  5. 是否支持突发流量
  6. 有些API,尤其是企业内部的系统,需要支持超过速率限制的突发流量。这种情况下,可以采用令牌桶算法(Token Bucket)来实现速率限制。
  7. 是否允许例外
  8. 有时候,对于应用的开发者来说,一个限速策略或一组限速策略可能都不太适用。对于你信任的开发者,如果他们的请求超出了配额,可能需要给他们一些例外的允许,但是在此之前,你要做这些工作:
  9. 保证每个开发者的用例对客户来说都是合理有益的。
  10. 要确认确实没有其它不超出约束的办法来达到相同的目的。
  11. 确保你的基础架构确实可以支撑想要请求的速率。

针对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(App.Configuration.GetSection("IpRateLimiting"));
            //从appsettings.json中加载Ip规则
            services.Configure(App.Configuration.GetSection("IpRateLimitPolicies"));
            //注入计数器和规则存储
            services.AddSingleton();
            services.AddSingleton();
            //配置(解析器、计数器密钥生成器)
            services.AddSingleton();
            services.AddSingleton();
        }
    }
}

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
{
    /// 
    /// 验证码
    /// 
    public class CaptchaHelper
    {
        /// 
        /// 获取验证码
        /// 
        /// 验证码数
        /// 类型 0:数字 1:字符
        /// 
        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();
            }
        }
        /// 
        /// 获取数字验证码
        /// 
        /// 验证码数
        /// 
        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;
        }
        /// 
        /// 获取字符验证码
        /// 
        /// 验证码数
        /// 
        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;
        }
        /// 
        /// 获取运算符验证码
        /// 
        /// 
        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;
        }
    }
    /// 
    /// 验证码信息
    /// 
    public class VerifyCode
    {
        /// 
        /// 验证码
        /// 
        public string Code { get; set; }
        /// 
        /// 验证码数据流
        /// 
        public byte[] Image { get; set; }
        /// 
        /// base64
        /// 
        public string Base64Str { get { return Convert.ToBase64String(Image); } }
    }
    /// 
    /// 验证码类型
    /// 
    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
{
    /// 
    /// Microsoft.Extensions.Caching.redis 缓存
    /// 
    public  class CacheHelper
    {
        private readonly IDistributedCache _cache;
        public CacheHelper(IDistributedCache cache)
        {
            _cache = cache;
        }
        /// 
        /// 设置缓存
        /// 
        /// 
        /// 
        /// 过期时间 单位秒
        public void SetRedisCache(string key,string value,int time)
        {
            _cache.Set(key, Encoding.UTF8.GetBytes(value), new DistributedCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromSeconds(time)));
        }
        /// 
        /// 获取缓存
        /// 
        /// 
        /// 
        public string GetReidsCache(string key)
        {
           byte[]?valueByte=_cache.Get(key);
           return valueByte != null ? Encoding.Default.GetString(valueByte) : null;
        }
        /// 
        /// 删除缓存
        /// 
        /// 
        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("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
{
    /// 
    /// 用户信息
    /// 
    [SugarTable("Sys_User")]
    [Tenant(0)]
    public class UserInfo:BaseEntity
    {
        /// 
        /// 用户姓名
        /// 
        [SugarColumn(IsNullable =true,ColumnDescription = "用户姓名")]
        public string? UserName { get; set; }
        /// 
        /// 用户昵称
        /// 
        [SugarColumn(IsNullable = true, ColumnDescription = "用户昵称")]
        public string? NickName { get; set; }
        /// 
        /// 登录账户
        /// 
        [SugarColumn(IsNullable = false, ColumnDescription = "登录账户")]
        public string? Account { get; set; }
        /// 
        /// 登录密码
        /// 
        [SugarColumn(IsNullable = false, ColumnDescription = "登录密码")]
        public string? LoginPwd { get; set; }
        /// 
        /// 联系电话
        /// 
        [SugarColumn(IsNullable = true, ColumnDescription = "联系电话")]
        public string? Tel { get; set; }
        /// 
        /// 电子邮箱
        /// 
        [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
{
    /// 
    /// 用户信息服务仓储
    /// 
    public class UserRepository:BaseRepository,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
{
    /// 
    /// Login
    /// 
    [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;
        }
        /// 
        /// 获取验证码
        /// 
        /// 
        [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 };
        }
        /// 
        /// 登录
        /// 
        /// 
        /// 
        [AllowAnonymous]
        [HttpPost("login")]
        public async Task 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
            {
                {"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
{
    /// 
    /// JWT身份认证
    /// 
    public class JwtHandler : AppAuthorizeHandler
    {
        /// 
        /// 管道请求
        /// 
        /// 
        /// 
        /// 
        public override Task PipelineAsync(AuthorizationHandlerContext context, DefaultHttpContext httpContext)
        {
            // 这里写您的授权判断逻辑,授权通过返回 true,否则返回 false
            return Task.FromResult(true);
        }
        /// 
        /// 重写 Handler 添加自动刷新
        /// 
        /// 
        /// 
        public override async Task HandleAsync(AuthorizationHandlerContext context)
        {

            // 自动刷新Token            
            if (JWTEncryption.AutoRefreshToken(context, context.GetCurrentHttpContext(),App.GetOptions().ExpiredTime))
            {
                await AuthorizeHandleAsync(context);
            }
            else
            {
                context.Fail(); // 授权失败
                DefaultHttpContext currentHttpContext = context.GetCurrentHttpContext();
                if (currentHttpContext == null)
                {
                    return;
                }
                currentHttpContext.SignoutToSwagger();
            }
        }
    }
}

Startup中的ConfigureServices启用JWT身份认证

services.AddJwt(enableGlobalAuthorize: true);//启用Jwt 身份验证 全局权限

 Configure中启用身份认证

 app.UseAuthentication();
 app.UseAuthorization();

4.日志

关于日志,使用IEventSubscriber时间订阅依赖接口实现,将日志存储到数据库中

4.1错误日志

MyFurion.Model项目,创建ErrorLog类文件,MyFurion.Application项目创建ErrorLog仓储


namespace MyFurion.Model
{
    /// 
    /// 错误日志
    /// 
    [SugarTable("Sys_ErrorLog")]
    [Tenant(0)]
    public class ErrorLog:BaseEntity
    {
        /// 
        /// 类名
        /// 
        [SugarColumn(Length = 100, IsNullable = true, ColumnDescription = "类名")]
        public string ClassName { get; set; }
        /// 
        /// 方法名
        /// 
        [SugarColumn(Length = 100, IsNullable = true, ColumnDescription = "方法名")]
        public string MethodName { get; set; }
        /// 
        /// 异常名称
        /// 
        [SugarColumn(IsNullable = true, ColumnDataType = CommonConst.DB_STRING_MAX, ColumnDescription = "异常名称")]
        public string ExceptionName { get; set; }
        /// 
        /// 异常信息
        /// 
        [SugarColumn(IsNullable = true, ColumnDataType = CommonConst.DB_STRING_MAX, ColumnDescription = "异常信息")]
        public string ExceptionMsg { get; set; }
        /// 
        /// 异常源
        /// 
        [SugarColumn(IsNullable = true, ColumnDataType = CommonConst.DB_STRING_MAX, ColumnDescription = "异常源")]
        public string ExceptionSource { get; set; }
        /// 
        /// 堆栈信息
        /// 
        [SugarColumn(IsNullable = true, ColumnDataType = CommonConst.DB_STRING_MAX, ColumnDescription = "堆栈信息")]
        public string StackTrace { get; set; }
        /// 
        /// 参数对象
        /// 
        [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
{
    /// 
    /// 错误日志服务仓储
    /// 
    public class ErrorLogRepository : BaseRepository, ITransient
    {
        //TODO
    }
}

4.2请求审计日志

MyFurion.Model项目,创建RequestLog类文件,MyFurion.Application项目创建RequestLog仓储


namespace MyFurion.Model
{
    /// 
    /// 请求审计日志
    /// 
    [SugarTable("Sys_RequestLog")]
    [Tenant(0)]
    public class RequestLog:BaseEntity
    {
        /// 
        /// 是否执行成功
        /// 
        [SugarColumn(ColumnDescription = "是否执行成功")]
        public bool Success { get; set; }
        /// 
        /// 具体消息
        /// 
        [SugarColumn(IsNullable = true, ColumnDataType = CommonConst.DB_STRING_MAX, ColumnDescription = "具体消息")]
        public string Message { get; set; }
        /// 
        /// IP
        /// 
        [SugarColumn(Length = 100, IsNullable = true, ColumnDescription = "ip地址")]
        public string Ip { get; set; }
        /// 
        /// 地址
        /// 
        [SugarColumn(Length = 1024, IsNullable = true, ColumnDescription = "地址")]
        public string Location { get; set; }
        /// 
        /// 浏览器
        /// 
        [SugarColumn(Length = 100, IsNullable = true, ColumnDescription = "浏览器")]
        public string Browser { get; set; }
        /// 
        /// 操作系统
        /// 
        [SugarColumn(Length = 100, IsNullable = true, ColumnDescription = "操作系统")]
        public string OsSystem { get; set; }
        /// 
        /// 请求地址
        /// 
        [SugarColumn(Length = 100, IsNullable = true, ColumnDescription = "请求地址")]
        public string Url { get; set; }
        /// 
        /// 类名称
        /// 
        [SugarColumn(Length = 100, IsNullable = true, ColumnDescription = "类名称")]
        public string ClassName { get; set; }
        /// 
        /// 方法名称
        /// 
        [SugarColumn(Length = 100, IsNullable = true, ColumnDescription = "方法名称")]
        public string MethodName { get; set; }
        /// 
        /// 请求方式(GET POST PUT DELETE)
        /// 
        [SugarColumn(Length = 100, IsNullable = true, ColumnDescription = "请求方式")]
        public string ReqMethod { get; set; }
        /// 
        /// 请求参数
        /// 
        [SugarColumn(IsNullable = true, ColumnDataType = CommonConst.DB_STRING_MAX, ColumnDescription = "请求参数")]
        public string Param { get; set; }
        /// 
        /// 返回结果
        /// 
        [SugarColumn(IsNullable = true, ColumnDataType = CommonConst.DB_STRING_MAX, ColumnDescription = "返回结果")]
        public string Result { get; set; }
        /// 
        /// 耗时(毫秒)
        /// 
        [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
{
    /// 
    /// 请求审计日志服务仓储
    /// 
    public class RequestLogRepository : BaseRepository, 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
    {
        /// 
        /// 获取请求的ip4
        /// 
        /// 
        /// 
        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
{
    /// 
    /// 日志事件订阅依赖接口
    /// 
    public class LogEventSubscriber : IEventSubscriber
    {
        /// 
        /// 请求日志
        /// 
        /// 
        /// 
        [EventSubscribe("Add:RequestLog")]
        public async Task AddWorkLog(EventHandlerExecutingContext context)
        {
            var log = (RequestLog)context.Source.Payload;
            await App.GetService().Add(log);
        }
        /// 
        /// 错误日志
        /// 
        /// 
        /// 
        [EventSubscribe("Add:ErrorLog")]
        public async Task AddErrorLog(EventHandlerExecutingContext context)
        {
            var log = (ErrorLog)context.Source.Payload;
            await App.GetService().Add(log);
        }
    }
}

MyFurion.Start项目,创建全局异常处理类ErrorLogFilter

using Furion.DependencyInjection;
using Furion.EventBus;
using Furion.FriendlyException;
using Microsoft.AspNetCore.Mvc.Filters;

namespace MyFurion.Start
{
    /// 
    /// 全局异常处理
    /// 
    public class ErrorLogFilter : IGlobalExceptionHandler, ISingleton
    {
        private readonly IEventPublisher _eventPublisher;
        /// 
        /// 全局异常处理
        /// 
        /// 
        public ErrorLogFilter(IEventPublisher eventPublisher)
        {
            _eventPublisher = eventPublisher;
        }
        /// 
        /// 异常处理
        /// 
        /// 
        /// 
        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
{
    /// 
    /// 请求日志
    /// 
    public class RequestLogFilter : IAsyncActionFilter
    {
        private readonly IEventPublisher _eventPublisher;
        /// 
        /// 请求日志拦截
        /// 
        /// 
        public RequestLogFilter(IEventPublisher eventPublisher)
        {
            _eventPublisher = eventPublisher;
        }
        /// 
        /// 请求处理(执行操作前后)
        /// 
        /// 
        /// 
        /// 
        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();
            });
            //全局注册请求日志
            services.Configure(options =>
            {
                options.Filters.Add();
            });

5.用户权限配置

一般我们用到的权限配置是菜单权限、按钮操作权限、数据域权限,此处不做详细说明了,大家可根据实际情况自行设计开发

6.附录

主要最终版源码的可以至MyFurion: 项目使用Furion开源框架、SqlSugar开源多库架构ORM框架、.Net6,用户登录、角色、权限、部门、菜单、数据字典、日志、多租户等基础功能的实现

下载 

你可能感兴趣的:(.Net,.net,Furion)