踩过了一段时间的坑,现总结一下,与大家分享,愿与大家一起讨论。
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基础上进行的改造:
(注:插件系统没有完整的重构过,所以有部分设计会有些不合理)
服务端的主要任务就是开放资源访问和开放一些必须要后端来实现的功能性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实现的功能:
权限部分实现了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都要求有比较好的处理机制,不过因为是客户端所以对处理性能要求就不是很高了。
相关传送门: