Restful WebApi项目开发实践

前言

踩过了一段时间的坑,现总结一下,与你们分享,愿与你们一块儿讨论。html

Restful WebApi特色

WebApi相较于Asp.Net MVC/WebForm开发的特色就是先后端彻底分离,后端使用WebApi直接针对资源进行暴露,大部分的业务转移到前端进行。前端能够采用Html页面或各平台的原生程序开发,很是灵活。前端

咱们采用的是WebApi+angularjs/WPF的方式开发。angularjs

设计思想

目前就算使用Asp.net MVC开发,为了用户体验也须要使用Ajax来异步加载数据,而Html5的单页App也愈来愈流行,因此干脆让后端只提供数据的存储,Api除极个别状况只针对实体提供实体的增删改查功能,后台尽可能摘除业务逻辑,把业务逻辑移到前端实现。使后端专一于数据仓储和数据查询的性能优化,而前端更专一于业务逻辑、UI等方面的优化。web

服务端

根据数据模型建立ApiController直接暴露实体,处理增删改查,配合Odata扩展使用很是方便。
这块看上去简单,实际上是很重要的一个地方。因为直接对资源/实体进行暴露,通信采用的又是HTTP协议,前端是没法保证Api访问安全的,并且业务逻辑也移到了前端,因此后端Api的安全性、权限拦截的粒度和灵活性尤其重要。通常进行权限拦截都会针对功能特性进行判断,好比:XX用户可否使用A功能,可是Restful WebApi提供的Api是直接针对资源/实体的,业务逻辑又移到了客户端去实现,后端在业务上功能性的描述弱化了,变成了:可否增/删/改/查A资源,而这种转变就要求权限须要拦截到数据行级别。数据库

服务端插件系统

服务端插件系统,Host到Asp.net MVC WebApi项目上使用

服务端我是在HappyFramework.OSGi基础上进行的改造:
(注:插件系统没有完整的重构过,因此有部分设计会有些不合理)后端

  • 精简掉主体中不用的的部分,好比ioc、企业库。
  • 把主体改形成实现+契约两个类库。
  • 添加自写的权限模块、自写服务定位器实现的服务总线、观察者模式的事件总线,所有使用反射进行查找组装。
  • 根据服务总线的须要添加预启动插件状态。
  • 添加WebApi集成,实现CORS,替换系统WebApi的服务:IAssembliesResolver、IHttpControllerTypeResolver、IHttpControllerSelector实现插件的控制器加载、命名空间隔离。

服务端的主要任务就是开放资源访问和开放一些必需要后端来实现的功能性Api。api

模型设计

既然把大部分业务逻辑都移到了前端,那么后端模型设计上就不用设计的太过详细,除了必须的一些字段,好比Id,Time这种会涉及到查询搜索、抢占更新(文章访问量)之类的,我设计了ExtType和ExtData两个String型字段,前端能够自定义数据模型(ExtType),而后把对应模型数据放到ExtData字段中,尽量提升前端的灵活性和后端数据模型稳定性。安全

权限设计

权限设计-后端:

先来看一个例子,这个例子对应的Url为:
GET api/XXX/WebApiExt/ACL/Activity/{TenantId}/{AggregationId}/{SiteId}
GET api/XXX/WebApiExt/ACL/Activity/{TenantId}/{AggregationId}/{SiteId}/{id}
POST api/XXX/WebApiExt/ACL/Activity/{TenantId}/{AggregationId}/{SiteId}
PUT api/XXX/WebApiExt/ACL/Activity/{TenantId}/{AggregationId}/{SiteId}/{id}
DELETE api/XXX/WebApiExt/ACL/Activity/{TenantId}/{AggregationId}/{SiteId}/{id}性能优化

public class ActivityController : BaseController<Domain.Activity, ActivityModel, Guid>
{
    protected override IEnumerable<Domain.Activity> GetAvailableData(Guid TenantId, Guid AggregationId, Guid SiteId)
    {
        InitVisibleSiteIds(TenantId, AggregationId, SiteId);
        return db.AsNoTracking().Where(s => VisibleSiteIds.Contains(s.SiteId));
    }

    [UppBundleEngine.Web.Permission.WebApi.PermissionAuthorize(
        DisplayName = "获取活动",
        AuthType = AuthorizeType.ACL | AuthorizeType.PermissionCode,
        AuthorizeInfomation = "GetActivities",
        Description = ""
        )]
    [Queryable(AllowedQueryOptions = AllowedQueryOptions.OrderBy | AllowedQueryOptions.Skip | AllowedQueryOptions.Top, MaxTop = 50)]
    public override IQueryable GetAll(Guid TenantId, Guid AggregationId, Guid SiteId)
    {
        var data = GetAvailableData(TenantId, AggregationId, SiteId);
        return data.AsEnumerable().Select(model => AutoMapToModel(model, new[]
    {
        "ExtType", 
        "ExtData", 
    })).AsQueryable();
    }

    [UppBundleEngine.Web.Permission.WebApi.PermissionAuthorize(
        DisplayName = "获取活动",
        AuthType = AuthorizeType.ACL | AuthorizeType.PermissionCode,
        AuthorizeInfomation = "GetActivity"
        )]
    public override IHttpActionResult GetOne(Guid TenantId, Guid AggregationId, Guid SiteId, Guid id)
    {
        var model = GetAvailableData(TenantId, AggregationId, SiteId).SingleOrDefault(s => s.Id == id);
        if (model == null) return NotFound();
        return Ok(AutoMapToModel(model));
    }

    [UppBundleEngine.Web.Permission.WebApi.PermissionAuthorize(
        DisplayName = "添加活动",
        AuthType = AuthorizeType.ACL | AuthorizeType.PermissionCode,
        AuthorizeInfomation = "PostActivity"
        )]
    public override IHttpActionResult Post(Guid TenantId, Guid AggregationId, Guid SiteId, ActivityModel model)
    {
        if (!ModelState.IsValid) return BadRequest(ModelState);
        if (model.SiteId != SiteId) return BadRequest();

        model.Id = Guid.NewGuid();
        db.Add(AutoMapToEntity(model));
        dbContext.SaveChanges();
        return Ok(model);
    }

    [UppBundleEngine.Web.Permission.WebApi.PermissionAuthorize(
        DisplayName = "修改活动",
        AuthType = AuthorizeType.ACL | AuthorizeType.PermissionCode,
        AuthorizeInfomation = "PutActivity"
        )]
    public override IHttpActionResult Put(Guid TenantId, Guid AggregationId, Guid SiteId, Guid id, ActivityModel model)
    {
        if (!ModelState.IsValid) return BadRequest(ModelState);
        if (id != model.Id || SiteId != model.SiteId) return BadRequest();
        var oldmodel = GetAvailableData(TenantId, AggregationId, SiteId).SingleOrDefault(s => s.Id == id);
        if (oldmodel == null) return NotFound();

        dbContext.Entry(AutoMapToEntity(model)).State = EntityState.Modified;
        dbContext.SaveChanges();
        return StatusCode(System.Net.HttpStatusCode.NoContent);
    }

    [UppBundleEngine.Web.Permission.WebApi.PermissionAuthorize(
        DisplayName = "删除活动",
        AuthType = AuthorizeType.ACL | AuthorizeType.PermissionCode,
        AuthorizeInfomation = "DeleteActivity"
        )]
    public override IHttpActionResult Delete(Guid TenantId, Guid AggregationId, Guid SiteId, Guid id)
    {
        var model = GetAvailableData(TenantId, AggregationId, SiteId).SingleOrDefault(s => s.Id == id);
        if (model == null) return NotFound();

        dbContext.Entry(model).State = EntityState.Deleted;
        dbContext.SaveChanges();
        return Ok(AutoMapToModel(model));
    }

    protected override bool ModelExists(Guid TenantId, Guid AggregationId, Guid SiteId, Guid id)
    {
        return db.Any(s => s.Id == id);
    }
}

其中AuthType能够为:ACL/PermissionCode/NoNeed,须要仅登陆能够再加上系统的[Authorize]。能够看到这个Controller里基本都是通用代码,因此实际上能够直接复制粘贴快速的建立资源Api,至于那个自定义的抽象类BaseController实现的功能:restful

  • 从数据上下文中把对应的数据提取出来。
  • 使用EmitMapper提供了数据模型和传输模型间的映射。
  • InitVisibleSiteIds实现了调用租户模块提供的服务,查找TenantId对应的SiteIds。
  • 调用Member模块,读取当前登陆用户,用户信息。
  • 提供Get、GetOne、Post、Put、Delete模板方法,若是须要能够深度集成Odata for WebApi,就可使用Patch方法。

权限部分实现了RBAC和ACL两种权限方式,用RBAC来管理“谁能怎么操做哪些资源”这种权限,用ACL来管理“谁能怎么操做哪些数据”这种权限。权限模块能够同时应用于MVC和WebApi。实现的方式是自定义AuthorizeAttribute,来实现拦截,能够很容易拿到RBAC所须要的数据,而ACL就麻烦些了,总不能定死url吧,因此根据Sharepoint的启发设计了这种路由:
api/XXX/WebApiExt/ACL/Activity/{TenantId}/{AggregationId}/{SiteId}/{Id}?xxx

  • “XXX/WebApiExt”是插件的命名空间。
  • “ACL”表明这些Api须要采用ACL方式进行控制,写什么均可以没有强制定义,通常开放给资源管理者的Api都用ACL(后台),若是不用ACL的,好比开放给资源读取、动词类的Api,咱们通常写成“Common”(前台),以示区分。
  • 我设计的ACL的判断方式为:比对路由和实际访问路径的差别化部分再加上Http Method做为特征值和数据库存储的用户可访问列表进行比对,支持通配符。因此“{TenantId}/{AggregationId}/{SiteId}”是ACL实现的基础,就是租户模块。

    资源A的实体字段里存储SiteId,而租户模块中存储着TenantId和SiteId的对应关系、AggregationId和SiteId的对应关系,AggregationId做为聚合租户内不一样子站点的一种方式,甚至能够根据须要聚合不一样租户下的数据,为系统提供了足够的灵活性。

权限模块附带的一个功能就是能够在写Api的时候直接把文档写上去,集成后的ASP.NET Web API Help Page页就变成了:

权限设计-前端

后端的权限设计的描述方法是不适合于前端的,因此前端就须要维护相应的对应关系,将前端业务上的的Feature和后端Api的RBAC的权限进行对应,后端的ACL在对用户分组时处理便可。

客户端

客户端主要实现业务逻辑,后端直接暴露资源,因此能够看做是直连数据库操做,而且不用太过考虑安全性问题,数据校验更多的是从交互体验角度去考虑。Web的话咱们使用的是AnglarJs作SPA开发,PC应用使用WPF开发。在这种模式的开发下客户端的工做就稍微有些复杂,对于一些模型的ExtType和ExtData都要求有比较好的处理机制,不过由于是客户端因此对处理性能要求就不是很高了。

相关传送门:

相关文章
相关标签/搜索