先纠正一下第一篇的的错误.

在 Login.cshtml 和 Login.cshtml.cs 里, 本来应该是 Register 我却写成 Login .

cshtml 修改部分

<form asp-page="Login" asp-page-handler="Register">
<input type="text" name="username" placeholder="username">
<input type="password" name="password" placeholder="password">
<button type="submit">Login</button>
</form>

cshtml.cs 修改部分

public class RegisterInputModel
{
public string username { get; set; }
public string password { get; set; }
} [BindProperty]
public RegisterInputModel RegisterData { get; set; } public async Task OnPostRegisterAsync(
[FromServices] UserManager<IdentityUser> userManager
)
{
var user = new IdentityUser
{
UserName = RegisterData.username
};
var reuslt = await userManager.CreateAsync(user, RegisterData.password);
if (reuslt.Succeeded)
{ }
}

继续上路.

上回说到 register 完成了, 一般上情况下,我们可以直接使用 SignInManager 让用户注册完成自动登入.

但我想来点麻烦的, 我们不直接给用户登入. 而是要求用户先 confirm email.

之前的 register 没有要求 username 必须是 email, 我们现在加上这个要求. (model 验证我就不写了, 读者自己要懂啊)

首先我们先在 startup.cs service 里添加 options

services.Configure<IdentityOptions>(options =>
{
options.Password.RequireDigit = false;
options.Password.RequireLowercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.Password.RequiredLength = ;
options.Password.RequiredUniqueChars = ; options.User.AllowedUserNameCharacters = null; // 默认是 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+"; null 表示啥都行
options.User.RequireUniqueEmail = false; options.SignIn.RequireConfirmedEmail = true; // 加这个
options.SignIn.RequireConfirmedPhoneNumber = false;
});

一旦设置了 RequireConfirmedEmail, 如果 user 没有 confirm 过, 那么 sign in 的时候就会 error

RequireConfirmedPhoneNumber 和 email 同样原理

有一种情况我们可以考虑进去,就是当 phone = null or "" 的时候, 我们可以直接设置 phone confirm 为 true.

当以后 phone 有值了在设置 phone confirm 为 false 然后发出 token 验证. 这些就看项目需求做变化了.

那我这里给的项目需求是 email 要 confirm, phone 不管.

首先在 register 的时候填入 email

然后 generate token

然后一个 link url

然后发 confirmation email 给用户

public async Task OnPostRegisterAsync(
[FromServices] UserManager<IdentityUser> userManager
)
{
var user = new IdentityUser
{
UserName = RegisterData.username,
Email = RegisterData.username // 加入这个
};
var reuslt = await userManager.CreateAsync(user, RegisterData.password);
if (reuslt.Succeeded)
{
var token = await userManager.GenerateEmailConfirmationTokenAsync(user);
var callbackUrl = Url.Page(
"ConfirmEmail",
pageHandler: null,
values: new { userId = user.Id, token },
protocol: Request.Scheme
);
var subject = "Confirm your email";
var htmlMessage = $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.";
var sendTo = RegisterData.username;
// EmailService.send(sendTo, subject, htmlMessage);
}
}

email service 我就不写具体实现了. 如果你不熟悉可以看看这个 https://www.cnblogs.com/keatkeat/p/7576748.html

token 是一个很长很长的字符串, 1 小时过期,  基本上是不可能暴力破解的.

现在呢,我们去写一个 confirm email razor page 来接收吧.

public class ConfirmEmailModel : PageModel
{
public async Task OnGetAsync(
string token, string userId,
[FromServices] UserManager<IdentityUser> userManager
)
{
var user = await userManager.FindByIdAsync(userId);
var result = await userManager.ConfirmEmailAsync(user, token);
if (result.Succeeded)
{ }
}
}

我们直接让 register 跳转到 confirm 页面

        public async Task<IActionResult> OnPostRegisterAsync(
[FromServices] UserManager<IdentityUser> userManager
)
{
var user = new IdentityUser
{
UserName = RegisterData.username,
Email = RegisterData.username // 加入这个
};
var reuslt = await userManager.CreateAsync(user, RegisterData.password);
if (reuslt.Succeeded)
{
var token = await userManager.GenerateEmailConfirmationTokenAsync(user);
var callbackUrl = Url.Page(
"ConfirmEmail",
pageHandler: null,
values: new { userId = user.Id, token },
protocol: Request.Scheme
);
var subject = "Confirm your email";
var htmlMessage = $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.";
var sendTo = RegisterData.username;
// EmailService.send(sendTo, subject, htmlMessage); return RedirectToPage("ConfirmEmail", new { userId = user.Id, token });
}
return Page();
}

这样 confirm email 就搞定了。

当然真实情况下并不会那么顺利. 我们来找点麻烦.

1.用户不 confirm 但强行登入怎么办 ?

当然是挡丫. identity 有替我们照顾这一点, 上面有提到 signIn 的时候会有 error, 我们来试试吧.

添加一个 login form 在 Login.cshtml

Login form
<form asp-page="Login" asp-page-handler="Login">
<input type="text" name="username" placeholder="username">
<input type="password" name="password" placeholder="password">
<button type="submit">Login</button>
</form>

添加一个 handler 在 Login.cshtml.cs

public class LoginInputModel
{
public string username { get; set; }
public string password { get; set; }
} [BindProperty]
public LoginInputModel LoginData { get; set; } public async Task OnPostLoginAsync(
[FromServices] UserManager<IdentityUser> userManager,
[FromServices] SignInManager<IdentityUser> signInManager
)
{
var result = await signInManager.PasswordSignInAsync(LoginData.username, LoginData.password, lockoutOnFailure: true, isPersistent: true);
if (result.IsNotAllowed)
{
// 报错了, 因为 email 或 phone 还没有 confirm
}
}

这里有一点要留意, 就是 SignInManager, 这个必须在 startup.cs 里 service 添加

services.AddIdentityCore<IdentityUser>(o =>
{
o.Stores.MaxLengthForKeys = ;
})
.AddDefaultTokenProviders()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddSignInManager(); // 加这个
//.AddPasswordValidator<MyPasswordValidator>()
//.AddUserValidator<MyUserValidator>();

如果使用 AddDefaultIdentity 它是会在 AddDefaultUI 时帮我们添加 AddSignInManager, 但第一篇时我们移除了 AddDefaultUI 所以必须自己添加了。

2.用户收不到 email 怎么办 ?

再发丫...哈哈哈

那我们继续来看看 phone confirm 是怎样做的.

和 email 区别挺多的, 首先是它并没有 GeneratePhoneNumberConfirmationTokenAsync 方法, 也没有 ConfirmPhoneNumberAsync 方法.

取而代之的是 GenerateChangePhoneNumberTokenAsync 和 ChangePhoneNumberAsync 方法

要知道 email 也是有这 2 个方法的哦 GenerateChangeEmailTokenAsync 和 ChangeEmailAsync.

奇葩吧...

还有一个区别就是 token, email 的 token 是长字符串.

phone token 自然就无法用长的啦,因为用户是看着手机输入,而不像 email 是通过一个 url 链接.

phone token 只有 6 个号码.

这就诞生了一个危险. 就是暴力破解啦.

先来段代码看看

添加一个 phone input

Register form
<form asp-page="Login" asp-page-handler="Register">
<input type="text" name="username" placeholder="username">
<input type="text" name="phone" placeholder="phone">
<input type="password" name="password" placeholder="password">
<button type="submit">Login</button>
</form>

.cs

public async Task<IActionResult> OnPostRegisterAsync(
[FromServices] UserManager<IdentityUser> userManager
)
{
var user = new IdentityUser
{
UserName = RegisterData.username,
Email = RegisterData.username,
PhoneNumber = RegisterData.phone // 加入这个
};
var reuslt = await userManager.CreateAsync(user, RegisterData.password);
if (reuslt.Succeeded)
{
var token = await userManager.GenerateEmailConfirmationTokenAsync(user);
var callbackUrl = Url.Page(
"ConfirmEmail",
pageHandler: null,
values: new { userId = user.Id, token },
protocol: Request.Scheme
);
var subject = "Confirm your email";
var message = $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.";
var sendTo = RegisterData.username; // EmailService.send(sendTo, subject, message);
// return RedirectToPage("ConfirmEmail", new { userId = user.Id, token }); var phoneToken = await userManager.GenerateChangePhoneNumberTokenAsync(user, user.PhoneNumber);
subject = "Confirm your account";
sendTo = RegisterData.phone;
message = $"please confirm your account by enter numbers : {phoneToken}";
// SMSService.send(sendTo, subject, message);
for (var i = Convert.ToInt32(phoneToken) - ; i < ; i++) // 尝试 3000 次就好了
{
var phoneResult = await userManager.ChangePhoneNumberAsync(user, user.PhoneNumber, i.ToString());
if (phoneResult.Succeeded)
{
//暴力破解成功
}
} }
return Page();
}

看的出来, identity 并没有帮我们保护暴力破解. 我们先不要管它, 因为 SignIn 的时候也有暴力破解的问题, identity 是否有保护那边呢 ?

如果你眼睛利,刚才应该已经看见了

var result = await signInManager.PasswordSignInAsync(LoginData.username, LoginData.password, lockoutOnFailure: true, isPersistent: true);

这里有个叫 lockoutOnFailure 的东西.

这个就是防止暴力破解的冬冬啦。 identity 的做法是这样的, 用户登入密码错误, 记入错误的次数在数据库里, 用户继续试错... 一旦到了试错次数上限, 账户被锁上一个时辰.

在这个时辰内,不管密码对还是不对,一律返回账号被封锁信息。 这就是它防止暴力破解的方式了.

在 startup.cs 的 service 里加入配置

services.Configure<IdentityOptions>(options =>
{
options.Password.RequireDigit = false;
options.Password.RequireLowercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.Password.RequiredLength = ;
options.Password.RequiredUniqueChars = ; options.User.AllowedUserNameCharacters = null; // 默认是 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+"; null 表示啥都行
options.User.RequireUniqueEmail = false; options.SignIn.RequireConfirmedEmail = true;
options.SignIn.RequireConfirmedPhoneNumber = false; // 加入这些
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromSeconds(); // 锁 20 秒
options.Lockout.MaxFailedAccessAttempts = ; // 错误次数上限 = 2 次
options.Lockout.AllowedForNewUsers = true; // 每一个新账户都开启这个功能
});

被锁后会返回 error

var result = await signInManager.PasswordSignInAsync(LoginData.username, LoginData.password, lockoutOnFailure: true, isPersistent: true);
if (result.IsLockedOut)
{
var user = await userManager.FindByNameAsync(LoginData.username);
DateTimeOffset? datetime = await userManager.GetLockoutEndDateAsync(user);
}

我们可以通过 GetLockoutEndDateAsync 查看封锁到几点可以在尝试.

好,话说回来,刚才的 phone confirm token 我们是否需要自己做一个防止暴力破解的方案呢?.

我想 identity 不提供给我们也是有原因的, 因为 phone token 是基于 Time-based One-time Password (TOTP)

如果你不熟悉可以看这里 : http://www.ruanyifeng.com/blog/2017/11/2fa-tutorial.html

一般上 30 秒就失效了, 所以如果对方真的要暴力. 30 秒内得发 90 万个请求试错. 即便是一半 45 万次, 服务器应该也挂掉了...哈哈哈

但是 identity 默认并不是 30 秒.. 而是 9 分钟. 所以还是有点危险得. 在源码中可以看到

也有人提出了 issue 希望可以通过 options 来配置. 因为 Google Authenticator App 也是 30 秒.

本来呢,想自己写一个类似 lockoutOnFailure 方案来防爆, 但是感觉未来 identity 会开放这个功能出来, 干脆就等等吧.

https://github.com/aspnet/AspNetIdentity/issues/15 options for adjust timestep

https://github.com/aspnet/AspNetCore/issues/5811 开放 Rfc6238AuthenticationService

identity 一共给了 4 个 token provider

emailTokenProviderType 没有找到任何地方在用, 所以暂时忽略掉它.

在源码 TokenOptions.cs 里面可以看到所有使用的地方

EmailConfirmationTokenProvider = DefaultProvider = dataProtectionProviderType

PasswordResetTokenProvider = DefaultProvider = dataProtectionProviderType

ChangeEmailTokenProvider = DefaultProvider = dataProtectionProviderType

ChangePhoneNumberTokenProvider = DefaultPhoneProvider = phoneNumberProviderType

AuthenticatorTokenProvider = DefaultAuthenticatorProvider = authenticatorProviderType

authenticatorProviderType 和 phoneNumberProviderType 都用到了 TOTP

dataProtectionProviderType 用的是一般的 dataProtection 加密,  如果你不了解 data protection 可以看这里 : https://www.cnblogs.com/keatkeat/p/9316389.html

所以主要分 2 种 token, 一种是长的,默认过期 1 小时(可以调), 短的就是 TOTP 默认 9 分钟, 不可以调.

1.confirm email, confirm phone, change email, change phone, reset password

这些情况下都需要把 token 发到 email 或 手机上. 显然 identity 的 reset password 用的是长 token 不利于手机.

这个意思就是说歧视手机用户啦.. 如果用户就只用手机注册不可以吗 ? 手机注册就不可以 reset password 吗. 非要给 email ?

那如果项目需要实现有办法吗 ?

看看这个源码... 要实现的话,我们需要直接调用 GenerateUserTokenAsync 然后把 Options.Tokens.PasswordResetTokenProvider 换掉才可以了.

验证的时候也是一样.. 好多代码丫,这显然不太友好... 一般这种情况我就不会去动它了. 除非项目非要不可.

另外我想说说 two factor, 有 3 个重点

two factor 意义在于至少 2 个身份识别.

它是通过 token hardware 或者手机 + apps 来弄的 (不是手机 sms 哦)

在处理 reset password 时要额外小心, 2 个识别下, 如果弄丢了一个,我们不可以用另外一个来恢复哦,必须找到第 3 个识别才是安全的。

这里就不做实现了,  因为上面的 TOTP issue 嘛.

说说实现方式就好. 首先是要有一个 token hardware, 基本上是用手机 + google app 来做啦.

参考

https://docs.microsoft.com/en-us/aspnet/core/security/authentication/identity-enable-qrcodes?view=aspnetcore-2.2

https://medium.freecodecamp.org/how-to-set-up-two-factor-authentication-on-asp-net-core-using-google-authenticator-4b15d0698ec9

用 asp.net core + js 做一个 qrcode, 用户用 google app 来扫描. 这个是第一次的密钥, 然后就可以用 google app 来生成 6 digit 了

当请求验证时, asp.net core 用同样的密钥生成 6 digit 来 match. 相同密钥 + 同个时间点 (standard 是不能差超过 30 秒)就可以匹配成功了.

最后来讲讲 token provider 的 override.

我们可以参考之前的图, AddDefaultTokenProviders 去创建 token provider 或者是继承 override 也行.

然后通过修改 TokenOptions 的匹配 provider 就可以实现 override 了.

目前我是没有看到什么好 override 的啦, 要弄就要弄大的,比如上面提到的 reset password by phone

虽然可以把所有 reset password 换成 by phone 但是,通常需求并不是要全部, 而是依据用户的选择. 这种 dynamic 的方式就不可能用这种 override 方式做到了.

最终还是要修改 userManger 实现才行. 算了吧 。以后在打算.

来说说 reset password, 这个非常简单

public async Task OnPostResetPassword(
[FromServices] UserManager<IdentityUser> userManager
)
{
var user = await userManager.FindByNameAsync("hengkeat87@gmail.com");
var token = await userManager.GeneratePasswordResetTokenAsync(user);
await userManager.ResetPasswordAsync(user, token, newPassword: "");
}

搞定.

在来说说 SignIn 的 cookie 配置.

services.ConfigureApplicationCookie(options =>
{
options.Cookie.HttpOnly = true;
options.Cookie.Name = "Identity";
options.ExpireTimeSpan = TimeSpan.FromMinutes();
options.SlidingExpiration = true;
options.LoginPath = "/Login";
options.LogoutPath = "/Logout";
options.AccessDeniedPath = "/AccessDenied";
options.ReturnUrlParameter = CookieAuthenticationDefaults.ReturnUrlParameter;
});

HttpOnly 是说 js 无法读取, 为了安全一定要的啊.

Cookie.Name 也是一定要的啦.

expiredTimespan 是 cookie 多久过期

SlidingExpiration 是自动更新延长过期, 就是如果用户一直有访问网站, cookie 就一直有效. 实现方式大概就是请求来的时候发现 cookie 要过期了就写入一个新的时间咯.

LoginPath 就是登入页面咯, 用户访问权限页面时如果没有登入会自动跳转到这里

LogoutPath 用户登出后自动跳转到这里

AccessDeniedPath 如果用户登入了但依然无权限访问的话会跳转到这里 (比如页面要求 role = admin, 但是用户没有 role admin)

ReturnUrlParameter 默认叫 returnUrl ... 从前我就想问为什么不叫 redirect url 呢 ? 很好,现在可以换掉.

总结 :

这篇主要讲的是

要求 email confirm 或 phone confirm

sign in 的暴力破解

还有 token provider 的设计和运用

最后是一些基本的 cookie 配置.

https://docs.microsoft.com/en-us/aspnet/core/security/authentication/identity-configuration?view=aspnetcore-2.2#tokens

Asp.net core Identity + identity server + angular 学习笔记 (第二篇)的更多相关文章

  1. Asp.net core Identity + identity server + angular 学习笔记 (第一篇)

    用了很长一段时间了, 但是一直没有做过任何笔记,感觉 identity 太多东西要写了, 提不起劲. 但是时间一久很多东西都记不清了. 还是写一轮吧. 加深记忆. 这是 0-1 的笔记, 会写好多篇. ...

  2. Asp.net core Identity + identity server + angular 学习笔记 (第三篇)

    register -> login 讲了 我们来讲讲 forgot password -> reset password  和 change password 吧 先来 forgot pa ...

  3. ASP.NET Core 学习笔记 第二篇 依赖注入

    前言 ASP.NET Core 应用在启动过程中会依赖各种组件提供服务,而这些组件会以接口的形式标准化,这些组件这就是我们所说的服务,ASP.NET Core框架建立在一个底层的依赖注入框架之上,它使 ...

  4. Asp.net core Identity + identity server + angular 学习笔记 (第五篇)

    ABAC (Attribute Based Access Control) 基于属性得权限管理. 属性就是 key and value 表达力非常得强. 我们可以用 key = role value ...

  5. Asp.net core Identity + identity server + angular 学习笔记 (第四篇)

    来说说 RBAC (role based access control) 这是目前全世界最通用的权限管理机制, 当然使用率高并不是说它最好. 它也有很多局限的. 我们来讲讲最简单的 role base ...

  6. Asp.Net Core + Dapper + Repository 模式 + TDD 学习笔记

    0x00 前言 之前一直使用的是 EF ,做了一个简单的小项目后发现 EF 的表现并不是很好,就比如联表查询,因为现在的 EF Core 也没有啥好用的分析工具,所以也不知道该怎么写 Linq 生成出 ...

  7. ASP.NET Core微服务 on K8S学习笔记(第一章:详解基本对象及服务发现)

    课程链接:http://video.jessetalk.cn/course/explore 良心课程,大家一起来学习哈! 任务1:课程介绍 任务2:Labels and Selectors 所有资源对 ...

  8. Android学习笔记(第二篇)View中的五大布局

    PS:人不要低估自己的实力,但是也不能高估自己的能力.凡事谦为本... 学习内容: 1.用户界面View中的五大布局... i.首先介绍一下view的概念   view是什么呢?我们已经知道一个Act ...

  9. Node 之 Express 学习笔记 第二篇 Express 4x 骨架详解

    周末,没事就来公司加班继续研究一下Express ,这也许也是单身狗的生活吧. 1.目录结构: bin, 存放启动项目的脚本文件 node_modules, 项目所有依赖的库,以及存放 package ...

随机推荐

  1. python拼接multipart/form-data类型post请求格式

    # 最近要做form-data类型接口,大多数这种格式用来文件上传,但是我们公司就是用这种格式传输请求数据. # 百度了一些基本都是files方式的,可是我们需要data=方式的.下面自己来拼接,代码 ...

  2. 第四周Java作业

    老师说让用二维数组找最大,也就是最大和块,要求必须挨着,我其实不会写这个程序,所以我只能把自己的思路写出来 我觉得可以大问题缩小,我的思路是先把四个数一个正方形来进行计算,然后六个数矩形,把他化成两个 ...

  3. hibernate框架的简单入门

    1.什么是框架 框架是一个半成品,框架帮我们实现了一部分的功能. 2.使用框架的最大好处 使用框架的最大好处就是,少写一部分代码但仍能实现我们所需要实现的功能. 3.什么是hiberbnate框架 ( ...

  4. 【转】OJ提交时G++与C++的区别

    关于G++ 首先更正一个概念,C++是一门计算机编程语言,G++不是语言,是一款编译器中编译C++程序的命令而已.那么他们之间的区别是什么? 在提交题目中的语言选项里,G++和C++都代表编译的方式. ...

  5. 论文阅读(XiangBai——【PAMI2018】ASTER_An Attentional Scene Text Recognizer with Flexible Rectification )

    目录 XiangBai--[PAMI2018]ASTER_An Attentional Scene Text Recognizer with Flexible Rectification 作者和论文 ...

  6. css基础重点内容总结

    一.目录引入 ./同级(当前) ../上级目录  ../../上上级目录 二.标签种类: 1.块级标签(block):独占一行,宽高可设: 2.行内块标签(inline-block):不独占一行,宽高 ...

  7. /bin, /sbin & /usr/bin, /usr/sbin & /usr/local/bin, /usr/local/sbin & glibc

    操作系统为自身完成启动所需要的 /bin, /sbin 系统基本管理所需要的 /usr/bin, /usr/sbin 第三方的 /usr/local/bin, /usr/local/sbin 核心库 ...

  8. 移动端开发调试工具神器--Weinre使用方法

    前端开发调试必备: DOM操作断点调试: debugger断点调试: native方法hook(个人暂时还没有试过,不知效果如何): 远程映射本地测试: Weinre移动调试(详细介绍): 像Dom断 ...

  9. java线程学习之yield方法

    yield方法是暂停当前正在执行的线程对象,并执行其他线程. 这是一个静态方法,一旦执行,它会使当前线程让出CPU.让出的cpu并不代表当前线程不执行了.当前线程让出CPU后,还会CPU资源的争夺,但 ...

  10. mask rcnn input数据理解

    Array.min() #无参,所有中的最小值 Array.min(0) # axis=0; 每列的最小值 Array.min(1) # axis=1:每行的最小值 字符串在输出时的对齐: S.lju ...