最近把一个Asp .net core 2.0的项目迁移到Asp .net core 3.1,项目启动的时候直接报错:

InvalidOperationException: Endpoint CoreAuthorization.Controllers.HomeController.Index (CoreAuthorization) contains authorization metadata, but a middleware was not found that supports authorization.
Configure your application startup by adding app.UseAuthorization() inside the call to Configure(..) in the application startup code. The call to app.UseAuthorization() must appear between app.UseRouting() and app.UseEndpoints(...).
Microsoft.AspNetCore.Routing.EndpointMiddleware.ThrowMissingAuthMiddlewareException(Endpoint endpoint)

看意思是缺少了一个authorization的中间件,这个项目在Asp.net core 2.0上是没问题的。

startup是这样注册的:

public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
} public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(options =>
{
options.LoginPath = "/account/Login";
}); services.AddControllersWithViews();
} // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
//app.UseHttpsRedirection();
app.UseStaticFiles(); app.UseRouting(); app.UseAuthentication(); app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}

查了文档后发现3.0的示例代码多了一个UseAuthorization,改成这样就可以了:

 app.UseRouting();
app.UseAuthentication();
//use授权中间件
app.UseAuthorization(); app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});

看来Asp .net Core 3.1的认证跟授权又不太一样了,只能继续看文档学习了。

UseAuthentication and UseAuthorization

先说一下Authentication跟Authorization的区别。这两个单词长的十分相似,而且还经常一起出现,很多时候容易搞混了。

  1. Authentication是认证,明确是你谁,确认是不是合法用户。常用的认证方式有用户名密码认证。
  2. Authorization是授权,明确你是否有某个权限。当用户需要使用某个功能的时候,系统需要校验用户是否需要这个功能的权限。

    所以这两个单词是不同的概念,不同层次的东西。UseAuthorization在asp.net core 2.0中是没有的。在3.0之后微软明确的把授权功能提取到了Authorization中间件里,所以我们需要在UseAuthentication之后再次UseAuthorization。否则,当你使用授权功能比如使用[Authorize]属性的时候系统就会报错。

Authentication(认证)

认证的方案有很多,最常用的就是用户名密码认证,下面演示下基于用户名密码的认证。新建一个MVC项目,添加AccountController:

        [HttpPost]
public async Task<IActionResult> Login(
[FromForm]string userName, [FromForm]string password
)
{
//validate username password
...
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, userName),
new Claim(ClaimTypes.Role, "老师")
}; var claimsIdentity = new ClaimsIdentity(
claims, CookieAuthenticationDefaults.AuthenticationScheme); await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity)); return Redirect("/");
}
public async Task<IActionResult> Logoff()
{
await HttpContext.SignOutAsync(); return Redirect("Login");
} public IActionResult AccessDenied()
{
return Content("AccessDenied");
}

修改login.cshtml

@{
ViewData["Title"] = "Login Page";
} <h1>
Login Page
</h1> <form method="post">
<p>
用户名: <input name="userName" value="administrator" />
</p>
<p>
密码: <input name="password" value="123" />
</p> <p>
<button>登录</button>
</p>
</form>

从前台传入用户名密码后进行用户名密码校验(示例代码省略了密码校验)。如果合法,则把用户的基本信息存到一个claim list里,并且指定cookie-base的认证存储方案。最后调用SignInAsync把认证信息写到cookie中。根据cookie的特性,接来下所有的http请求都会携带cookie,所以系统可以对接来下用户发起的所有请求进行认证校验。Claim有很多翻译,个人觉得叫“声明”比较好。一单认证成功,用户的认证信息里就会携带一串Claim,其实就是用户的一些信息,你可以存任何你觉得跟用户相关的东西,比如用户名,角色等,当然是常用的信息,不常用的信息建议在需要的时候查库。调用HttpContext.SignOutAsync()方法清除用户登认证信息。

Claims信息我们可以方便的获取到:

@{
ViewData["Title"] = "Home Page";
} <h2>
CoreAuthorization
</h2> <p>
@Context.User.FindFirst(System.Security.Claims.ClaimTypes.Name)?.Value
</p>
<p>
角色:
@foreach (var claims in Context.User.Claims.Where(c => c.Type == System.Security.Claims.ClaimTypes.Role))
{
<span> @claims.Value </span>
}
</p>
<p>
<a href="/Student/index">/Student/index</a>
</p>
<p>
<a href="/Teacher/index">/Teacher/Index</a>
</p>
<p>
<a href="/Teacher/Edit">/Student/Edit</a>
</p> <p>
<a href="/Account/Logoff">退出</a>
</p>

改一下home/Index页面的html,把这些claim信息展示出来。



以上就是一个基于用户名密码以及cookie的认证方案。

Authorization(授权)

有了认证我们还需要授权。刚才我们实现了用户名密码登录认证,但是系统还是没有任何管控,用户可以随意查库任意页面。现实中的系统往往都是某些页面可以随意查看,有些页面则需要认证授权后才可以访问。

AuthorizeAttribute

当我们希望一个页面只有认证后才可以访问,我们可以在相应的Controller或者Action上打上AuthorizeAttribute这个属性。修改HomeController:

    [Authorize]
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
} }

重新启动网站,如果没有登录,访问home/index的时候网站会跳转到/account/AccessDenied。如果登录后则可以正常访问。AuthorizeAttribute默认授权校验其实是把认证跟授权合为一体了,只要认证过,就认为有授权,这是也是最最简单的授权模式。

基于角色的授权策略

显然上面默认的授权并不能满足我们开发系统的需要。AuthorizeAttribute还内置了基于Role(角色)的授权策略。

登录的时候给认证信息加上角色的声明:

  [HttpPost]
public async Task<IActionResult> Login(
[FromForm]string userName,
[FromForm]string password
)
{
//validate username password var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, userName),
new Claim(ClaimTypes.Role, "老师"),
}; var claimsIdentity = new ClaimsIdentity(
claims, CookieAuthenticationDefaults.AuthenticationScheme); await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity)); return Redirect("/");
}

新建一个TeacherController:

    [Authorize(Roles = "老师")]
public class TeacherController : Controller
{
public IActionResult Index()
{
return Content("Teacher index");
}
}

给AuthorizeAttribute的属性设置Roles=老师,表示只有老师角色的用户才可以访问。如果某个功能可以给多个角色访问那么可以给Roles设置多个角色,使用逗号进行分割。

  [Authorize(Roles = "老师,校长")]
public class TeacherController : Controller
{
public IActionResult Index()
{
return Content("Teacher index");
} }

这样认证的用户只要具有老师或者校长其中一个角色就可以访问。

基于策略的授权

上面介绍了内置的基于角色的授权策略。如果现实中需要更复杂的授权方案,我们还可以自定义策略来支持。比如我们下面定义一个策略:编辑功能只能姓王的老师可以访问。

定义一个要求:

 public class LastNamRequirement : IAuthorizationRequirement
{
public string LastName { get; set; }
}

IAuthorizationRequirement其实是一个空接口,仅仅用来标记,继承这个接口就是一个要求。这是空接口,所以要求的定义比较宽松,想怎么定义都可以,一般都是根据具体的需求设置一些属性。比如上面的需求,本质上是根据老师的姓来决定是否授权通过,所以把姓作为一个属性暴露出去,以便可以配置不同的姓。

除了要求,我们还需要实现一个AuthorizationHandler:

 public class LastNameHandler : AuthorizationHandler<IAuthorizationRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IAuthorizationRequirement requirement)
{
var lastNameRequirement = requirement as LastNamRequirement;
if (lastNameRequirement == null)
{
return Task.CompletedTask;
} var isTeacher = context.User.HasClaim((c) =>
{
return c.Type == System.Security.Claims.ClaimTypes.Role && c.Value == "老师";
});
var isWang = context.User.HasClaim((c) =>
{
return c.Type == "LastName" && c.Value == lastNameRequirement.LastName;
}); if (isTeacher && isWang)
{
context.Succeed(requirement);
} return Task.CompletedTask;
}
}

AuthorizationHandler是一个抽象类,继承它后需要重写其中的HandleRequirementAsync方法。这里才是真正判断是否授权成功的地方。要求(Requirement)跟用户的声明(Claim)信息会被传到这方法里,然后我们根据这些信息进行判断,如果符合授权就调用context.Succeed方法。这里注意如果不符合请谨慎调用context.Failed方法,因为策略之间一般是OR的关系,这个策略不通过,可能有其他策略通过

在ConfigureServices方法中添加策略跟注册AuthorizationHandler到DI容器中:

services.AddSingleton<IAuthorizationHandler, LastNameHandler>();
services.AddAuthorization(options =>
{
options.AddPolicy("王老师", policy =>
policy.AddRequirements(new LastNamRequirement { LastName = "王" })
);
});

使用AddSingleton生命周期来注册LastNameHandler,这个生命周期并不一定要单例,看情况而定。在AddAuthorization中添加一个策略叫"王老师"。这里有个个人认为比较怪的地方,为什么AuthorizationHandler不是在AddAuthorization方法中配置?而是仅仅注册到容器中就可以开始工作了。如果有一个需求,仅仅是需要自己调用一下自定义的AuthorizationHandler,而并不想它真正参与授权。这样的话就不能使用DI的方式来获取实例了,因为一注册进去就会参与授权的校验了。

在TeacherController下添加一个 Edit Action:

  [Authorize(Policy="王老师")]
public IActionResult Edit()
{
return Content("Edit success");
}

给AuthorizeAttribute的Policy设置为“王老师”。

修改Login方法添加一个姓的声明:

  [HttpPost]
public async Task<IActionResult> Login(
[FromForm]string userName,
[FromForm]string password
)
{
//validate username password var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, userName),
new Claim(ClaimTypes.Role, "老师"),
new Claim("LastName", "王"),
}; var claimsIdentity = new ClaimsIdentity(
claims, CookieAuthenticationDefaults.AuthenticationScheme); await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity)); return Redirect("/");
}

运行一下程序,访问一下/teacher/edit,可以看到访问成功了。如果修改Login方法,修改LastName的声明为其他值,则访问会拒绝。

使用泛型Func方法配置策略

如果你的策略比较简单,其实还有个更简单的方法来配置,就是在AddAuthorization方法内直接使用一个Func来配置策略。

使用Func来配置一个女老师的策略:

 options.AddPolicy("女老师", policy =>
policy.RequireAssertion((context) =>
{
var isTeacher = context.User.HasClaim((c) =>
{
return c.Type == System.Security.Claims.ClaimTypes.Role && c.Value == "老师";
});
var isFemale = context.User.HasClaim((c) =>
{
return c.Type == "Sex" && c.Value == "女";
}); return isTeacher && isFemale;
}
)
);

总结

  1. Authentication跟Authorization是两个不同的概念。Authentication是指认证,认证用户的身份;Authorization是授权,判断是否有某个功能的权限。
  2. Authorization内置了基于角色的授权策略。
  3. 可以使用自定义AuthorizationHandler跟Func的方式来实现自定义策略。

吐槽

关于认证跟授权微软为我们考虑了很多很多,包括identityserver,基本上能想到的都有了,什么oauth,openid,jwt等等。其实本人是不太喜欢用的。虽然微软都给你写好了,考虑很周到,但是学习跟Trouble shooting都是要成本的。其实使用中间件、过滤器再配合redis等组件,很容易自己实现一套授权认证方案,自由度也更高,有问题修起来也更快。自己实现一下也可以更深入的了解某项的技术,比如jwt是如果工作的,oauth是如何工作的,这样其实更有意义。

ASP.NET Core Authentication and Authorization的更多相关文章

  1. ASP.NET Core Authentication系列(二)实现认证、登录和注销

    前言 在上一篇文章介绍ASP.NET Core Authentication的三个重要概念,分别是Claim, ClaimsIdentity, ClaimsPrincipal,以及claims-bas ...

  2. ASP.NET Core Authentication系列(四)基于Cookie实现多应用间单点登录(SSO)

    前言 本系列前三篇文章分别从ASP.NET Core认证的三个重要概念,到如何实现最简单的登录.注销和认证,再到如何配置Cookie 选项,来介绍如何使用ASP.NET Core认证.感兴趣的可以了解 ...

  3. ASP.NET Core Authentication系列(一)理解Claim, ClaimsIdentity, ClaimsPrincipal

    前言 首先我们来看一下在ASP.NET时代,Authentication是如何使用的.下面介绍的是System.Web.Security.FormsAuthentication: // 登录 Syst ...

  4. Asp.Net Core Authentication Middleware And Generate Token

    .mytitle { background: #2B6695; color: white; font-family: "微软雅黑", "宋体", "黑 ...

  5. ASP.NET Core Authentication系列(三)Cookie选项

    前言 在本系列第一篇文章介绍了ASP.NET时代如何认证,并且介绍了如何通过web.config文件来配置Auth Cookie的选项. 第二篇文章介绍了如何使用Cookie认证,本文介绍几个常见的C ...

  6. ASP.NET Core 中基于 API Key 对私有 Web API 进行保护

    这两天遇到一个应用场景,需要对内网调用的部分 web api 进行安全保护,只允许请求头账户包含指定 key 的客户端进行调用.在网上找到一篇英文博文 ASP.NET Core - Protect y ...

  7. 使用 NLog 给 Asp.Net Core 做请求监控

    为了减少由于单个请求挂掉而拖垮整站的情况发生,给所有请求做统计是一个不错的解决方法,通过观察哪些请求的耗时比较长,我们就可以找到对应的接口.代码.数据表,做有针对性的优化可以提高效率.在 asp.ne ...

  8. [转]教你实践ASP.NET Core Authorization

    本文转自:http://www.cnblogs.com/rohelm/p/Authorization.html 本文目录 Asp.net Core 对于授权的改动很友好,非常的灵活,本文以MVC为主, ...

  9. 教你实践ASP.NET Core Authorization

    本文目录 Asp.net Core 对于授权的改动很友好,非常的灵活,本文以MVC为主,当然如果说webapi或者其他的分布式解决方案授权,也容易就可以实现单点登录都非常的简单,可以使用现成的Iden ...

随机推荐

  1. 前端每日实战:52# 视频演示如何用纯 CSS 创作一个小球绕着圆环盘旋的动画

    效果预览 按下右侧的"点击预览"按钮可以在当前页面预览,点击链接可以全屏预览. https://codepen.io/comehope/pen/gKxyWo 可交互视频 此视频是可 ...

  2. Ubuntu16.04下安装python3.6.4详细步骤

    记录一下: Ubuntu16.04自带的python版本为python2.7和python3.5,现在想要安装python3.6.4,注意:系统自带的python版本别删除 步骤: # 官网下载安装包 ...

  3. .netCore下的jwt的梳理-->借鉴于“老张的哲学”

    之前在公司的项目中有用到jwt进行token验证,但是公司里用的框架已经集成好了jwt,所以对jwt的的了解不够清晰,感觉还是隔着一层.在看了“老张的哲学”的jwt部分后对jwt的认识才更加深刻了一些 ...

  4. .Net Core 为 x86 和 x64 程序集编写 AnyCPU 包装

    前言 这几天研究了一下 vJoy 这个虚拟游戏手柄驱动,感觉挺好玩的.但是使用时发现一个问题,C# SDK 中的程序集被分为 x86 和 x64 两个版本,如果直接在 AnyCPU 平台编译运行就有隐 ...

  5. Oracle批量插入有日期类型数据

    例如现在有张表 id(number) startTime(date) name(varchar2) 1 2017-08-13  zhangsan 2 2017-08-14  zhangsan 需要批量 ...

  6. 使用vue cli3新建一个vue项目

    写在最前 虽然身为一个java后端工作者,前端还是要沾点的,基于vue的火热,所以平常的工作中项目前端基本都是vue.这次就主要讲一讲vue项目的创建,并从vue的安装开始讲起,附带上我之前安装使用时 ...

  7. vue-element-admin 模板 登录页面 post请求通过django的csrf认证,处理304错误

    经过一天的研究,终于把 vue-admin-template 模板的 post 请求 和django的api 弄通了 没有了那该死的304报错了 直接贴代码: 在main.js中 我直接给设置了一个 ...

  8. WEB渗透 - SQL注入(持续更新)

    SQL注入 按变量类型分:数字型和字符型 按HTTP提交方式分:POST注入.GET注入和Cookie注入 按注入方式分:布尔注入.联合注入.多语句注入.报错注入.延时注入.内联注入 按数据库类型分: ...

  9. scrapydweb的初步使用(管理分布式爬虫)

    https://github.com/my8100/files/blob/master/scrapydweb/README_CN.md 一.安装配置 1.请先确保所有主机都已经安装和启动 Scrapy ...

  10. 【作业1.0】OO第一单元作业总结

    OO第一单元作业已全部完成,为了使这一单元的作业能够收获更多一点,我回忆起我曾经在计算机组成课设中,经常我们会写一些实验报告,经常以此对实验内容反思总结.在我们开始下一单元的作业之前,我在此对OO第一 ...