当前位置: 移动技术网 > IT编程>开发语言>.net > Abp + MongoDb 改造默认的审计日志存储位置

Abp + MongoDb 改造默认的审计日志存储位置

2019年01月15日  | 移动技术网IT编程  | 我要评论

中国电力安全管理网,ncis第十季,最牛古董商

一、背景

在实际项目的开发当中,使用 abp zero 自带的审计日志功能写入效率比较低。其次审计日志数据量中后期十分庞大,不适合与业务数据存放在一起。所以我们可以重新实现 abp 的 iauditingstore 接口,来让我们的审计日志数据存储在 mongodb 当中。

二、实现

2.0 引入相关包

这里我们需要在模块项目引入 abp 与 mongocsharpdriver 包,引入之后项目如下图。

2.1 实体封装

基于 abp 框架的设计,它许多组件都可以随时被我们所替换。这里我们先定义存储到 mongodb 数据库的实体,取名叫做 mongodbauditentity。下面就是它的基本定义,它是我从 zero 里面单独扒出来的,是基于 abp 的审计信息定义重新进行封装的一个实体。

using system;
using system.linq;
using abp.extensions;
using abp.runtime.validation;
using abp.ui;

namespace abp.auditing.mongodb
{
    /// <summary>
    /// 审计日志记录实体,仅用于 mongodb 存储使用。
    /// </summary>
    public class mongodbauditentity
    {
        /// <summary>
        /// <see cref="servicename"/> 属性的最大长度。
        /// </summary>
        public static int maxservicenamelength = 256;

        /// <summary>
        /// <see cref="methodname"/> 属性的最大长度。
        /// </summary>
        public static int maxmethodnamelength = 256;

        /// <summary>
        /// <see cref="parameters"/> 属性的最大长度。
        /// </summary>
        public static int maxparameterslength = 1024;

        /// <summary>
        /// <see cref="clientipaddress"/> 属性的最大长度。
        /// </summary>
        public static int maxclientipaddresslength = 64;

        /// <summary>
        /// <see cref="clientname"/> 属性的最大长度。
        /// </summary>
        public static int maxclientnamelength = 128;

        /// <summary>
        /// <see cref="browserinfo"/> 属性的最大长度。
        /// </summary>
        public static int maxbrowserinfolength = 512;

        /// <summary>
        /// <see cref="exception"/> 属性的最大长度。
        /// </summary>
        public static int maxexceptionlength = 2000;

        /// <summary>
        /// <see cref="customdata"/> 属性的最大长度。
        /// </summary>
        public static int maxcustomdatalength = 2000;
        
        /// <summary>
        /// 调用接口时用户的编码,如果是匿名访问,则可能为 null。
        /// </summary>
        public string usercode { get; set; }

        /// <summary>
        /// 调用接口时用户的集团 id,如果是匿名访问,则可能为 null。
        /// </summary>
        public int? groupid { get; set; }

        /// <summary>
        /// 调用接口时,请求的应用服务/控制器名称。
        /// </summary>
        public string servicename { get; set; }

        /// <summary>
        /// 调用接口时,请求的的具体方法/接口名称。
        /// </summary>
        public string methodname { get; set; }

        /// <summary>
        /// 调用接口时,传递的具体参数。
        /// </summary>
        public string parameters { get; set; }

        /// <summary>
        /// 调用接口的时间,以服务器的时间进行记录。
        /// </summary>
        public datetime executiontime { get; set; }

        /// <summary>
        /// 调用接口执行方法时所消耗的时间,以毫秒为单位。
        /// </summary>
        public int executionduration { get; set; }

        /// <summary>
        /// 调用接口时客户端的 ip 地址。
        /// </summary>
        public string clientipaddress { get; set; }

        /// <summary>
        /// 调用接口时客户端的名称(通常为计算机名)。
        /// </summary>
        public string clientname { get; set; }
        
        /// <summary>
        /// 调用接口的浏览器信息。
        /// </summary>
        public string browserinfo { get; set; }

        /// <summary>
        /// 调用接口时如果产生了异常,则记录在本字段,如果没有异常则可能 null。
        /// </summary>
        public string exception { get; set; }

        /// <summary>
        /// 自定义数据
        /// </summary>
        public string customdata { get; set; }

        /// <summary>
        /// 从给定的 <see cref="auditinfo"/> 审计信息创建一个新的 mongodb 审计日志实体
        /// (<see cref="mongodbauditentity"/>)。
        /// </summary>
        /// <param name="auditinfo">原始审计日志信息。</param>
        /// <returns>创建完成的 <see cref="mongodbauditentity"/> 实体对象。</returns>
        public static mongodbauditentity createfromauditinfo(auditinfo auditinfo)
        {
            var expmsg = getabpclearexception(auditinfo.exception);
            
            return new mongodbauditentity
            {
                usercode = auditinfo.userid?.tostring(),
                groupid = null,
                servicename = auditinfo.servicename.truncatewithpostfix(maxservicenamelength),
                methodname = auditinfo.methodname.truncatewithpostfix(maxmethodnamelength),
                parameters = auditinfo.parameters.truncatewithpostfix(maxparameterslength),
                executiontime = auditinfo.executiontime,
                executionduration = auditinfo.executionduration,
                clientipaddress = auditinfo.clientipaddress.truncatewithpostfix(maxclientipaddresslength),
                clientname = auditinfo.clientname.truncatewithpostfix(maxclientnamelength),
                browserinfo = auditinfo.browserinfo.truncatewithpostfix(maxbrowserinfolength),
                exception = expmsg.truncatewithpostfix(maxexceptionlength),
                customdata = auditinfo.customdata.truncatewithpostfix(maxcustomdatalength)
            };
        }
        
        public override string tostring()
        {
            return string.format(
                "审计日志: {0}.{1} 由用户 {2} 执行,花费了 {3} 毫秒,请求的源 ip 地址为: {4} 。",
                servicename, methodname, usercode, executionduration, clientipaddress
            );
        }
        
        /// <summary>
        /// 创建更加清楚明确的异常信息。
        /// </summary>
        /// <param name="exception">要处理的异常数据。</param>
        private static string getabpclearexception(exception exception)
        {
            var clearmessage = "";
            switch (exception)
            {
                case null:
                    return null;

                case abpvalidationexception abpvalidationexception:
                    clearmessage = "异常为参数验证错误,一共有 " + abpvalidationexception.validationerrors.count + "个错误:";
                    foreach (var validationresult in abpvalidationexception.validationerrors) 
                    {
                        var membernames = "";
                        if (validationresult.membernames != null && validationresult.membernames.any())
                        {
                            membernames = " (" + string.join(", ", validationresult.membernames) + ")";
                        }

                        clearmessage += "\r\n" + validationresult.errormessage + membernames;
                    }
                    break;

                case userfriendlyexception userfriendlyexception:
                    clearmessage =
                        $"业务相关错误,错误代码: {userfriendlyexception.code} \r\n 异常详细信息: {userfriendlyexception.details}";
                    break;
            }

            return exception + (string.isnullorempty(clearmessage) ? "" : "\r\n\r\n" + clearmessage);
        }
    }
}

2.2 编写 mongodb 配置类

一般来说,我们编写一个 abp 模块肯定是需要构建一个配置类的,以便其他开发人员在使用我们的模块可以进行一些自定义配置。这里我们的 mongodb 审计日志模块无非就是需要配置两个信息,第一个就是 mongodb 数据库的连接字符串,第二个就是要存储的库名称。

/// <summary>
/// 审计日志的 mongodb 存储模块。
/// </summary>
public interface iauditingmongodbconfiguration
{
    /// <summary>
    /// mongodb 连接字符串。
    /// </summary>
    string connectionstring { get; set; }

    /// <summary>
    /// 要连接的 mongodb 数据库名称 
    /// </summary>
    string databasename { get; set; }
}

同理,再编写一个实现。

public class auditingmongodbconfiguration : iauditingmongodbconfiguration
{
    public string connectionstring { get; set; }
    
    public string databasename { get; set; }
}

2.3 编写 imongoclient 的工厂类

其实你直接 new 也可以,这里编写一个工厂类是省去一些构建流程而已,首先为工厂类定义一个接口,该接口只有一个方法,就是创建 imongoclient 的实例对象。

public interface imongoclientfactory
{
    imongoclient create();
}

这个工厂的实现也很简单,只不过我们在工厂当中注入了 iauditingmongodbconfiguration ,方便我们创建实例。

public class mongoclientfactory : imongoclientfactory
{
    private readonly iauditingmongodbconfiguration _mongodbconfiguration;
    
    public mongoclientfactory(iauditingmongodbconfiguration mongodbconfiguration)
    {
        _mongodbconfiguration = mongodbconfiguration;
    }
    
    public imongoclient create()
    {
        return new mongoclient(_mongodbconfiguration.connectionstring);
    }
}

2.4 审计日志的具体存储动作

上面几点都是做一些准备工作,下面我们需要实现 iauditingstore 接口,以便将我们的审计日志存储在 mongodb 数据库当中。iauditingstore 接口只定义了一个方法,就是 saveasync(auditinfo auditinfo) 方法。该方法是在每次接口请求的时候,通过过滤器/拦截器的时候会被调用。当然整个审计日志的构成不是这么简单的,如果大家有兴趣可以查看我的另一篇博客 《[abp 源码分析] 十五、自动审计记录》 ,在这篇博客有详细讲述审计日志的相关知识。

我们接着继续,因为 saveasync(auditinfo auditinfo) 方法传入了一个 auditinfo 对象,我们就可以基于这个对象来构造我们的数据实体。构造完成之后,将其通过 imongoclient 对象存储到 mongodb 数据库当中。

/// <summary>
/// <see cref="iauditingstore"/> 的特殊实现,使用的是 mongodb 作为持久化存储。
/// </summary>
public class mongodbauditingstore : iauditingstore
{
    private readonly imongoclientfactory _clientfactory;
    private readonly iauditingmongodbconfiguration _mongodbconfiguration;
    
    public mongodbauditingstore(imongoclientfactory clientfactory, iauditingmongodbconfiguration mongodbconfiguration)
    {
        _clientfactory = clientfactory;
        _mongodbconfiguration = mongodbconfiguration;
    }

    public async task saveasync(auditinfo auditinfo)
    {
        var entity = mongodbauditentity.createfromauditinfo(auditinfo);
        
        await _clientfactory.create()
            .getdatabase(_mongodbconfiguration.databasename)
            .getcollection<mongodbauditentity>(typeof(mongodbauditentity).name)
            .insertoneasync(entity);
    }
}

可以看到整体代码还是十分简单的,直接通过 auditinfo 对象构造好数据实体之后,插入到 mongodb 数据库当中。

2.5 编写模块类

每一个基于 abp 的第三方模块都会有一个模块类,模块类的主要作用就是针对于第三方模块进行一些基本配置,以及对一些组件的替换动作。

using abp.auditing.mongodb.configuration;
using abp.auditing.mongodb.infrastructure;
using abp.dependency;
using abp.modules;

namespace abp.auditing.mongodb
{
    [dependson(typeof(abpkernelmodule))]
    public class abpauditingmongodbmodule : abpmodule
    {
        public override void preinitialize()
        {
            iocmanager.register<iauditingmongodbconfiguration,auditingmongodbconfiguration>();
            iocmanager.register<imongoclientfactory,mongoclientfactory>();
            
            // 替换自带的审计日志存储实现
            configuration.replaceservice(typeof(iauditingstore),() =>
            {
                iocmanager.register<iauditingstore, mongodbauditingstore>(dependencylifestyle.transient);
            });
        }

        public override void initialize()
        {
            iocmanager.registerassemblybyconvention(typeof(abpauditingmongodbmodule).assembly);
        }
    }
}

2.6 编写集成的扩展方法

abp 模块都会基于 imoduleconfigurations 接口编写一个扩展方法,这样其他基于 abp 框架的项目开发人员就可以很方便地在其启动模块的 preinitialzie() 方法当中通过 configuration.modules 来进行配置。

/// <summary>
/// mongodb 审计日志存储提供器的配置类的扩展方法。
/// </summary>
public static class auditingmongodbconfigurationextensions
{
    /// <summary>
    /// 配置审计日志的 mongodb 实现的相关参数。 
    /// </summary>
    /// <param name="modules">模块配置类</param>
    /// <param name="connectstring">mongodb 连接字符串。</param>
    /// <param name="databasename">要操作的 mongodb 数据库。</param>
    public static void configuremongodbauditingstore(this imoduleconfigurations modules,string connectstring,string databasename)
    {
        var configuration = modules.abpconfiguration.get<iauditingmongodbconfiguration>();
        
        configuration.connectionstring = connectstring;
        configuration.databasename = databasename;
    }
}

三、测试

新建一个项目,并添加对我们库的引用,在其启动模块当中添加对 abpauditingmongodbmodule 模块的依赖,在其 preinitialize() 方法当中加入以下代码,以配置审计日志相关功能。

[dependson(typeof(abpauditingmongodbmodule))]
public class startupmodule : abpmodule
{
    public override void preinitialize()
    {
        // 其他代码...
    
        // 开启审计日志记录
        configuration.auditing.isenabled = true;
        // 允许记录匿名用户请求
        configuration.auditing.isenabledforanonymoususers = true;
        // 配置 monggodb 数据库地址与名称
        configuration.modules.configuremongodbauditingstore("mongodb://username:zpassword@ip:port","testdatabase");
        
        // 其他代码...
    }
}

启动项目之后,我们尝试访问测试方法,之后来到 mongodb 数据库当中,查看具体的审计日志信息。

可以看到,所有对接口的请求都被记录到了 mongodb 当中,这样后续可以基于这些数据进行二次分析。

四、结语

abp.auditing.mongodb 包下载地址

abp.auditing.mongodb 包 github 地址

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网