前言

之前写过一篇 Identity – User Login, Forgot Password, Reset Password, Logout, 当时写的比较简陋, 今天有机会就写多一篇实战版.

建议先阅读之前那篇做一个 warm up.

本篇会讲到

1. user login

2. forgot and reset password

3. two factor

4. external login

5. logout

我这个实战场景是一个 CMS 的登入. 由 Admin 提前创建好 User (员工), 所以本篇不会涉及 user registrator 的环节.

而且 external login 也是提前由 Admin 配置好的.

这篇主要是讲和 Identity 相关的代码. 不会涵盖所有的代码, 它不是一个 step by step 的教程哦.

主要参考

Identity – Introduction & Scaffold (做一个 Scaffold 看代码)

Identity source code

Authentication source code

Docs – Identity (所有相关的)

Program.cs Setup

identity configuration

builder.Services
.AddIdentity<User, Role/* 包含简单的 RBAC*/>(options =>
{
options.Lockout.MaxFailedAccessAttempts = 15; // default 是 5 次, 太少了
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5); // default 是 15 分钟, 太久了 // strong password required
options.Password.RequireDigit = true;
options.Password.RequireLowercase = true;
options.Password.RequireNonAlphanumeric = true;
options.Password.RequireUppercase = true;
options.Password.RequiredLength = 10;
options.Password.RequiredUniqueChars = 1; options.User.AllowedUserNameCharacters = ""; // default 有一些符号是不允许的, 我 by pass all character
options.User.RequireUniqueEmail = true; // default 是 false 不 practical 吧
})
.AddDefaultTokenProviders()
.AddEntityFrameworkStores<ApplicationDbContext>();

cookie configuration

builder.Services.ConfigureApplicationCookie(options =>
{
options.AccessDeniedPath = "/access-denied";
options.LoginPath = "/login";
options.LogoutPath = "/logout";
options.ReturnUrlParameter = "returnUrl";
options.ExpireTimeSpan = TimeSpan.FromDays(7);
options.SlidingExpiration = true;
});

改了一些路径, 和 cookie 有效期规则, 没什么重点.

External Login 的 configuration 我下面会补上.

Create User

// create super admin role
var superAdminRole = new Role
{
Name = "Super Admin",
Description = "The super admin role has full permission to access any page, API or function. Please be cautious when assigning this role.",
Status = RoleStatus.Active,
IsSystemRole = true,
};
await roleManager.CreateAsync(superAdminRole); // create user
var derrick = new User
{
UserName = "Derrick",
Email = "hengkeat87@gmail.com",
ProfileName = "Derrick Yam",
};
await userManager.CreateAsync(derrick);
await userManager.AddPasswordAsync(derrick, "temporary strong password");
await userManager.AddToRoleAsync(derrick, "Super Admin");

每一个新 user 都 set 一个 strong password, 用户第一次登入时可以通过 forgot password, 登入后在自行更换密码.

External Login 和 Two-factor 下面我会补上.

Visit protect page and redirect to login

做一个 private 页面

访问它就会被 redirect 到 /login (我们在 cookie configuration 配置的路径), 同时会带上 query params ?returnUrl=/private

当用户登入后会被 redirect 回来 /private 页面. 这个是常见的用户体验.

Login

Login page 长这样

我们假设用户已经知道他的密码. 那他就输入 username, password, remember me 然后 submit.

题外话:

remember me default 是 tick. 这个我是参考了 Google 和 Microsoft 的体验.

Google 是完全没有问直接就是 remember me, Microsoft 会有一个 popup 询问, 它的引导是 Yes remember me.

所以大家都假设用户是在自己设备上使用居多.

Login.cs

主要代码是

var loginResult = await _signInManager.PasswordSignInAsync(
user, formData.Password,
isPersistent: formData.RememberMe,
lockoutOnFailure: true
);

PasswordSignInAsync 内部会替我们管理暴力破解. 当密码错误太多次后, user account 就会被 lock 一段时间.

loginResult 会显示各种可能出现的错误. 我们逐个处理就可以了.

when every ok, return Page() at the end. 在 /login 页面执行 SignIn 代码, ASP.NET Core 会替我们 redirect user to returnUrl. 我们不需要自己做 redirect.

Forgot Password

在 login button 下方有一个小小的 forgot password link

user 忘记密码时点击它会去到 /forgot-password 页面, 它长这样

通过 email 找回 username 和 password.

ForgotPassword.cs

主要代码是创建 token

var token = await userManager.GeneratePasswordResetTokenAsync(user);

这个 token 在 /reset-password page 就会用上了.

其余的代码都是在做 email 的东西, user 打开 email 按一个 link 会去到 /reset-password 输入新密码.

email 内容长这样

Reset Password

ResetPassword.cs

主要代码是

var resetPasswordResult = await userManager.ResetPasswordAsync(user, token, formData.NewPassword);
if (userManager.SupportsUserLockout && await userManager.IsLockedOutAsync(user))
{
await userManager.ResetAccessFailedCountAsync(user);
await userManager.SetLockoutEndDateAsync(user, null);
}

user reset password 我们也可以直接替他 sign in, 这个看你体验要怎样设计. 因为也要考虑到后续的 two-factor, 带去 login 走一遍正规流程也是一个不错的选择.

注: 如果要替 user 直接 sign in 的话, 需要手动 redirect to returnUrl. ASP.NET Core 只有在 /login 页面 sign in 才会替自动 redirect.

Require Two-factor

首先在 create user 时要 enable two-factor

// create user
var derrick = new User
{
UserName = "Derrick",
Email = "hengkeat87@gmail.com",
ProfileName = "Derrick Yam",
};
await userManager.CreateAsync(derrick);
await userManager.AddPasswordAsync(derrick, "temporary strong password");
await userManager.AddToRoleAsync(derrick, "Super Admin");
// 开启 two-factor
await userManager.SetTwoFactorEnabledAsync(derrick, true);
await userManager.ResetAuthenticatorKeyAsync(derrick);

Login.cs

1. 只有在 SetTwoFactorEnabledAsync 和 ResetAuthenticatorKeyAsync 两个都成立后, PasswordSignInAsync 才会检查 two factor.

如果只是 enable 但是没有 generate authenticator key 是不算开启 two-factor 的哦.

2. for 第一次 two-factor 我们需要让用户 scan qrcode setup authenticator. 这里我利用了 recovery code 做一个判断是否是第一次 (取巧)

3. recovery code 是当 user lost device 的时候可以使用的 two-factor login backup 方案.

Setup Two-Factor

第一次 login two-factor 需要 scan qrcode setup authenticator app.

bek3 caln luvw uplr zmwl 75eq dhuk sm4h 这段 code 就是上面 ResetAuthenticatorKeyAsync 搞出来的.

我只是把它按四个字母空格分隔, 然后 lowercase. 这是为了用户体验. qrcode 也是基于这个 key 做出来的.

SetupTwoFactor.cs

要先检查 user 是否已经通过 password login, 如果有 require two-factor, password login 会保存在 session cookie 而且时间很短而已.

authen key 是在 create user 时就已经 set 进去了的, 现在只是拿出来而已.

generate qrcode 代码

string GenerateQrCodeBase64(string companyName, string username, string authenticatorKey)
{
var queryBuilder = new QueryBuilder();
queryBuilder.Add("secret", authenticatorKey);
queryBuilder.Add("issuer", companyName + " CMS");
queryBuilder.Add("digits", "6");
var uri = $"otpauth://totp/{StgUtil.EncodeURIComponent(username)}" + queryBuilder.ToQueryString().Value; var writer = new BarcodeWriterPixelData
{
Format = BarcodeFormat.QR_CODE,
Options = new QrCodeEncodingOptions
{
Height = 200,
Width = 200,
NoPadding = true
}
};
var pixelData = writer.Write(uri);
var image = Image.LoadPixelData<Rgba32>(pixelData.Pixels, pixelData.Width, pixelData.Height);
return image.ToBase64String(JpegFormat.Instance);
}

用到了 ZXing 和 ImageSharp 插件. qrcode 内容是一个 URI, 开头是 otpauth://totp 然后是一些参数. secret authen key 不可以有空格哦.

user scan 了之后就会拿到 OTP, 输入后就 verify

登入后可以显示 recovery code 给 user 让它记入起来. (这里我偷懒没有做处理)

注: 虽然 database 里有保存 recovery code, 但是 Identity 没有任何 public 接口可以查看. 调用 GenerateNewTwoFactorRecoveryCodesAsync 后会返回 recovery codes 它就只有一次机会.

往后可以在调用, 它会 delete 掉之前的然后创建过新的.

Login with Two-factor

for 2nd times login 的 user 就不需要再 setup authen app 了.

LoginWithTwoFactor.cs

和 setup two-factor 差不多, 检查 OTP 就可以了.

1. TwoFactorAuthenicatorSignInAsync 的 isPersistent 指的是 user cookie 和 password sign in 的 isPersistent 等价 , 而 rememberClient 指的是 two-factor cookie.

如果 isPersistent: false, rememberClient: true 就表示 close browser 后 user 必须重新 password sign in, 但不需要再一次 two-factor OTP login.

2. 登入后记得要 manual redirect to returnUrl 哦. ASP.NET Core 只有在 /login page 会替我们自动 redirect

Login with Recovery Code

LoginWithRecoveryCode.cs

1. 处理 two-factor 前, 记得要先检查 user 是否已经通过 pasword login

2. recovery code 是可以一直 repeat use 的.

3. TwoFactoryRecoveryCodeSignInAsync 的 isPersistent 和 rememberClient 一定是 false. 全部走 session cookie. 这个是 Identity 封装的用户体验 (为了安全)

External Login

介绍

虽然 external login 经常和 Identity 一起出现, 但其实它们是可以完全无关系的.

你可以 external login without Identity. (external login package under /security/authentication 而 /identity 是独立另一个 package)

external login 通常有两个最要目的.

1. 拿来替代 password login

2. 拿来 call thrid party API service.

这两个目的是完全不同而且没有关联的, 你可以只用作第一个或只用作第二个, 这篇我主要是作为第一个用途.

Register Client App

参考:

Docs – Google external login setup in ASP.NET Core

Docs – Microsoft Account external login setup with ASP.NET Core

Docs – Facebook external login setup in ASP.NET Core

Program.cs

builder.Services.AddAuthentication()
.AddGoogle(options =>
{
options.ClientId = "client id";
options.ClientSecret = "client secret";
options.AccessDeniedPath = "/external-login"; // if user suddenly cancel login will redirect to this
options.ReturnUrlParameter = "returnUrl";
options.SaveTokens = true; // true if we need direct use access token / refresh token
options.AccessType = "offline"; // if we need refresh token
// add whatever scope you need
// scope list: https://developers.google.com/identity/protocols/oauth2/scopes
options.Scope.Add("https://www.googleapis.com/auth/userinfo.email");
options.Scope.Add("https://www.googleapis.com/auth/userinfo.profile");
options.Scope.Add("https://www.googleapis.com/auth/plus.business.manage");
// prompt = every time re-popup ask for permission
// include_granted_scopes = scope will 累加
options.AuthorizationEndpoint += "?prompt=consent&include_granted_scopes=true";
})
.AddMicrosoftAccount(options =>
{
options.ClientId = "client id";
options.ClientSecret = "client secret";
options.AccessDeniedPath = "/external-login";
options.ReturnUrlParameter = "returnUrl";
options.SaveTokens = true;
options.Scope.Add("offline_access"); // if we need refresh token
options.Scope.Add("https://graph.microsoft.com/User.ReadBasic.All");
options.Scope.Add("https://graph.microsoft.com/Calendars.ReadWrite");
options.Scope.Add("https://graph.microsoft.com/Mail.ReadWrite");
options.AuthorizationEndpoint += "?prompt=consent";
})
.AddFacebook(options =>
{
options.AppId = "client id";
options.AppSecret = "client secret";
options.AccessDeniedPath = "/external-login";
options.ReturnUrlParameter = "returnUrl";
options.SaveTokens = true;
options.AuthorizationEndpoint += "?prompt=consent";
});

看注释理解细节.

1. Google 和 Microsoft 的接口不完全一致.

2. client client 和 secret 最好存放进 User Secrets 里.

3. facebook 的 version 通常比较旧,比如目前最新版本是 v18,但是 ASP.NET Core 用的是最旧的 v14 版。

如果我们上面 3 个 API 不同版本的接口都是一直的话,我们可以 override 掉它。

通过修改 FacebookOptions 就可以了

4. facebook 没有 refresh token 的概念,它只有 Long-lived User Access Token

Create User

注:下面所有例子只会涉及 Google 和 Microsoft Login,Facebook Login 被我省略掉了,但它没有什么特别,大家都大同小异而已。

在创建 user 的时候添加 external login.

有些系统的体验是让用户 password login 后, 自己去添加. 而我则是让 admin 其添加, user 第一次登入就不需要用 password, two-factor 了.

var providerKey = "Temp_" + Guid.NewGuid().ToString();
await userManager.AddLoginAsync(newDerrick, new UserLoginInfo(
loginProvider: GoogleDefaults.AuthenticationScheme,
providerKey: providerKey,
displayName: GoogleDefaults.DisplayName)
); await userManager.AddLoginAsync(newDerrick, new UserLoginInfo(
loginProvider: MicrosoftAccountDefaults.AuthenticationScheme,
providerKey: providerKey,
displayName: MicrosoftAccountDefaults.DisplayName)
); var googleLoginInfo = newDerrick.Logins.Single(e => e.LoginProvider == GoogleDefaults.AuthenticationScheme);
googleLoginInfo.Email = "hengkeat87@gmail.com";
var microsoftLoginInfo = newDerrick.Logins.Single(e => e.LoginProvider == MicrosoftAccountDefaults.AuthenticationScheme);
microsoftLoginInfo.Email = "keatkeat87@live.com";
await userManager.UpdateAsync(newDerrick);

providerKey by right 应该是要放 user external account 的 ID 的. 但是这个阶段 user 还没有 external login 所以还不知道 ID. 那就先放一个 Temp_Guid 吧.

user external login 后, 会通过 email address 做 matching 找到对应的 account. 这个是我的规则.

Login.cshtml

ExternalLogin.cs

主要就是做 callback link 然后 Challenge

这时 user 就会被 redirect to third party login page (比如 Google / Microsoft login page)

after 登入后 user 会先 redirect 到 /signin-google, 这个是封装好的 request handle 处理 code, state 那些. 全部完成后才会 redirect to 我们的 callback link.

callback link 是 GET /external-login?handler=callback

而当前我们处在 POST /external-login.

如果 user cancel login 会被带到 GET /external-login

全部同一个 page, 但是不同 handler.

OnGetCallbackAsync

1. info.LoginProvider 相等于 info.Principal.Claims.Single(e => e.Type == ClaimTypes.NameIdentifier).Value.

2. 如果想保存 refresh token 可以考虑存放在 UserToken entity

await userManager.SetAuthenticationTokenAsync(user, info.LoginProvider, "RefreshToken", refreshToken);

这个 entity 是 identity 用来存放 two-factor 资料的

提醒:

SetAuthenticationTokenAsync 不可以用来更新 token value,它只能用来创建,如果已有,那将被无视。

所以若想 update token value,我们需要先删除,再创建一个新的。

await _userManager.RemoveAuthenticationTokenAsync(user, info.LoginProvider, "RefreshToken");
await _userManager.SetAuthenticationTokenAsync(user, info.LoginProvider, "RefreshToken", refreshToken);

Logout

/private.cshtml

在任何你想 logout 的 page 做一个 form post to /logout

Logout.cs

public async Task OnPostAsync(
[FromServices] SignInManager<User> signInManager
)
{
await signInManager.SignOutAsync();
// 如果也想把 two factor remember device 也 drop 掉
await signInManager.ForgetTwoFactorClientAsync();
}

1. SignOutAsync 会 sign out 三样东西

如果我们不希望全部 sign out 就自己调用 Context.SignOutAsync 来控制.

2. ForgetTwoFactorClientAsync 是清除 two-factor rememberClient 的 cookie, SignOutAsync 没有负责这个.

3. Logout page 也会自动 redirect to returnUrl 哦.

总结

这篇主的场景是 admin create user account then user login.

没有 cover 到的地方是 user 自己的 account management, 比如 change password 等等.

也没有 cover user 自行 register account 的场景.

以后有机会在补上呗.

Identity – user login, forgot & reset password, 2fa, external login, logout 实战篇的更多相关文章

  1. Reset Password Functionality FAQ

    In this Document   Purpose   Questions and Answers   How can users request a password reset?   How d ...

  2. (phpmyadmin error)Login without a password is forbidden by configuration (see AllowNoPassword) in ubuntu

    1.Go to /etc/phpmyadmin/config.inc.php and open it your favorite editor. 2.Search for below line of ...

  3. Mysql re-set password, mysql set encode utf8 mysql重置密码,mysql设置存储编码格式

    There is a link about how to re-set password. http://database.51cto.com/art/201010/229528.htm words ...

  4. GET: https://login.weixin.qq.com/cgi-bin/mmwebwx-bin/login? loginicon=true &uuid=odcptUu2JA==&tip=0

    GET: https://login.weixin.qq.com/cgi-bin/mmwebwx-bin/login? loginicon=true &uuid=odcptUu2JA==&am ...

  5. db2 identity列重置,reset/restart

    db2中可以对表中的某一个列创建identity列,用于自动填充值,某些情况下(比如删除数据后,需要从最小值开始,并不重复,那可以对标识列进行reset操作) 语法: ALTER TABLE < ...

  6. How to reset password for unknow root

    1. Click "e" when entering the grub 2. Add option " init=/bin/sh" to linux line. ...

  7. reset password for local admin on Windows2016 by Powershell

    上脚本吧,找半天 $password = "yourpassword" $pwd = $password | ConvertTo-SecureString -asPlainText ...

  8. VUE 出现Access to XMLHttpRequest at 'http://192.168.88.228/login/Login?phone=19939306484&password=111' from origin 'http://localhost:8080' has been blocked by CORS policy: The value of the 'Access-Contr

    报错如上图!!!!    解决办法首先打开 config -> index.js ,粘贴 如下图代码,'https://www.baidu.com'换成要访问的的api域名,注意只要域名就够了, ...

  9. SQLite reset password

    https://www.codeproject.com/tips/993395/sqliter-change-set-remove-passwords-on-sqlite-d https://sour ...

  10. mysql reset password重置密码

    安全模式启动 chown -R mysql.mysql /var/run/mysqld/ mysqld_safe --skip-grant-tables & 无密码root帐号登陆 mysql ...

随机推荐

  1. 转载 | ofd转pdf最好用的软件,ofd文件如何转化成pdf?

    1.背景 需要将ofd转换为pdf 2.使用方法 使用taurusxin 开发的软件Ofd2Pdf.exe即可实现,软件版权归原作者所有.这里表示感谢! 3.下载地址 官网:https://githu ...

  2. 转载 | win11右键菜单改为win10的bat命令(以及恢复方法bat)

    原文来自这里:https://blog.51cto.com/knifeedge/5340751 版权归:IT利刃出鞘 本质上就是写入注册表. 一.右键菜单改回Win10(展开) 1. 新建文件:win ...

  3. Swift开发基础02-流程控制

    if-slse let age = 4 if age >= 22 { print("Get married") } else if age >= 18 { print( ...

  4. 可视化—gojs 超多超实用经验分享(三)

    目录 32.go.Palette 一排放两个 33.go.Palette 基本用法 34.创建自己指向自己的连线 35.设置不同的 groupTemplate 和 linkTemplate 36.监听 ...

  5. Error: Dynamic require of "path" is not supported

    failed to load config from D:\BaiduSyncdisk\vue3\sys-manager\vite.config.jserror when starting dev s ...

  6. iOS开发基础133-崩溃预防

    现代移动应用的用户体验依赖于其稳定性和可靠性.然而,在开发过程中,我们时常会遇到各种崩溃问题.崩溃不仅会影响用户的使用体验,还可能损害应用的声誉.因此,本文将详细介绍一个名为CrashPreventi ...

  7. java集合解析

    1,java集合体系 2,Colletion集合 子接口有List和Set (1)List接口:ArrayList,Vector,LinkedList list是collection接口的子接口,特点 ...

  8. Django model 层之聚合查询总结

    Django model 层之聚合查询总结 by:授客 QQ:1033553122 实践环境 Python版本:python-3.4.0.amd64 下载地址:https://www.python.o ...

  9. mysql面试汇总

    最近一直在关注mysql方面的面试题目,并且从最近的面试情况来看,mysql在java后端的面试中,肯定是必问的题目,所以这里有必要对这块的内容进行总结,大家可以根据下面的导图进行重点复习, 引擎 1 ...

  10. 浅谈Git架构和如何避免代码覆盖的事故

    浅谈Git架构和如何避免代码覆盖的事故 Git 不同于 SVN 的地方在于, Git 是分布式的版本管理系统, 所有的客户端和服务器都保存了一份代码, 涉及到仓库仓之间的同步, 所以处理不当极易造成冲 ...