主頁 > .NET開發 > [Abp vNext 原始碼分析] - 8. 審計日志

[Abp vNext 原始碼分析] - 8. 審計日志

2020-09-23 03:13:27 .NET開發

一、簡要說明

ABP vNext 當中的審計模塊早在 依賴注入與攔截器一文中有所提及,但沒有詳細的對其進行分析,

審計模塊是 ABP vNext 框架的一個基本組件,它能夠提供一些實用日志記錄,不過這里的日志不是說系統日志,而是說介面每次呼叫之后的執行情況(執行時間、傳入引數、例外資訊、請求 IP),

除了常規的日志功能以外,關于 物體聚合 的審計欄位介面也是存放在審計模塊當中的,(創建人創建時間修改人修改時間洗掉人洗掉時間

二、原始碼分析

2.1. 審計日志攔截器

2.1.1 審計日志攔截器的注冊

Volo.Abp.Auditing 的模塊定義十分簡單,主要是提供了 審計日志攔截器 的注冊功能,下面代碼即在組件注冊的時候,會呼叫 AuditingInterceptorRegistrar.RegisterIfNeeded 方法來判定是否為實作型別(ImplementationType) 注入審計日志攔截器,

public class AbpAuditingModule : AbpModule
{
    public override void PreConfigureServices(ServiceConfigurationContext context)
    {
        context.Services.OnRegistred(AuditingInterceptorRegistrar.RegisterIfNeeded);
    }
}

跳轉到具體的實作,可以看到內部會結合三種型別進行判斷,分別是 AuditedAttributeIAuditingEnabledDisableAuditingAttribute

前兩個作用是,只要型別標注了 AuditedAttribute 特性,或者是實作了 IAuditingEnable 介面,都會為該型別注入審計日志攔截器,

DisableAuditingAttribute 型別則相反,只要型別上標注了該特性,就不會啟用審計日志攔截器,某些介面需要 提升性能 的話,可以嘗試使用該特性禁用掉審計日志功能,

public static class AuditingInterceptorRegistrar
{
    public static void RegisterIfNeeded(IOnServiceRegistredContext context)
    {
        // 滿足條件時,將會為該型別注入審計日志攔截器,
        if (ShouldIntercept(context.ImplementationType))
        {
            context.Interceptors.TryAdd<AuditingInterceptor>();
        }
    }

    private static bool ShouldIntercept(Type type)
    {
        // 首先判斷型別上面是否使用了輔助型別,
        if (ShouldAuditTypeByDefault(type))
        {
            return true;
        }

        // 如果任意方法上面標注了 AuditedAttribute 特性,則仍然為該型別注入攔截器,
        if (type.GetMethods().Any(m => m.IsDefined(typeof(AuditedAttribute), true)))
        {
            return true;
        }

        return false;
    }

    //TODO: Move to a better place
    public static bool ShouldAuditTypeByDefault(Type type)
    {
        // 下面就是根據三種輔助型別進行判斷,是否為當前 type 注入審計日志攔截器,
        if (type.IsDefined(typeof(AuditedAttribute), true))
        {
            return true;
        }

        if (type.IsDefined(typeof(DisableAuditingAttribute), true))
        {
            return false;
        }

        if (typeof(IAuditingEnabled).IsAssignableFrom(type))
        {
            return true;
        }

        return false;
    }
}

2.1.2 審計日志攔截器的實作

審計日志攔截器的內部實作,主要使用了三個型別進行協同作業,它們分別是負責管理審計日志資訊的 IAuditingManager,負責創建審計日志資訊的 IAuditingHelper,還有統計介面執行時常的 Stopwatch

整個審計日志攔截器的大體流程如下:

  1. 首先是判定 MVC 審計日志過濾器是否進行處理,
  2. 再次根據特性,和型別進行二次驗證是否應該創建審計日志資訊,
  3. 根據呼叫資訊,創建 AuditLogInfoAuditLogActionInfo 審計日志資訊,
  4. 呼叫 StopWatch 的計時方法,如果出現了例外則將例外資訊添加到剛才構建的 AuditLogInfo 物件中,
  5. 無論是否出現例外,都會進入 finally 陳述句塊,這個時候會呼叫 StopWatch 實體的停止方法,并統計完成執行時間,
public override async Task InterceptAsync(IAbpMethodInvocation invocation)
{
    if (!ShouldIntercept(invocation, out var auditLog, out var auditLogAction))
    {
        await invocation.ProceedAsync();
        return;
    }

    // 開始進行計時操作,
    var stopwatch = Stopwatch.StartNew();

    try
    {
        await invocation.ProceedAsync();
    }
    catch (Exception ex)
    {
        // 如果出現了例外,一樣的將例外資訊添加到審計日志結果中,
        auditLog.Exceptions.Add(ex);
        throw;
    }
    finally
    {
        // 統計完成,并將資訊加入到審計日志結果中,
        stopwatch.Stop();
        auditLogAction.ExecutionDuration = Convert.ToInt32(stopwatch.Elapsed.TotalMilliseconds);
        auditLog.Actions.Add(auditLogAction);
    }
}

可以看到,只有當 ShouldIntercept() 方法回傳 true 的時候,下面的統計等操作才會被執行,

protected virtual bool ShouldIntercept(
    IAbpMethodInvocation invocation, 
    out AuditLogInfo auditLog, 
    out AuditLogActionInfo auditLogAction)
{
    auditLog = null;
    auditLogAction = null;

    if (AbpCrossCuttingConcerns.IsApplied(invocation.TargetObject, AbpCrossCuttingConcerns.Auditing))
    {
        return false;
    }

    // 如果沒有獲取到 Scop,則回傳 false,
    var auditLogScope = _auditingManager.Current;
    if (auditLogScope == null)
    {
        return false;
    }

    // 進行二次判斷是否需要存盤審計日志,
    if (!_auditingHelper.ShouldSaveAudit(invocation.Method))
    {
        return false;
    }

    // 構建審計日志資訊,
    auditLog = auditLogScope.Log;
    auditLogAction = _auditingHelper.CreateAuditLogAction(
        auditLog,
        invocation.TargetObject.GetType(),
        invocation.Method, 
        invocation.Arguments
    );

    return true;
}

2.2 審計日志的持久化

大體流程和我們上面說的一樣,不過好像缺少了重要的一步,那就是 持久化操作,你可以在 Volo.Abp.Auditing 模塊發現有 IAuditingStore 介面的定義,但是它的 SaveAsync() 方法卻沒有在攔截器內部被呼叫,同樣在 MVC 的審計日志過濾器實作,你也會發現沒有呼叫持久化方法,

那么我們的審計日志是在什么時候被持久化的呢?找到 SaveAsync() 被呼叫的地方,發現 ABP vNext 實作了一個審計日志的 ASP.NET Core 中間件,

在這個中間件內部的實作比較簡單,首先通過一個判定方法,決定是否為本次請求執行 IAuditingManager.BeginScope() 方法,如果判定通過,則執行,否則不僅行任何操作,

public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
    if (!ShouldWriteAuditLog(context))
    {
        await next(context);
        return;
    }

    using (var scope = _auditingManager.BeginScope())
    {
        try
        {
            await next(context);
        }
        finally
        {
            await scope.SaveAsync();
        }
    }
}

可以看到,在這里 ABP vNext 使用 IAuditingManager 構建,呼叫其 BeginScope() 構建了一個 IAuditLogSaveHandle 物件,并使用其提供的 SaveAsync() 方法進行持久化操作,

2.2.1 嵌套的持久化操作

在構造出來的 IAuditLogSaveHandle 物件里面,還是使用的 IAuditingManager 的默認實作 AuditingManager 所提供的 SaveAsync() 方法進行持久化

閱讀原始碼之后,發現了下面兩個問題:

  1. IAuditingManager 沒有將持久化方法公開 出來,而是作為一個 protected 級別的方法,
  2. 為什么還要借助 IAuditLogSaveHandle 間接地呼叫 管理器的持久化方法,

這就要從中間件的代碼說起了,可以看到它是構造出了一個可以被釋放的 IAuditLogSaveHandle 物件,ABP vNext 這樣做的目的,就是可以嵌套多個 Scope,即 只在某個范圍內 才將審計日志記錄下來,這種特性類似于 作業單元 的用法,其底層實作是 之前文章 講過的 IAmbientScopeProvider 物件,

例如在某個應用服務內部,我可以這樣寫代碼:

using (var scope = _auditingManager.BeginScope())
{
    await myAuditedObject1.DoItAsync(new InputObject { Value1 = "我是內部嵌套測驗方法1,", Value2 = 5000 });
    using (var scope2 = _auditingManager.BeginScope())
    {
        await myAuditedObject1.DoItAsync(new InputObject {Value1 = "我是內部嵌套測驗方法2,", Value2 = 10000});
        await scope2.SaveAsync();
    }
    await scope.SaveAsync();
}

想一下之前的代碼,在攔截器內部,我們是通過 IAuditingManager.Current 拿到當前可用的 IAuditLogScope ,而這個 Scope 就是在呼叫 IAuditingManager.BeginScope() 之后生成的

2.2.3 最終的持久化代碼

通過上述的流程,我們得知最后的審計日志資訊會通過 IAuditingStore 進行持久化,ABP vNext 為我們提供了一個默認的 SimpleLogAuditingStore 實作,其內部就是呼叫 ILogger 將資訊輸出,如果需要將審計日志持久化到資料庫,你可以實作 IAUditingStore 介面,覆寫原有實作 ,或者使用 ABP vNext 提供的 Volo.Abp.AuditLogging 模塊,

2.3 審計日志的序列化

審計日志的序列化處理是在 IAuditingHelper 的默認實作內部被使用,可以看到構建審計日志的方法內部,通過自定義的序列化器來將 Action 的引數進行序列化處理,方便存盤,

public virtual AuditLogActionInfo CreateAuditLogAction(
    AuditLogInfo auditLog,
    Type type, 
    MethodInfo method, 
    IDictionary<string, object> arguments)
{
    var actionInfo = new AuditLogActionInfo
    {
        ServiceName = type != null
            ? type.FullName
            : "",
        MethodName = method.Name,
        // 序列化引數資訊,
        Parameters = SerializeConvertArguments(arguments),
        ExecutionTime = Clock.Now
    };

    //TODO Execute contributors

    return actionInfo;
}

protected virtual string SerializeConvertArguments(IDictionary<string, object> arguments)
{
    try
    {
        if (arguments.IsNullOrEmpty())
        {
            return "{}";
        }

        var dictionary = new Dictionary<string, object>();

        foreach (var argument in arguments)
        {
            // 忽略的代碼,主要作用是構建引數字典,
        }

        // 呼叫序列化器,序列化 Action 的呼叫引數,
        return AuditSerializer.Serialize(dictionary);
    }
    catch (Exception ex)
    {
        Logger.LogException(ex, LogLevel.Warning);
        return "{}";
    }
}

下面就是具體序列化器的代碼:

public class JsonNetAuditSerializer : IAuditSerializer, ITransientDependency
{
    protected AbpAuditingOptions Options;

    public JsonNetAuditSerializer(IOptions<AbpAuditingOptions> options)
    {
        Options = options.Value;
    }

    public string Serialize(object obj)
    {
        // 使用 JSON.NET 進行序列化操作,
        return JsonConvert.SerializeObject(obj, GetSharedJsonSerializerSettings());
    }

    // ... 省略的代碼,
}

2.4 審計日志的配置引數

針對審計日志相關的配置引數的定義,都存放在 AbpAuditingOptions 當中,下面我會針對各個引數的用途,對其進行詳細的說明,

public class AbpAuditingOptions
{
    //TODO: Consider to add an option to disable auditing for application service methods?

    // 該引數目前版本暫未使用,為保留引數,
    public bool HideErrors { get; set; }

    // 是否啟用審計日志功能,默認值為 true,
    public bool IsEnabled { get; set; }

    // 審計日志的應用程式名稱,默認值為 null,主要在構建 AuditingInfo 被使用,
    public string ApplicationName { get; set; }

    // 是否為匿名請求記錄審計日志默認值 true,
    public bool IsEnabledForAnonymousUsers { get; set; }

    // 審計日志功能的協作者集合,默認添加了 AspNetCoreAuditLogContributor 實作,
    public List<AuditLogContributor> Contributors { get; }

    // 默認的忽略型別,主要在序列化時使用,
    public List<Type> IgnoredTypes { get; }

    // 物體型別選擇器,
    public IEntityHistorySelectorList EntityHistorySelectors { get; }

    //TODO: Move this to asp.net core layer or convert it to a more dynamic strategy?
    // 是否為 Get 請求記錄審計日志,默認值 false,
    public bool IsEnabledForGetRequests { get; set; }

    public AbpAuditingOptions()
    {
        IsEnabled = true;
        IsEnabledForAnonymousUsers = true;
        HideErrors = true;

        Contributors = new List<AuditLogContributor>();

        IgnoredTypes = new List<Type>
        {
            typeof(Stream),
            typeof(Expression)
        };

        EntityHistorySelectors = new EntityHistorySelectorList();
    }
}

2.4 物體相關的審計資訊

在文章開始就談到,除了對 HTTP 請求有審計日志記錄以外,ABP vNext 還提供了物體審計資訊的記錄功能,所謂的物體的審計資訊,指的就是物體繼承了 ABP vNext 提供的介面之后,ABP vNext 會自動維護實作的介面欄位,不需要開發人員自己再進行處理,

這些介面包括創建物體操作的相關資訊 IHasCreationTimeIMayHaveCreatorICreationAuditedObject 以及洗掉物體時,需要記錄的相關資訊介面 IHasDeletionTimeIDeletionAuditedObject 等,除了審計日志模塊定義的型別以外,在 Volo.Abp.Ddd.Domain 模塊的 Auditing 里面也有很多審計物體的默認實作,

我在這里就不再一一列舉,下面僅快速講解一下 ABP vNext 是如何通過這些介面,實作對審計欄位的自動維護的,

在審計日志模塊的內部,我們看到一個介面名字叫做 IAuditPropertySetter,它提供了三個方法,分別是:

public interface IAuditPropertySetter
{
    void SetCreationProperties(object targetObject);

    void SetModificationProperties(object targetObject);

    void SetDeletionProperties(object targetObject);
}

所以,這幾個方法就是用于設定創建資訊、修改資訊、洗掉資訊的,現在跳轉到默認實作 AuditPropertySetter,隨便找一個 SetCreationTime() 方法,該方法內部首先是判斷傳入的 object 是否實作了 IHasCreationTime 介面,如果實作了對其進行強制型別轉換,然后賦值即可,

private void SetCreationTime(object targetObject)
{
    if (!(targetObject is IHasCreationTime objectWithCreationTime))
    {
        return;
    }

    if (objectWithCreationTime.CreationTime == default)
    {
        objectWithCreationTime.CreationTime = Clock.Now;
    }
}

其他幾個 Set 方法大同小異,那我們看一下有哪些地方使用到了上述三個方法,

可以看到使用者就包含有 EF Core 模塊和 MongoDB 模塊,這里我以 EF Core 模塊為例,猜測應該是傳入了物體物件過來,

果不其然...查看這個方法的呼叫鏈,發現是 DbContext 每次進行 SaveChanges/SaveChangesAsync 的時候,就會對物體進行審計欄位自動賦值操作,

三、總結

審計日志是 ABP vNext 為我們提供的一個可選組件,當開啟審計日志功能后,我們可以根據審計日志資訊快速定位問題,但審計日志的開啟,也會較大的影響性能,因為每次請求都會創建審計日志資訊,之后再進行持久化,因此在使用審計日志功能時,可以結合 DisableAuditingAttribute 特性和 IAuditingManager.BeginScope(),按需開啟審計日志功能,

四、點擊我跳轉到文章目錄

轉載請註明出處,本文鏈接:https://www.uj5u.com/net/108784.html

標籤:.NET Core

上一篇:dotnetcore+vue+elementUI 前后端分離---支持前端、后臺業務代碼擴展的快速開發框架

下一篇:C# 資料型別

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • WebAPI簡介

    Web體系結構: 有三個核心:資源(resource),URL(統一資源識別符號)和表示 他們的關系是這樣的:一個資源由一個URL進行標識,HTTP客戶端使用URL定位資源,表示是從資源回傳資料,媒體型別是資源回傳的資料格式。 接下來我們說下HTTP. HTTP協議的系統是一種無狀態的方式,使用請求/ ......

    uj5u.com 2020-09-09 22:07:47 more
  • asp.net core 3.1 入口:Program.cs中的Main函式

    本文分析Program.cs 中Main()函式中代碼的運行順序分析asp.net core程式的啟動,重點不是剖析原始碼,而是理清程式開始時執行的順序。到呼叫了哪些實體,哪些法方。asp.net core 3.1 的程式入口在專案Program.cs檔案里,如下。ususing System; us ......

    uj5u.com 2020-09-09 22:07:49 more
  • asp.net網站作為websocket服務端的應用該如何寫

    最近被websocket的一個問題困擾了很久,有一個需求是在web網站中搭建websocket服務。客戶端通過網頁與服務器建立連接,然后服務器根據ip給客戶端網頁發送資訊。 其實,這個需求并不難,只是剛開始對websocket的內容不太了解。上網搜索了一下,有通過asp.net core 實作的、有 ......

    uj5u.com 2020-09-09 22:08:02 more
  • ASP.NET 開源匯入匯出庫Magicodes.IE Docker中使用

    Magicodes.IE在Docker中使用 更新歷史 2019.02.13 【Nuget】版本更新到2.0.2 【匯入】修復單列匯入的Bug,單元測驗“OneColumnImporter_Test”。問題見(https://github.com/dotnetcore/Magicodes.IE/is ......

    uj5u.com 2020-09-09 22:08:05 more
  • 在webform中使用ajax

    如果你用過Asp.net webform, 說明你也算是.NET 開發的老兵了。WEBform應該是2011 2013左右,當時還用visual studio 2005、 visual studio 2008。后來基本都用的是MVC。 如果是新開發的專案,估計沒人會用webform技術。但是有些舊版 ......

    uj5u.com 2020-09-09 22:08:50 more
  • iis添加asp.net網站,訪問提示:由于擴展配置問題而無法提供您請求的

    今天在iis服務器配置asp.net網站,遇到一個問題,記錄一下: 問題:由于擴展配置問題而無法提供您請求的頁面。如果該頁面是腳本,請添加處理程式。如果應下載檔案,請添加 MIME 映射。 WindowServer2012服務器,添加角色安裝完.netframework和iis之后,運行aspx頁面 ......

    uj5u.com 2020-09-09 22:10:00 more
  • WebAPI-處理架構

    帶著問題去思考,大家好! 問題1:HTTP請求和回傳相應的HTTP回應資訊之間發生了什么? 1:首先是最底層,托管層,位于WebAPI和底層HTTP堆疊之間 2:其次是 訊息處理程式管道層,這里比如日志和快取。OWIN的參考是將訊息處理程式管道的一些功能下移到堆疊下端的OWIN中間件了。 3:控制器處理 ......

    uj5u.com 2020-09-09 22:11:13 more
  • 微信門戶開發框架-使用指導說明書

    微信門戶應用管理系統,采用基于 MVC + Bootstrap + Ajax + Enterprise Library的技術路線,界面層采用Boostrap + Metronic組合的前端框架,資料訪問層支持Oracle、SQLServer、MySQL、PostgreSQL等資料庫。框架以MVC5,... ......

    uj5u.com 2020-09-09 22:15:18 more
  • WebAPI-HTTP編程模型

    帶著問題去思考,大家好!它是什么?它包含什么?它能干什么? 訊息 HTTP編程模型的核心就是訊息抽象,表示為:HttPRequestMessage,HttpResponseMessage.用于客戶端和服務端之間交換請求和回應訊息。 HttpMethod類包含了一組靜態屬性: private stat ......

    uj5u.com 2020-09-09 22:15:23 more
  • 部署WebApi隨筆

    一、跨域 NuGet參考Microsoft.AspNet.WebApi.Cors WebApiConfig.cs中配置: // Web API 配置和服務 config.EnableCors(new EnableCorsAttribute("*", "*", "*")); 二、清除默認回傳XML格式 ......

    uj5u.com 2020-09-09 22:15:48 more
最新发布
  • C#多執行緒學習(二) 如何操縱一個執行緒

    <a href="https://www.cnblogs.com/x-zhi/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2943582/20220801082530.png" alt="" /></...

    uj5u.com 2023-04-19 09:17:20 more
  • C#多執行緒學習(二) 如何操縱一個執行緒

    C#多執行緒學習(二) 如何操縱一個執行緒 執行緒學習第一篇:C#多執行緒學習(一) 多執行緒的相關概念 下面我們就動手來創建一個執行緒,使用Thread類創建執行緒時,只需提供執行緒入口即可。(執行緒入口使程式知道該讓這個執行緒干什么事) 在C#中,執行緒入口是通過ThreadStart代理(delegate)來提供的 ......

    uj5u.com 2023-04-19 09:16:49 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    <a href="https://www.cnblogs.com/huangxincheng/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/214741/20200614104537.png" alt="" /&g...

    uj5u.com 2023-04-18 08:39:04 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    一:背景 1. 講故事 前段時間協助訓練營里的一位朋友分析了一個程式卡死的問題,回過頭來看這個案例比較經典,這篇稍微整理一下供后來者少踩坑吧。 二:WinDbg 分析 1. 為什么會卡死 因為是表單程式,理所當然就是看主執行緒此時正在做什么? 可以用 ~0s ; k 看一下便知。 0:000> k # ......

    uj5u.com 2023-04-18 08:33:10 more
  • SignalR, No Connection with that ID,IIS

    <a href="https://www.cnblogs.com/smartstar/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/u36196.jpg" alt="" /></a>...

    uj5u.com 2023-03-30 17:21:52 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:15:33 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:13:31 more
  • C#遍歷指定檔案夾中所有檔案的3種方法

    <a href="https://www.cnblogs.com/xbhp/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/957602/20230310105611.png" alt="" /></a&...

    uj5u.com 2023-03-27 14:46:55 more
  • C#/VB.NET:如何將PDF轉為PDF/A

    <a href="https://www.cnblogs.com/Carina-baby/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2859233/20220427162558.png" alt="" />...

    uj5u.com 2023-03-27 14:46:35 more
  • 武裝你的WEBAPI-OData聚合查詢

    <a href="https://www.cnblogs.com/podolski/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/616093/20140323000327.png" alt="" /><...

    uj5u.com 2023-03-27 14:46:16 more