踩過了一段時間的坑,現總結一下,與你們分享,願與你們一塊兒討論。html
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資源,而這種轉變就要求權限須要攔截到數據行級別。數據庫
服務端我是在HappyFramework.OSGi基礎上進行的改造:
(注:插件系統沒有完整的重構過,因此有部分設計會有些不合理)後端
服務端的主要任務就是開放資源訪問和開放一些必需要後端來實現的功能性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
權限部分實現了RBAC和ACL兩種權限方式,用RBAC來管理「誰能怎麼操做哪些資源」這種權限,用ACL來管理「誰能怎麼操做哪些數據」這種權限。權限模塊能夠同時應用於MVC和WebApi。實現的方式是自定義AuthorizeAttribute,來實現攔截,能夠很容易拿到RBAC所須要的數據,而ACL就麻煩些了,總不能定死url吧,因此根據Sharepoint的啓發設計了這種路由:
api/XXX/WebApiExt/ACL/Activity/{TenantId}/{AggregationId}/{SiteId}/{Id}?xxx
權限模塊附帶的一個功能就是能夠在寫Api的時候直接把文檔寫上去,集成後的ASP.NET Web API Help Page頁就變成了:
後端的權限設計的描述方法是不適合於前端的,因此前端就須要維護相應的對應關係,將前端業務上的的Feature和後端Api的RBAC的權限進行對應,後端的ACL在對用戶分組時處理便可。
客戶端主要實現業務邏輯,後端直接暴露資源,因此能夠看做是直連數據庫操做,而且不用太過考慮安全性問題,數據校驗更多的是從交互體驗角度去考慮。Web的話咱們使用的是AnglarJs作SPA開發,PC應用使用WPF開發。在這種模式的開發下客戶端的工做就稍微有些複雜,對於一些模型的ExtType和ExtData都要求有比較好的處理機制,不過由於是客戶端因此對處理性能要求就不是很高了。
相關傳送門: