asp.net core 身份认证/权限管理系统简介及简单案例
如今的网站大多数都离不开账号注册及用户管理,而这些功能就是通常说的身份验证。这些常见功能微软都为我们做了封装,我们只要利用.net core提供的一些工具就可以很方便的搭建适用于大部分应用的权限管理系统。
先贴官方文档地址:https://docs.microsoft.com/en-us/aspnet/core/security/authentication/identity 善用谷歌翻译可以减少语言障碍。
这里我们用一个相对简单的权限管理案例来作为讲解。环境:vs2017,asp.net core 2.1,sqlserver 2017 dev,datagrip
新建asp.net core项目,选择更改身份验证->个人用户账号->存储应用内的用户账户
为了之后更直观的查看成果,项目创建间完成后将appsettings.json中的默认连接字符串修改以连接自己的数据库。
这个时候启动项目,点击右上角的注册,填写邮箱和密码(密码要求包含大小写字母及符号)
完成后提交,发现报错了。查看错误日志,提示数据库没有迁移。回到资源管理器下的项目根目录,重新生成解决方案,运行:
dotnet ef migrations add first
dotnet ef database update
这两条命令的详细介绍见微软官方文档:https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/
完成后用数据库连接工具查看数据库下的默认架构(dbo),发现多出来了8张表。
为了给大家一个初步的印象以理解之后的内容,这里需要稍微讲解几个名词:
User:用户。包含了用户名、邮箱、昵称、加密后的登录密码等数据
Role:角色。用来承载权限。
Claims:具体的权限。比如能够发表文章是一条权限,能够删除文章也是一条权限。
权限可以赋予给具体的用户和角色。角色/用户和权限是多对多的关系。
生成的8张表就是为了存放这些数据。其中__EFMigrationsHistory用来记录数据库的迁移历史,如果需要重新迁移则需要清空或删除这张表。
这时候我们再打开调试,跟之前一样注册用户,这次我们没有看到任何错误,完成后跳转到了首页,右上角也显示出了我们刚刚注册的用户名。
现在我们尝试添加一个需要一定权限才能访问的接口。新建Auth控制器(AuthController),创建AddEmp方法。
public class AuthController : Controller { [Authorize(Policy = "ManagerOnly")] //Policy的意思是“策略”,可以理解为对一组权限需求的描述。ManagerOnly是Policy的名字 public IActionResult AddEmp() { return Content("true"); } }
转到Startup.cs的ConfigureServices,添加
services.AddAuthorization(opt => { opt.AddPolicy(“ManagerOnly”, p => p.RequireRole(“manager”)); });
这一行可以理解为添加名为ManagerOnly的权限策略,这个策略要求访问者具备名为manager的角色。
现在我们 启动网站,访问/auth/AddEmp,发现跳转到了权限不足页面。怎样才能获得权限,让结果变成true呢
由于默认的代码没有提供角色管理,这时候我们需要对startup.cs进行修改。
修改ConfigureServices方法,将
services.AddDefaultIdentity<IdentityUser>()
.AddEntityFrameworkStores<ApplicationDbContext>();
改为
// services.AddDefaultIdentity<IdentityUser>() services.AddIdentity<IdentityUser, IdentityRole>() .AddEntityFrameworkStores<ApplicationDbContext>();
IdentityUser, IdentityRole是系统默认的用户和角色,如果我们需要添加自定义属性,就需要自定义类来继承这两个自带的类。这个我们之后讲。
添加完毕后,我们回到AuthController,使用依赖注入来注入Manager类:
private UserManager<IdentityUser> _userManager; private RoleManager<IdentityRole> _roleManager; public AuthController(UserManager<IdentityUser> userManager, RoleManager<IdentityRole> roleManager) { _userManager = userManager; _roleManager = roleManager; }
完成后添加AddManagerRole方法:
public IActionResult AddManagerRole() { var role = _roleManager.Roles.SingleOrDefault(o=>o.Name=="manager"); if (role==null) { var identityResult = _roleManager.CreateAsync(new IdentityRole("manager")).Result; if (!identityResult.Succeeded) { return Content("create role error"); } } var user = _userManager.GetUserAsync(User).Result; //User指的是当前登陆的用户。HttpContext中自带 var result = _userManager.AddToRoleAsync(user, "manager").Result; if (result.Succeeded) { return Content("true"); } return Content("false"); }
完成后访问/Auth/AddManagerRole,返回true的话就成功的为当前登录用户添加“manager”角色了。
然后我们访问之前的/Auth/AddEmp接口,发现这次成功的返回了true。这就是基于角色的权限管理。
接下来谈谈之前提到的Claim,这次我们尝试使用claim进行权限管理。
auth控制器添加方法Raise
[Authorize(Policy = "RequireRaise")] public IActionResult Raise() { return Content("true"); }
Startup.cs添加Policy
services.AddAuthorization(opt => { opt.AddPolicy("ManagerOnly", p => p.RequireRole("manager")); opt.AddPolicy("RequireRaise",p=>p.RequireClaim("salary","raise")); });
完成后访问/Auth/Raise,提示权限不足。
为了获得权限,auth控制器添加方法AddRaiseClaim:
public IActionResult AddRaiseClaim() { var user = _userManager.GetUserAsync(User).Result; var result = _userManager.AddClaimAsync(user, new Claim("salary", "raise")).Result; //一个Claim由Type和Value构成。这句的意思是添加type为salary,value为raise的claim。 if (result.Succeeded) { return Content("true"); } return Content("false"); }
完成后启动网站,访问/Auth/AddRaiseClaim,返回true则表示添加成功。
再次访问/Auth/Raise,返回true则表示鉴权成功。
最后再来讲解一下如何修改asp.net core自带User和Role的主键类型。
新建User类和Role类,分别继承IdentityUser和IdentityRole并添加目标类型泛型。这里以修改为int型为例:
public class User : IdentityUser<int> //可添加自定义属性 public class Role:IdentityRole<int>
回到startup.cs,将
services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
改为
services.AddIdentity<User,Role>()
.AddEntityFrameworkStores<ApplicationDbContext>();
转到ApplicationDbContext.cs,注册之前添加的用户和角色:
public class ApplicationDbContext : IdentityDbContext<User,Role,int>
完成后删除数据库中之前迁移的8张表,并且删除项目中的Migrations文件夹(很重要,不然报错),重新执行迁移命令:
dotnet ef migrations add sec
dotnet ef database update
完成后查看数据库,发现主键已经成功修改成了int并且自增了。
这里需要注意的一点是,如果以后在ApplicationDbContext里注册新的类,并修改OnModelCreating方法的时候,记得调用base.OnModelCreating(modelBuilder); 不然会报错。
这时候启动网站,发现报错了。提示No service for type \’Microsoft.AspNetCore.Identity.UserManager`1[Microsoft.AspNetCore.Identity.IdentityUser]\’ has been registered. 意思就是UserManager<IdentityUser>没注册。因为我们之前在startup中将services.Addidentity<IdentityUser, IdentityRole>()改成了services.AddIdentity<User,Role>(),所以UserManager<IdentityUser>自然就没注册了。解决方法是全局搜索UserManager<IdentityUser>和,RoleManager<IdentityRole>,将其改成UserManager<User>和RoleManager<Role>就行了。
这时启动网站,访问登录页面,发现提示404。这也是由于我们修改了services.Addidentity<IdentityUser, IdentityRole>()导致的问题,解决方案是自己实现login和register。
页面这里就不写了,只提供控制器的login和register方法:
public class RegisterDto { public string UserName { get; set; } public string Password { get; set; } }
private readonly UserManager<MyUser> _userManager; private readonly SignInManager<MyUser> _signInManager; //管理登录登出的Manager类。系统自带。 private readonly RoleManager<MyRole> _roleManager; public AccountController(UserManager<MyUser> userManager, SignInManager<MyUser> signInManager, RoleManager<MyRole> roleManager) { _userManager = userManager; _signInManager = signInManager; _roleManager = roleManager; }
[HttpPost] public async Task<IActionResult> Login(RegisterDto dto) { var result = await _signInManager.PasswordSignInAsync(dto.UserName, dto.Password, true, false); if (result.Succeeded) { return RedirectToAction("Index", "Home"); } return Content("login fail"); }
[HttpPost] public async Task<IActionResult> Register(RegisterDto dto) { var user = new MyUser() { UserName = dto.UserName, }; var result = await _userManager.CreateAsync(user, dto.Password); if (result.Succeeded) { await _signInManager.SignInAsync(user, isPersistent: true); // await _userManager.AddToRoleAsync(user, "Member"); return Content("true"); } return Content("false"); }
public async Task<IActionResult> Logout() { await _signInManager.SignOutAsync(); return RedirectToAction("Index","Home"); }
完成后访问相应方法即可完成登陆等功能。
关于Startup中的配置,一些常用的就在这里说一下:
配置相关文档:https://docs.microsoft.com/en-us/aspnet/core/security/authentication/identity-configuration
配置密码校验:
services.Configure<IdentityOptions>(options => { options.Password. ... = ...; //密码相关 options.User.AllowedUserNameCharacters = null; //允许用户名使用任意字符。不配置则默认只允许英文字母及数字 });
配置重定向地址:
services.ConfigureApplicationCookie(opt => { opt.AccessDeniedPath = "/denied"; //配置鉴权失败后跳转的地址 opt.LoginPath = "/login"; //配置未登录情况下访问需要授权的接口后跳转的地址 });
附送一个演示项目:
https://github.com/axel10/auth_demo