手工搭建基于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的源码。为了弄清楚一些犄角旮旯的细节,在文档里翻找半天每每不如直接查阅代码来得效率高。

相关文章
相关标签/搜索