手工搭建基於ABP的框架(3) - 登陸,權限控制與日誌

爲了防止不提供原網址的轉載,特在這裏加上原文連接:
http://www.cnblogs.com/skabyy/p/7695258.htmlhtml

本篇將實現登陸、權限控制、日誌配置與審計日誌的功能。首先咱們先實現登陸功能,在登陸的基礎上,經過控權使得只有ID爲1988的用戶才能建立tweet。最後配置Log4Net日誌,並開啓審計日誌,記錄全部Web請求。前端

簡單的界面

爲了測試方便,在實現登陸功能以前,先簡單實現了幾個頁面:git

  1. Tweets列表頁面github

  2. 建立tweet頁面web

  3. 登陸頁面數據庫

頁面代碼沒有什麼特別的,這裏就不贅述了。安全

登陸

咱們不但願全部人都能建立tweet,而是隻有已登陸的用戶才能建立。本小節將實現登陸功能,限制建立tweet頁面只有已登錄用戶才能訪問。cookie

首先在Web.configsystem.web里加上這段配置:app

而後設置首頁和登陸頁面能夠匿名訪問。給Home/IndexAccount/Login這兩個Action加上AllowAnonymous特性。框架

[AllowAnonymous]
public ActionResult Index()

[AllowAnonymous]
public ActionResult Login(string returnUrl)

接下來實現登陸功能。登陸功能的實現有兩步:

  1. 用戶發起登陸請求後,驗證完用戶名密碼,生成cookie,而後把cookie返回給前端。

    [HttpPost]
    [AllowAnonymous]
    public ActionResult LoginAjax(LoginInput input)
    {
        // 這裏應該放驗證用戶名密碼是否正確的代碼。
        // 爲了測試方便,這裏跳過驗證,使用任意用戶名任意密碼都能登陸。
        var username = input.Username;
        var ticket = new FormsAuthenticationTicket(
            1 /* version */,
            username,
            DateTime.Now,
            DateTime.Now.Add(FormsAuthentication.Timeout),
            false /* persistCookie */,
            "" /* userData */);
        var userCookie = new HttpCookie(
            FormsAuthentication.FormsCookieName,
            FormsAuthentication.Encrypt(ticket));
        HttpContext.Response.Cookies.Add(userCookie);
        return Json("OK");
    }

    注意LoginAjax接口要加上AllowAnonymous特性容許匿名訪問。爲了測試方便,這裏沒有對用戶名密碼進行驗證,使用任意用戶名任意密碼都能登陸。

  2. 用戶每次訪問時,根據userId建立claim、identity和principal,並把principal賦值給HttpContext.Current.User。這部分代碼實如今過濾器。新建過濾器類MvcAuthorizeAttribute

    public class MvcAuthorizeAttribute : AuthorizeAttribute
    {
        public override void OnAuthorization(AuthorizationContext filterContext)
        {
            // IIS會從cookie解析出userId並生成一個principal賦值給Thread.CurrentPrincipal
            var userId = Thread.CurrentPrincipal?.Identity?.Name;
    
            if (!string.IsNullOrEmpty(userId))
            {
                // 建立identity
                var identity = new GenericIdentity(userId);
                // 添加Type爲AbpClaimTypes.UserId使userId能注入到AbpSession
                identity.AddClaim(new Claim(AbpClaimTypes.UserId, userId));
                // 建立principal
                var principal = new GenericPrincipal(identity, null);
                // 同步Thread.CurrentPrincipal
                Thread.CurrentPrincipal = principal;
                if (HttpContext.Current != null)
                {
                    // 將principal賦值給HttpContext.Current.User,用戶就登陸進去了。
                    HttpContext.Current.User = principal;
                }
            }
    
            base.OnAuthorization(filterContext);
        }
    }

    關於claim、identity和principal這三個概念的詳細解釋能夠看這位哥們的博客。而後將過濾器MvcAuthorizeAttribute加到全局過濾器配置:

    filters.Add(new MvcAuthorizeAttribute());

    注意到過濾器MvcAuthorizeAttribute繼承了AuthorizeAttribute。於是將這個過濾器加到全局過濾後,除了帶AllowAnonymous特性的Action外,其餘Action被未登陸用戶訪問時就會跳轉到登陸頁面。

    另外爲了讓ABP可以使用登陸用戶信息,要將TypeAbpClaimTypes.UserId,值爲userIdClaim添加到Identity裏,這樣userId會自動注入到AbpSession中。咱們在後續代碼中也能夠經過AbpSession.UserId.HasValue來判斷用戶是否已登錄。

    須要注意的一點是ABP只支持數字類型的userId。因此要確保userId是一個能轉成整數的字符串。若是須要其餘類型的userId(好比字符串類型)則須要AbpSession進行擴展

權限控制

本小節將對建立tweet的權限作進一步的限制,讓只有ID爲1988的用戶才能夠建立tweet。爲了實現權限控制,咱們須要實現三個部分:

  1. 定義權限。

    新建類MyTweetAuthorizationProvider,在SetPermissions方法中定義建立tweet的權限。MyTweetAuthorizationProvider要繼承AuthorizationProvider

    public static class MyTweetPermission
    {
        public const string CreateTweet = "CreateTweet";
    }
    
    public class MyTweetAuthorizationProvider : AuthorizationProvider
    {
        public override void SetPermissions(IPermissionDefinitionContext context)
        {
            context.CreatePermission(MyTweetPermission.CreateTweet);
        }
    }
  2. 權限判斷邏輯。即哪些用戶擁有哪些權限的邏輯。

    經過實現接口IPermissionChecker來實現自定義的權限判斷邏輯。新建類MyTweetPermissionChecker,並將邏輯寫在方法IsGrantedAsync中。咱們只容許ID爲「1988」的用戶建立tweet。

    public class MyTweetPermissionChecker : IPermissionChecker, ITransientDependency
    {
        public IAbpSession AbpSession { get; set; }
    
        public Task<bool> IsGrantedAsync(string permissionName)
        {
            var userId = AbpSession.GetUserId();
            return IsGrantedAsync(new UserIdentifier(null, userId), permissionName);
        }
    
        public Task<bool> IsGrantedAsync(UserIdentifier user, string permissionName)
        {
            var userId = user.UserId;
            var t = new Task<bool>(() =>
            {
                if (permissionName == MyTweetPermission.CreateTweet)
                {
                    return userId == 1988;
                }
                return true;
            });
            t.Start();
            return t;
        }
    }

    這裏有兩個地方要注意。一個是類MyTweetPermissionChecker同時要實現ITransientDependency,才能被自動注入(IPermissionChecker並無繼承ITransientDependency)。另外一個地方是方法IsGrantedAsync是異步方法,要返回Task<bool>類型,而且確保返回的task已經Start了。

  3. 標記哪些方法(通常是Action或AppService的方法)屬於哪些權限。

    使用AbpMvcAuthorize將建立tweet的權限標記在Action /Home/CreateTweet上:

    [AbpMvcAuthorize(MyTweetPermission.CreateTweet)]
    public ActionResult CreateTweet()

    爲了讓AbpMvcAuthorize能生效,咱們還須要讓HomeController繼承AbpController(在實踐中,通常要在AbpController上再封裝一個BaseController)。

    public class HomeController : AbpController

    而且MyTweetWebModule要依賴AbpWebMvcModule

    [DependsOn(
        typeof(AbpWebMvcModule),
        typeof(AbpWebApiModule),
        typeof(MyTweetApplicationModule))]
    public class MyTweetWebModule : AbpModule

    另外,建立tweet的POST接口也要控權。因爲WebAPI是取不到AbpSession的(若是必定要用WebAPI只能用其它方法控權),所以咱們須要另外作一個MVC版本的接口來控權(而後前端也作相應的修改):

    public class TweetController : AbpController
    {
        private IMyTweetAppService _myTweetAppService;
    
        public TweetController(IMyTweetAppService appSvc)
        {
            _myTweetAppService = appSvc;
        }
    
        [HttpPost]
        [AbpMvcAuthorize(MyTweetPermission.CreateTweet)]
        public ActionResult Create(CreateTweetInput input)
        {
            var tweet = _myTweetAppService.CreateTweet(input);
            return Json(tweet);
        }
    }

    另外,你也能夠在應用層上標記權限(前提是你是用MVC接口調用應用層的方法,而非WebAPI)。Controller用AbpMvcAuthorize標記權限,而AppService用AbpAuthorize標記權限。

    [AbpAuthorize(MyTweetPermission.CreateTweet)]
    public object CreateTweet(CreateTweetInput input)

    測試一下,用「1988」登陸能夠正常訪問(正常訪問的不截圖了)。而其餘用戶則提示無訪問權限:

日誌配置與審計日誌

日誌配置

ABP框架使用Log4Net來進行日誌管理,而且在Log4Net基礎上封裝了個Abp.Castle.Log4Net包。首先將NuGet包Abp.Castle.Log4Net安裝到MyTweet.Web項目。

而後在MyTweet.Web根目錄下建立Log4Net的配置文件(你也能夠在其餘你喜歡的位置建立,只要後面代碼裏寫對路徑就行),文件名爲log4net.config。下面是我用的配置文件,基本上用的Log4Net默認的配置內容,只是日誌存放文件修改到了Logs/Logs.txt

<?xml version="1.0" encoding="utf-8" ?>
<log4net>
  <appender name="RollingFileAppender" type="log4net.Appender.RollingFileAppender" >
    <file value="Logs/Logs.txt" />
    <appendToFile value="true" />
    <rollingStyle value="Size" />
    <maxSizeRollBackups value="10" />
    <maximumFileSize value="10000KB" />
    <staticLogFileName value="true" />
    <layout type="log4net.Layout.PatternLayout">
        <conversionPattern value="%-5level %date [%-5.5thread] %-40.40logger - %message%newline" />
    </layout>
  </appender>
  <root>
    <appender-ref ref="RollingFileAppender" />
    <level value="DEBUG" />
  </root>
  <logger name="NHibernate">
    <level value="WARN" />
  </logger>
</log4net>

建立好配置文件後,到MvcApplicationApplication_Start方法里加上下面這行代碼,開啓日誌功能。

IocManager.Instance.IocContainer.AddFacility<LoggingFacility>(
    f => f.UseAbpLog4Net().WithConfig("log4net.config"));

有個要注意的地方是須要在文件開頭加上下面這行using語句,否則f.UseAbpLog4Net會報錯。

using Abp.Castle.Logging.Log4Net;

配置好後,咱們可使用依賴注入注入到ILogger接口的Logger對象來寫日誌。在首頁的Action方法中加上幾行寫日誌的代碼:

[AllowAnonymous]
public ActionResult Index()
{
    if (AbpSession.UserId.HasValue)
    {
        Logger.Info(string.Format("用戶{0}訪問了首頁!", AbpSession.UserId));
    }
    else
    {
        Logger.Info("匿名用戶訪問了首頁!");
    }
    return View();
}

分別用匿名身份和登陸用戶身份訪問一下首頁,而後到MyTweet.Web的根目錄下查看Logs/Logs.txt文件:

審計日誌

維基百科說: 「審計跟蹤(也叫審計日誌)是與安全相關的按照時間順序的記錄,記錄集或者記錄源,它們提供了活動序列的文檔證據,這些活動序列能夠在任什麼時候間影響一個特定的操做,步驟或其餘」。

對於咱們Web應用來講,審計日誌負責記錄全部用戶的請求。若是是硬編碼實現的話,咱們須要在全部的Action方法里加上記錄日誌的代碼。這顯然是既耗時又不科學的。幸運的是咱們不須要這麼作,ABP框架自帶審計日誌的功能。只要咱們配置好日誌功能(前面已經作了),ABP會默認記錄全部已登錄用戶(經過AbpSession.UserId.HasValue判斷是否已登錄)的訪問。查看Logs/Logs.txt文件會發現剛纔咱們訪問首頁的行爲已經被記錄到日誌裏了。

INFO  2017-11-02 15:39:23,055 [29   ] Abp.Auditing.SimpleLogAuditingStore      - AUDIT LOG: MyTweet.Web.Controllers.HomeController.Index is executed by user 1988 in 1 ms from 10.211.55.3 IP address with succeed.

這行日誌記錄了這些信息:用戶名、用戶IP地址、訪問的方法、響應耗時以及訪問結果。另外,在這行日誌的開頭有這個字段:Abp.Auditing.SimpleLogAuditingStore。這個表示該日誌內容是由類Abp.Auditing.SimpleLogAuditingStore處理記錄的。該類實現了IAuditingStore接口。若是咱們要自定義審計日誌的內容,咱們須要本身實現這個接口。下面咱們實現一個輸出中文的審計日誌。在MyTweet.Web項目下新建類MyTweetLogAuditingStore

public class MyTweetLogAuditingStore : IAuditingStore, ITransientDependency
{
    public ILogger Logger { get; set; }

    public MyTweetLogAuditingStore()
    {
        Logger = NullLogger.Instance;
    }

    public Task SaveAsync(AuditInfo auditInfo)
    {
        var userId = auditInfo.UserId;
        var userIp = auditInfo.ClientIpAddress;
        var browserInfo = auditInfo.BrowserInfo;
        var action = $"{auditInfo.ServiceName}.{auditInfo.MethodName}";
        var ms = auditInfo.ExecutionDuration;
        var msg = $"用戶{userId}(座標{userIp})使用{browserInfo}訪問了方法{action},該方法在{ms}毫秒內進行了回擊,回擊結果:";
        if (auditInfo.Exception == null)
        {
            Logger.Info(msg + "成功!");
        }
        else
        {
            Logger.Warn(msg + "出錯了:" + auditInfo.Exception.Message);
        }
        return Task.FromResult(0);
    }
}

再訪問首頁,而後看看日誌記了啥:

INFO  2017-11-02 16:45:53,374 [35   ] et.Web.App_Start.MyTweetLogAuditingStore - 用戶1988(座標10.211.55.3)使用Chrome / 61.0 / WinNT訪問了方法MyTweet.Web.Controllers.HomeController.Index,該方法在77毫秒內進行了回擊,回擊結果:成功!

關於審計日誌的其餘配置這裏再也不多說,有須要的同窗能夠看這篇博客

總結

咱們已經使用ABP搭建了一個相對完整的tweet應用。它雖然十分簡陋,但也是五臟俱全。它可以進行數據庫訪問,擁有登陸、控權、日誌等功能。後面會再添加UoW、單元測試等內容。

關於ABP後續的學習和使用,除了查看官方文檔外,強烈建議直接閱讀ABP的源碼。爲了弄清楚一些犄角旮旯的細節,在文檔裏翻找半天每每不如直接查閱代碼來得效率高。

相關文章
相關標籤/搜索