Identity – Without Identity Framework

前言

上一回研究 Authenticate 和 Authorization 已经是 2 年前了.

业务需求一直没有增长, 所以也没有再去提升它了. 但最近业务开始上去了. 荒废的功夫又得拾起来了.

上一回我没有耐心写完 IdentityServer4 和 Angular. 这回希望能写完它. 不打算修改之前的了. 从头开始吧.

这 2 年微软的文档做的越来越好了. 这里就按着文档做一遍 warm up + summary 就好呗.

这个系列会涉及到的技术是: 

Razor Pages,

Web API,

Identity,

OpenIDDict Core (thrid party for OIDC + OAuth, 我没有使用 IdentityServer4 了),

Angular

 

介绍

Authentication (authen) 主要讲的就是登入, 身份验证,

Authorization (autho) 讲的是权限, 登入不表示拥有所有权限. 可以算是 2nd level protection.

用 ASP.NET Core 做 authen 一般上都会用 Identity Framework 插件 (build-in 的), 然后在插件上做一些魔改. 比较少从 0 开始写,

但其实 ASP.NET Core 底层是有一些基础功能的, 它可以让我们从 0 开始写. 这篇主要就是讲这一块.

 

主要参考

参考: Use cookie authentication without ASP.NET Core Identity

 

Protect Page

新建一个项目

dotnet new webapp -o WithoutIdentity

在 privacy page 加上 [Authorize] 标签.

Privacy.cshtml.cs

[Authorize]
public class PrivacyModel : PageModel
{
    public void OnGet()
    {
    }
}

这个时候, 访问这个页面的话就会报错了, 因为我们还没有做任何 authen 的配置.

 

Setup Config

现在去 Program.cs, 添加配置

builder.Services
    .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme);

Schemes 是方案的意思, 比如 Cookies Schemes, JWT Schemes 等等

CookieAuthenticationDefaults.AuthenticationScheme 是一个 string, 就是方案的名字. AddAuthentication 声明默认使用什么方案.

AddCookie 则是 ASP.NET Core 封装好的一个 Cookie 方案. 这也是最常见的方案, 参数是方案的名字.

另外, 还有一个常见的方案是 JWT (JSON Web Token), 采用前后端分离架构, 通常就会使用这个方案.

JWT 需要安装

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

配置

builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme)
    .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme);

services 配置好后就到 middleware.

添加一个 app.UseAuthentication()

注: 位置要对哦.

这时再访问 private page 出现的 error 就不一样了. 它会 redirect 到 Account/Login page.

这个是 default login page 的路径.

参考: CookieAuthenticationDefaults.cs

我们可以通过 options 来修改掉 login path, 还有其它 Cookie 相关的配置.

.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options => {
    options.Cookie.Name = "MyCookieName";
    options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Strict;
    options.ReturnUrlParameter = "returnUrl";
    options.LoginPath = "/login";
    options.LogoutPath = "/logout";
    options.AccessDeniedPath = "/access-denied";
});

注: SameSiteMode 默认是 Lax, Lax 级别已经足够我们做 OAuth 了. 所以不需要 set 成 Strict (我只是演示而已)

相关参考:

CSRF

Cookie Policy Middleware

Work with SameSite cookies in ASP.NET Core

 

Login Page

接下来, 我们做一个 login page.

当 user 被 redirect 到 login page 时, URL query 会附上一个 returnUrl, 它记入 user 是从哪一个路径被 redirect 过来的. 当登入成功, 我们可以让 user redirect 回去.

在这个 login page 做一个 login button post

public async Task OnPostAsync([FromQuery] string returnUrl)
{
    var claimsIdentity = new ClaimsIdentity(new List<Claim> { 
        new Claim(ClaimTypes.Name, "UserName")
    }, CookieAuthenticationDefaults.AuthenticationScheme);

    await HttpContext.SignInAsync(
        CookieAuthenticationDefaults.AuthenticationScheme, 
        new ClaimsPrincipal(claimsIdentity),
        new AuthenticationProperties()
    );
}

HttpContext.SignInAsync 是 ASP.NET Core 封装好的方法. 

参数 1 是方案的名字,

参数 2 是 Principal, 里面的 claims 是用来做 authorization 的 (最少都要有一个 Name 的 claims, 而已必须是 unique)

参数 3 是 authentication properties 比如 remenber me 之类的配置. 

我们不需要做额外写逻辑去 redirect, SignInAsync 会帮我们 redirect to returnUrl

完成后就成功进入 privacy page 了, 上图是 login 后 response cookie, 它的 value 是用 data protection 加密过的哦.

 

Logout Page

接着做一个 logout page

public async Task OnPostAsync()
{
    await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
}

然后在任何想 logout 的 page 里面写一个 form, post 去 logout page, 并且附上 returnUrl

<form asp-page="/Account/Logout/Index" asp-route-returnUrl="/" method="post">
    <button type="submit">Click here to Logout</button>
</form>

这里有一篇对 logout path 的解释: What does the CookieAuthenticationOptions.LogoutPath property do in ASP.NET Core 2.1?

我本来以为 logout path 的意思是你 logout 完成后它会跳转到这个 page

其实不是这样的. 它有点像 login page 是用来做 logout 的. 当调用 SignOutAsync 时, 当前的 path 必须是 options.LogoutPath (我们在 AddCookie 时写进去的) 那么才会触发 redirect

参考一下 Identity Framework 的实现, 这个是 logout button 可以放在任何地方. 它会 post to /Account/Logout page 附上了 returnUrl.

然后 /Account/Logout.cs 是这样

估计 SignInManger.SignOutAsync 里头就调用了 HttpContext.SignOutAsync.

 

React to back-end changes

这个概念蛮重要的, 之前我也是没有搞明白. 

首先要知道, cookie 保存了 user info, 通过 data protection 对称加密来保护.  

在验证的时候, 直接解密成功就登入了. 这个流程是不走数据库的. 

好处是快, 坏处就是无法即刻注销这个 cookie. 比如说用户换了密码, 他的预想是之前的 cookie 应该就失效了. 但是没有. 

那怎么办呢? 微软把这个权衡留给我们决定。

Identity Framework 对此的方案叫 Security Stamp, 它的做法是. 设定一个检测时期, 比如默认是超过 30 分钟就得去数据库检查一次.

检查什么呢? 就是 security stamp 是否和之前一样. 如果一样就表示这段期间用户并没有做出任何会影响到授权的事. 如果有那么就应该要 update 这个 security stamp. 那么期限一到, 检查的时候就会发现.

Identity Framework 检查的时候会跑 5 个 select from table, 性能还是蛮伤的呢. 

可以通过 config 去 set 检测的间隔时间

services.Configure<SecurityStampValidatorOptions>(o => {
    o.ValidationInterval = TimeSpan.FromSeconds(60);
});

说太多 Identity Framework 了, 讲回 without Identity Framework 的情况下, 它是怎样 work 的

首先是做 principal 的时候放多一个 security stamp claim

然后在 AddCookie 的时候放入一个 Events 拦截

继承 CookieAuthenticationEvents 然后 override 掉 ValidatePrincipal, 里头就可以检查然后 reject 之类的

或者像 identity 这样配置也是可以的

具体 validation 是这样的

判断时间 > 数据库检查 > 不通过就 signout > 通过就 replace principal, replace principal 不是 identity 的功能, 而是 ASP.NET Core 的功能.

 

Cookie Expires

refer: How to set asp.net Identity cookies expires time

有几个东西跟 expires 息息相关 

SlidingExpiration = 这个是说 keep renew cookie, 每次 request 来的时候如果 cookie 是 ok 的 then 就续命.

Persistent = 如果是 false 的话, 那么就是没有 remember me, cookie expires 是 session, 如果是 true 那么 expires 就 base on cookie option 的 ExpireTimeSpan (默认 14 天)

在 SignIn 的时候还可以调绝对过期 ExpiresUtc 它会覆盖掉 option 的 ExpireTimeSpan, 也不会理会 SlidingExpiration 了 (set 绝对的时候要 Persistent true 哦, 因为 false 就是跑 session)

Cookie expires session 就是说当 browser close 它就被清掉. 需要留意的是 chrome, 一但你 set 了 continue where you left off 它就不会清除掉 expires session cookie 了. 即使是你 off pc 也不会...

posted @ 2021-02-14 22:18  兴杰  阅读(495)  评论(0编辑  收藏  举报