1.0.0 • Published 4 years ago

fht-agency-web v1.0.0

Weekly downloads
-
License
ISC
Repository
-
Last release
4 years ago

代理商后台文档

权限控制

使用ASP.NET Identity提供的Claim-Based Authentication而非Role-Based Authentication

Role & Claim

每个用户有且只能有一个Role,而且创建用户之后不可以改变Role.

Role与Claim之间是一对多的关系。一个Role对应的Claim的数量可以增减。但是收到ApplicableToRole范围的限制(见下面)

ClaimAuthorize

建议在Action上面通过标记[ClaimAuthorize(...)]的方式,控制一个Action的权限,而非使用[Authorize(Role="Administrator")]这样的方式。因为后者是Role-based authentication的写法。

反射菜单列表

代理商左侧菜单栏是通过反射的方式控制的。

首先在enum FhtUserClaimType上面标记好ApplicableToRoleMenuItem这些attribute之后。 这里每个claim与MenuItem差不多是一一对应的关系。这样当前用户只能看到自己这个Role对应的那些Claim的菜单.

代码中的权限按照部门写死

代理商后台开发初期,对代理商后台的灵活性有过很长时间的讨论。主要有两种方案:

方案一

  • 用户的Role可以灵活的改变,譬如今天你是渠道经理,明天可以改成客服专员。甚至因为我们建模的时候将代理商用户和我们烽火台内部用户都当做用户的情况下,你还可以将渠道经理改成代理商客服这个Role。
  • 这样的话,代码就必须写成在每一个操作之前,检查当前用户的Role。譬如说查看代理商每天的打款,必须确认当前用户的Role是不是财务经理,否则抛出异常。
  • 甚至在这个方案里面,还考虑过是否一个人有多个角色,就是同时是代理商和渠道经理。。这样奇怪的组合。
  • 每个功能(也就差不多是菜单),可以任意分配给一个角色。

这个方案写代码的时候,到处可能都要这么写:

var user = userRepository.Get(userId);
if(user.Role != UserRoleType.AccountantManager)
{
    throw new UnauthorizedAccessException("..");
}

假如忘了写,就会导致这个地方有安全漏洞。

另外一个问题,就是这么灵活的可以改变,特别是代理商本身也作为角色的话,就是要处理每个角色背后不太一样的其他功能的问题。譬如说每个代理商用户都对应这一个AgentOrganization,又或者一个部门内部,有上下级的关系,这个目前是用外键指向的。假如这个人的角色要换,还需要处理好这些关系才能换。

所以这个方案我感觉太灵活而且对代码每个地方要事无巨细的检查这个用户的角色是不是可以这么操作,容易遗漏。而且检查遗漏只能一点点的看代码。

方案二

userRepository里面有GetAdmin, GetAgent, GetChannel这样的写法。

每个GetXxx后面的Xxx大致对应于江苏火火内部的一个部门(代理商也算作一个部门)。每个部门内部可以不同的Role.

在某一个Service或者Query代码中:

var channelUser = userRepository.GetChannel(userId); //假如这个人的Role不是ChannelManager, RegionManager(大区经理), ChannelSupervisor(渠道总监)中的任何一个,这个方法直接就会抛出异常,这样下面的代码根本不会执行。

目前后台业务代码中,写死了某个写操作或者读操作,只适用于某一种或几种部门。所以这个操作,在一个部门内部的Claim是可以增删的。也就是今天客服专员可以看代理商打款,明天我可以把这个权限从客服专员的权限列表中删除。

但是,这么写的副作用是,这个段Serivce或者Query的方法对应的Claim不可以随意分配给渠道部门之外的角色。假如这个代码分配给客服部门的人,在上面userRepository.GetChannel(userId)的地方直接就会抛出异常。当然,这个写法,要配合ApplicableToRoleAttribute使用,也就是管理员给某个Role,分配Claim的时候,只能看到可以分配的那些Claim。当然代码里面实际需要的部门与ApplicationToRole里面要相吻合。这个地方无法自动化,只能够标记的时候认真一点,做到标注匹配代码。

前端部分注意内容

代理商后台的UI部分使用了bootstrap和adminlte后,尽量让开发精力放在后端功能,所以前端部分需要保持一致。大家主要通过参考之前类似页面的写法。主要有如下注意事项:

  • 尽可能多的使用ASP.NET MVC的功能快速搭建页面,如使用@Html.EditorFor, EditorTemplates, Validation Annotation, Client-Side Validation, Remote Validation等技术点。
  • 重复的功能,通过js控制,譬如面包屑与菜单栏重合的部分,用js拷贝到面包屑上面去。

文件结构

静态文件如css, js, images不要放到Content或者Scripts文件夹里面, 请放到Asset文件夹里面!!!。这两个文件夹主要用来放nuget自动下载或者我们外部引用的库,如vue.js, respond.js等。

主要是为了方便使用ag或者powershell搜索的时候,收到很多库文件里面的内容。

UIStandard.js

日历的汉化,select2的封装, image uploader都模块化放在UIStandard.js里面,只要引用,标注好合适的class即可。

很多页面都有一个列表,然后可以根据一定的条件和文本进行过滤和搜索。这个被封装为一个叫filterable的控件。不复杂,但是挺有用。

Casperjs 自动化测试

因为代理商后台的前端页面,对于不同角色的用户来说,看到的页面功能不同。目前我们已经有至少五种用户角色。切换不同用户然后检查颇为麻烦。

所以,我写了一个简单的使用Casperjs自动检查所有页面的简单前端错误的脚本:

FHT\FHT.Agency.UnitTests\Casperjs 下面找到check_all_roles.bat,并点击,可以看到。所以定那些FAILED行,然后消除错误。

{F10994}

假如觉得显示在控制台看不方便,可以将脚本运行的输出导入到文件里面, 在cmd.exe控制台里面运行check_all_roles.bat > all.log, 完了去all.log文件里面去查找。

事实上这个脚本是用check_all_roles.bat调用了check_all_roles.ps1 powershell脚本。

一些可调参数

这个脚本测试哪个环境下面的代理商后台,是通过脚本运行时候的环境变量决定的。现在有两个环境变量,放在:

$env:ApplicationEnvironment = "MobileDevelopment" # 值可以是: Local, Development(默认), MobileDevelopment, Staging, Production,选择不同的值,对应不同的URL,假如选择MobileDevelopment,那么就测试 http://mobiledevelopment.fht360agency.com/ 这个url的代理商后台。
$env:CaptureImages = "true" # 只要是true就会将每页的截图并且存在`./snapshots`文件夹

filterable

列表页面的封装,采用BEM命名规范,检索条件统一放在.filterable__query里,检索采用Form表单提交方式(FormMethod.Get),检索条件InputModel用于强类型视图处理。

提交只需标注样式filterable__submit-btn,重置标注样式filterable__clear-btn,检索结果标注样式 filterable__result

智能提示 代理商,代理商客户,套餐名字

这三个需要智能提示的地方也是特别多,所以封装了这三个html class,标注之后既有 select2智能提示的效果。详见uistandard.js里面的实现。

autocomplete-agent-client autocomplete-agent autocomplete-package

##分页

实现分页功能统一采用MVCPaging 控件

##EditorTemplates

列表中目前常用的两个EditorTemplates: 枚举:EnumWithAll.cshtml(主要用于枚举,同时加上‘--全部--’) ,现在枚举属性上标注[UIHint("EnumWithAll")],在使用使用处直接调用,如:@Html.EditorFor(枚举的属性, new { enumSource = typeof(枚举) })

日期区间:DateRange.cshtml(用于区间时间检索),属性上标注[UIHint("DateRange")],使用代码:@Html.EditorFor(m => m.DateRange)