前言
踩過了一段時間的坑,現總結一下,與大家分享,願與大家一起討論。
Restful WebApi特點
WebApi相較於Asp.Net MVC/WebForm開發的特點就是前后端完全分離,后端使用WebApi直接針對資源進行暴露,大部分的業務轉移到前端進行。前端可以采用Html頁面或各平台的原生程序開發,非常靈活。
我們采用的是WebApi+angularjs/WPF的方式開發。
設計思想
目前就算使用Asp.net MVC開發,為了用戶體驗也需要使用Ajax來異步加載數據,而Html5的單頁App也越來越流行,所以干脆讓后端只提供數據的存儲,Api除極個別情況只針對實體提供實體的增刪改查功能,后台盡量摘除業務邏輯,把業務邏輯移到前端實現。使后端專注於數據倉儲和數據查詢的性能優化,而前端更專注於業務邏輯、UI等方面的優化。
服務端
根據數據模型創建ApiController直接暴露實體,處理增刪改查,配合Odata擴展使用非常方便。
這塊看上去簡單,其實是很重要的一個地方。由於直接對資源/實體進行暴露,通訊采用的又是HTTP協議,前端是無法保證Api訪問安全的,而且業務邏輯也移到了前端,所以后端Api的安全性、權限攔截的粒度和靈活性尤為重要。一般進行權限攔截都會針對功能特性進行判斷,比如:XX用戶能否使用A功能,但是Restful WebApi提供的Api是直接針對資源/實體的,業務邏輯又移到了客戶端去實現,后端在業務上功能性的描述弱化了,變成了:能否增/刪/改/查A資源,而這種轉變就要求權限需要攔截到數據行級別。
服務端我是在HappyFramework.OSGi基礎上進行的改造:
(注:插件系統沒有完整的重構過,所以有部分設計會有些不合理)
- 精簡掉主體中不用的的部分,比如ioc、企業庫。
- 把主體改造成實現+契約兩個類庫。
- 添加自寫的權限模塊、自寫服務定位器實現的服務總線、觀察者模式的事件總線,全部使用反射進行查找組裝。
- 根據服務總線的需要添加預啟動插件狀態。
- 添加WebApi集成,實現CORS,替換系統WebApi的服務:IAssembliesResolver、IHttpControllerTypeResolver、IHttpControllerSelector實現插件的控制器加載、命名空間隔離。
服務端的主要任務就是開放資源訪問和開放一些必須要后端來實現的功能性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實現的功能:
- 從數據上下文中把對應的數據提取出來。
- 使用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都要求有比較好的處理機制,不過因為是客戶端所以對處理性能要求就不是很高了。
相關傳送門: