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都要求有比較好的處理機制,不過由於是客戶端因此對處理性能要求就不是很高了。

相關傳送門:

相關文章
相關標籤/搜索