公司的ERP系统开发一拖再拖都拖了几个月了,需求分析和规划文档我很早之前就提交了就是不回复,到年终要放假了老板才着急有什么用呢?
言归正传!MVC下面的权限控制,有些系统采用的方式就是在Action加一个[Authorize(Roles = "admin",Users="User1,User2")]的属性来验证授权,这种方式粒度太大而且很不灵活。这种方式把每个Action和Role、User强制关联起来了,在系统设计之初就需要确定哪些角色或者哪些用户有对此Action的访问权限,如果后期更改就需要改代码可能还要改很多地方,角色授权在系统真正运行之后由系统管理员通过系统动态的赋予解除较好。
本人的设计思路:后台系统的导航菜单也就是指向一些资源的链接,所以菜单表的核心字段就是菜单名称、菜单code(唯一)、资源路径。菜单链接点进去之后一般就是一些列表或报表页面,每一个列表页面都对应着数据库的一张表,即一个数据集合,每个数据集合其实就是一类业务单据的数据库记录,所以每个菜单就对应一类业务单据。报表就是一些数据表的汇总,也可以看作某一类业务实体。对于报表所需要的权限一般就是查看不需要操作什么,对于一张具体的数据库表我们一般需要做的操作就是查看、查看所有、增加、删除、修改和审核。因此资源所需的权限范围:查看、查看所有、增加、删除、修改和审核。所以权限表记录有限,就是前面介绍的6个权限,核心字段就是权限名称及其code。其实Controller里的每个Action都应该对应某一类业务单据的某一种操作,即要么是查看要么是增加要么是删除...因此我们应该把Action和某一个业务单据某一种操作权限对应起来,即在Action应用[Authorize(AuthCode = Authorization.CAIGOUDAN_View)]特性(意为:所有拥有采购单查看权限的用户均可以执行此Action,这样开发人员对某一Action的访问控制关注点就放在了是否应有拥有某一单据操作的权限上了,而不是那些角色)。每个用户可以有一个或多个角色,每个角色下面拥有一个或多个用户,所以用户表和角色表是多对多的关系,需要一张中间表即用户角色表。最后一张关键表就是把菜单表、角色表和权限表关联起来的菜单角色权限表。
一、讲了一堆废话,来一张表关系和表结构更直观,如下图:
表结构基本字段,用户表Users:
角色表Role:
用户角色表UserRole:
菜单表MenuItems:
权限表Permissions:
菜单角色权限表Authorizations:
备注:Authorizations.Code=MenuItems.MenuCode+"_"+Permissions.PermissionCode
测试数据(注意GUID改为自己系统的):
--insert into Roles values(NEWID(),'admin',1); --insert into Roles values(NEWID(),'manager',2); --insert into Users(UserId,UserName,Salt,Password,TrueName,IsSysUser,Status,StatusName,RegTime,RegIp) --values(NEWID(),'admin','','123','老板',0,0,'正常',GETDATE(),'') --insert into UserRole values(NEWID(),'8B51CDA0-EC4A-459B-9249-CA1FA06B0325','118696A7-FF0B-4AC5-AE9F-04F7B04735F7') --insert into UserRole values(NEWID(),'8B51CDA0-EC4A-459B-9249-CA1FA06B0325','D7253244-B7F6-40A9-A5F7-57D0F13AF01E') --insert into Permissions values('101','View','查看',1) --insert into Permissions values('102','ViewAll','查看所有',2) --insert into Permissions values('103','Add','添加',3) --insert into Permissions values('104','Edit','修改',4) --insert into Permissions values('105','Delete','删除',5) --insert into Permissions values('106','Audit','审核',6) --insert into Authorizations values(NEWID(),'118696A7-FF0B-4AC5-AE9F-04F7B04735F7','D54BAB40-BF30-8146-458F-B934E462D5C5','101','CAIGOUDAN_View') --insert into Authorizations values(NEWID(),'118696A7-FF0B-4AC5-AE9F-04F7B04735F7','D54BAB40-BF30-8146-458F-B934E462D5C5','102','CAIGOUDAN_ViewAll') --insert into Authorizations values(NEWID(),'118696A7-FF0B-4AC5-AE9F-04F7B04735F7','D54BAB40-BF30-8146-458F-B934E462D5C5','103','CAIGOUDAN_Add') --insert into Authorizations values(NEWID(),'118696A7-FF0B-4AC5-AE9F-04F7B04735F7','D54BAB40-BF30-8146-458F-B934E462D5C5','104','CAIGOUDAN_Edit') --insert into Authorizations values(NEWID(),'118696A7-FF0B-4AC5-AE9F-04F7B04735F7','D54BAB40-BF30-8146-458F-B934E462D5C5','105','CAIGOUDAN_Delete') --insert into Authorizations values(NEWID(),'118696A7-FF0B-4AC5-AE9F-04F7B04735F7','D54BAB40-BF30-8146-458F-B934E462D5C5','106','CAIGOUDAN_Audit')
二、表设计完成,下面就介绍实EF体类配置:
用户表Users实体类:
public class Users { public Guid UserId { get; set; } public string UserName { get; set; } public string Salt { get; set; } public string Password { get; set; } public virtual List<Roles> Roles { get; set; } } public class UserConfiguration:EntityTypeConfiguration<Users> { public UserConfiguration() { HasKey(u=>u.UserId); Property(u => u.UserId).IsRequired(); HasMany(u => u.Roles).WithMany(u => u.Users).Map(m => { m.ToTable("UserRole"); m.MapLeftKey("UserId"); m.MapRightKey("RoleId"); }); } }
角色表Roles实体类:
public class Roles { public Guid RoleId { get; set; } public string RoleName { get; set; } public int? Sort { get; set; } public virtual List<Users> Users { get; set; } public virtual List<Authorizations> Authorizations { get; set; } } public class RoleConfiguration : EntityTypeConfiguration<Roles> { public RoleConfiguration() { HasKey(R => R.RoleId); Property(R => R.RoleId).IsRequired(); } }
用户角色表UserRole实体类:
public class UserRole { public Guid UserRoleId { get; set; } public Guid UserId { get; set; } public Guid RoleId { get; set; } }
菜单表MenuItem实体类:
public class MenuItem { public MenuItem() { Children = new List<MenuItem>(); } public Guid MenuItemID { get; set; } public string MenuName { get; set; } public Guid ParentMenuID { get; set; } public string Url { get; set; } public int Sort { get; set; } [NotMapped] public List<MenuItem> Children { get; set; } }
权限表Permissions实体类:
public class Permissions { [System.ComponentModel.DataAnnotations.Key] public string PermissionId { get; set; } public string PermissionCode { get; set; } public string PermissionName { get; set; } public int? Sort { get; set; } }
菜单角色权限表Authorizations实体类:
public class Authorizations { [Key] public Guid Id { get; set; } public Guid RoleId { get; set; } public Guid MenuItemId { get; set; } public string PermissionId { get; set; } public string Code { get; set; } }
EF上下文:
public class ERPDbContext : DbContext { public DbSet<MenuGroup> MenuGroups { get; set; } public DbSet<MenuItem> MenuItems { get; set; } public DbSet<Users> Users { get; set; } public DbSet<Roles> Roles { get; set; } public DbSet<UserRole> UserRole { get; set; } public DbSet<Permissions> Permissions { get; set; } public DbSet<Authorizations> Authorizations { get; set; } public ERPDbContext(string connectionString) : base(connectionString) { } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Configurations .Add(new UserConfiguration()) .Add(new RoleConfiguration()); base.OnModelCreating(modelBuilder); } public ERPDbContext() { } }
三、Forms认证模块
webconfig配置:
<system.web> <compilation debug="true" targetFramework="4.0" /> <authentication mode="Forms"> <forms loginUrl="~/Account/Login" timeout="2880" name="tkerp" defaultUrl="~/Home/Index" path="/" requireSSL="false" cookieless="UseDeviceProfile" slidingExpiration="true" protection="All" enableCrossAppRedirects="false"/> </authentication> <machineKey validation="3DES"/>
存储用户相关登录信息的类:
public class AuthModel { /// <summary> /// 用户ID /// </summary> public string UserId { get; set; } /// <summary> /// 用户名 /// </summary> public string UserName { get; set; } /// <summary> /// 此用户所属的所有角色 /// </summary> public List<string> Roles { get; set; } /// <summary> /// 此用户所有的权限代码:Authorizations表的Code字段 /// </summary> public List<string> AuthCodes { get; set; } }
FormsAuthTool类将创建Forms票据和将票据信息转为Principal信息
public class FormsAuthTool { //Cookie 和 票据 自动登录保存是时间 private const int CookieSaveDays = 14; /// <summary> /// 用户登录成功时设置Cookie /// </summary> /// <param name="username">用户名</param> /// <param name="authModel">验证时用户模型</param> /// <param name="rememberMe">是否自动登录</param> public static void SetAuthCookie(string username, AuthModel authModel, bool rememberMe) { if (username == null || authModel == null) throw new ArgumentNullException("Illegal Parameter."); string userData = JsonConvert.SerializeObject(authModel); //创建ticket FormsAuthenticationTicket ticket; if (rememberMe) ticket = new FormsAuthenticationTicket(1, username, DateTime.Now, DateTime.Now.AddDays(CookieSaveDays), rememberMe, userData); else ticket = new FormsAuthenticationTicket(1, username, DateTime.Now, DateTime.Now.AddMinutes(120), rememberMe, userData); //加密ticket var cookieValue = FormsAuthentication.Encrypt(ticket); //创建Cookie var cookie = new HttpCookie(FormsAuthentication.FormsCookieName, cookieValue) { HttpOnly = true, Secure = FormsAuthentication.RequireSSL, Domain = FormsAuthentication.CookieDomain, Path = FormsAuthentication.FormsCookiePath, }; if (rememberMe) cookie.Expires = DateTime.Now.AddDays(CookieSaveDays); //写入Cookie HttpContext.Current.Response.Cookies.Remove(cookie.Name); HttpContext.Current.Response.Cookies.Add(cookie); } /// <summary> /// 从Request中解析出Ticket,UserData /// </summary> /// <param name="request">HttpRequest</param> /// <returns></returns> public static ERPPrincipal TryParsePrincipal(HttpRequest request) { if (request == null) throw new ArgumentNullException("Illegal Parameter."); //读登录Cookie var cookie = request.Cookies[FormsAuthentication.FormsCookieName]; if (cookie == null || string.IsNullOrEmpty(cookie.Value)) return null; try { //解密Cookie值,获取FormsAuthenticationTicket对象 var ticket = FormsAuthentication.Decrypt(cookie.Value); if (ticket != null && !string.IsNullOrEmpty(ticket.UserData)) { var authModel = JsonConvert.DeserializeObject<AuthModel>(ticket.UserData); if (authModel != null) { return new ERPPrincipal(ticket, authModel); } } return null; } catch { return null; } } }
ERPPrincipal类
public class ERPPrincipal : IPrincipal { public ERPPrincipal(FormsAuthenticationTicket ticket, AuthModel authModel) { Identity = new FormsIdentity(ticket); UserInfo = authModel; } private AuthModel UserInfo; public IIdentity Identity { get; private set; } /// <summary> /// 判断用户是否拥有某权限 /// </summary> /// <param name="authCode">权限代码</param> public bool IsInRole(string authCode) { if (string.IsNullOrEmpty(authCode)) return true; try { if (UserInfo.AuthCodes == null) return false; return UserInfo.AuthCodes.Contains(authCode); } catch { return false; } } }
ERPAuthorizeAttribute类
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)] /// <summary> /// 权限过滤器 /// </summary> public class ERPAuthorizeAttribute : AuthorizeAttribute { /// <summary> /// 判断是否是登录用户 /// </summary> private bool IsLogin = false; /// <summary> /// 访问此Action所需的权限代码 /// </summary> public string AuthCode { get; set; } //在OnAuthorization执行后执行 protected override bool AuthorizeCore(HttpContextBase httpContext) { var user = httpContext.User as ERPPrincipal; if (user != null) { IsLogin = user.Identity.IsAuthenticated; return (user.IsInRole(AuthCode)); } else { IsLogin = false; } return false; } //AuthorizeCore 返回false时 调用该处理函数 protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext) { if (!IsLogin) filterContext.Result = new RedirectResult("/Account/Login");//没有登录的用户跳转到登录页面 else filterContext.Result = new RedirectResult("/Account/NoPermission"); } }
把数据库权限代码写到实体类中,要写好注释:
public class Authorization { #region 采购订单 /// <summary> /// 采购订单-查看 /// </summary> public const string CAIGOUDAN_View="CAIGOUDAN_View"; /// <summary> /// 采购订单-查看所有 /// </summary> public const string CAIGOUDAN_ViewAll="CAIGOUDAN_ViewAll"; /// <summary> /// 采购订单-增加 /// </summary> public const string CAIGOUDAN_Add="CAIGOUDAN_Add"; /// <summary> /// 采购订单-编辑 /// </summary> public const string CAIGOUDAN_Edit="CAIGOUDAN_Edit"; /// <summary> /// 采购订单-删除 /// </summary> public const string CAIGOUDAN_Delete="CAIGOUDAN_Delete"; /// <summary> /// 采购订单-审核 /// </summary> public const string CAIGOUDAN_Audit = "CAIGOUDAN_Audit"; #endregion }
四、验证授权测试
在Global.asax中添加如下代码:
protected void Application_PostAuthenticateRequest(object sender, System.EventArgs e) { var formsIdentity = HttpContext.Current.User.Identity as FormsIdentity; if (formsIdentity != null && formsIdentity.IsAuthenticated && formsIdentity.AuthenticationType == "Forms") { HttpContext.Current.User = FormsAuthTool.TryParsePrincipal(HttpContext.Current.Request); } }
控制器登录Action:
public class LogOnModel //登录模型 { public string username { get; set; } public string password { get; set; } public bool rememberme { get; set; } }
public class AccountController : Controller { // // GET: /Account/ public ActionResult Index() { return View(); } [HttpGet] public ActionResult Login() { return View(); } [HttpPost] public ActionResult Login(LogOnModel model,string returnUrl="") { if (!ModelState.IsValid) return View("Login"); ERPDbContext DbContext = new ERPDbContext("ERPDbContext"); var rlt = DbContext.Users.Where(u => u.UserName == model.username && u.Password == model.password).FirstOrDefault(); if (rlt == null) { ModelState.AddModelError("error", "用户名或密码不正确"); return View("Login"); } try { List<ERP.Models.Roles> rolelist = rlt.Roles.ToList(); List<string> authcodes = new List<string>(); foreach (var rl in rolelist) { foreach (var au in rl.Authorizations) { if (!authcodes.Contains(au.Code)) { authcodes.Add(au.Code); } } } AuthModel am = new AuthModel() { UserId = rlt.UserId.ToString(), UserName = rlt.UserName, Roles = rolelist.Select(r => r.RoleName).ToList(), AuthCodes = authcodes }; FormsAuthTool.SetAuthCookie(model.username, am, model.rememberme); if (!string.IsNullOrEmpty(returnUrl)) return Redirect(returnUrl); else return Redirect("/Home/Index"); } catch { return View("Login"); } } public ActionResult LogOff() { FormsAuthentication.SignOut(); return View("Login"); } public ActionResult NoPermission() { return View(); } }
查看某一单据:
public class ManagerController : Controller { // // GET: /PurchaseOrder/Manager/ [ERPAuthorize(AuthCode = Authorization.CAIGOUDAN_View)] //[ERPAuthorize(AuthCode = "Reply")] public ActionResult Index() { return View(); } }
前端登录页面:
Login
@{ ViewBag.Title = "Login"; Layout = null; } <h2>Login</h2> <div> <form action="Login" method="post"> <div> <label>用户名:</label><input type="text" name="username" width="200" /> </div> <div> <label>密 码:</label><input type="text" name="password" width="200" /> </div> <div> <input type="checkbox" name="rememberme" /> </div> <div><input type="submit" value="Login"></div> <input type="hidden" name="returnUrl" value="/PurchaseOrder/Manager/Index" /> </form> </div>
NoPermission
@{ ViewBag.Title = "NoPermission"; Layout = null; } <h2>NoPermission</h2>
备注:菜单加载呈现代码没有写,要浏览器手动敲URL测试验证授权,这仅是一个简单验证授权后台框架
本人技术一般,写作水平一般,有不足之处欢迎指点,不喜勿喷。