用Abp实现两步验证(Two-Factor Authentication,2FA)登录(三):免登录验证
@
免登录验证是用户在首次两步验证通过后,在常用的设备(浏览器)中,在一定时间内不需要再次输入验证码直接登录。
常见的网页上提示“7天免登录验证”或“信任此设备,7天内无需两步验证”等内容。
这样可以提高用户的体验。但同时也会带来一定的安全风险,因此需要用户自己决定是否开启。

原理
常用的实现方式是在用户登录成功后,生成一个随机的字符串Token,将此Token保存在用户浏览器的 cookie 中,同时将这个字符串保存在用户的数据库中。当用户再次访问时,如果 cookie 中的字符串和数据库中的字符串相同,则免登录验证通过。流程图如下:

为了安全,Token采用对称加密传输存储,同时参与校验的还有用户Id,以进一步验证数据一致性。Token存储于数据库中并设置过期时间(ExpireDate)
认证机制由JSON Web Token(JWT)实现,通过自定义Payload声明中添加Token和用户Id字段,实现校验。
下面来看代码实现:
修改请求报文
项目添加对Microsoft.AspNetCore.Authentication.JwtBearer包的引用
<packagereference include="Microsoft.AspNetCore.Authentication.JwtBearer" version="7.0.4">
在Authenticate方法参数AuthenticateModel中添加RememberClient和RememberClientToken属性,
当首次登录时,若用户选择免登录,RememberClient为true,
非首次登录时,系统校验RememberClientToken合法性,是否允许跳过两步验证。
public class AuthenticateModel
{
..
public bool RememberClient { get; set; }
public string RememberClientToken { get; set; }
}
同时返回值中添加RememberClientToken,用于首次登录生成的Token
public class AuthenticateResultModel
{
...
public string RememberClientToken { get; set; }
}
配置JwtBearerOptions
在TokenAuthController的Authenticate方法中,添加validation参数:
var validationParameters = new TokenValidationParameters
{
ValidAudience = _configuration.Audience,
ValidIssuer = _configuration.Issuer,
IssuerSigningKey = _configuration.SecurityKey
};
在默认的AbpBoilerplate模板项目中已经为我们生成了默认配置
"Authentication": {
"JwtBearer": {
"IsEnabled": "true",
"SecurityKey": "MatoAppSample_C421AAEE0D114E9C",
"Issuer": "MatoAppSample",
"Audience": "MatoAppSample"
}
},
生成Token
在TokenAuthController类中
添加自定义Payload声明类型
public const string USER_IDENTIFIER_CLAIM = "USER_IDENTIFIER_CLAIM";
public const string REMEMBER_CLIENT_TOKEN = "REMEMBER_CLIENT_TOKEN";
添加生成Token的方法CreateAccessToken,它将根据自定义Payload声明,validationParameters生成经过SHA256加密的Token,过期时间即有效期为7天:
private string CreateAccessToken(IEnumerable<claim> claims, TokenValidationParameters validationParameters)
{
var now = DateTime.UtcNow;
var expiration = TimeSpan.FromDays(7);
var signingCredentials = new SigningCredentials(validationParameters.IssuerSigningKey, SecurityAlgorithms.HmacSha256);
var jwtSecurityToken = new JwtSecurityToken(
issuer: validationParameters.ValidIssuer,
audience: validationParameters.ValidAudience,
claims: claims,
notBefore: now,
expires: now.Add(expiration),
signingCredentials: signingCredentials
);
return new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken);
}
更改方法TwoFactorAuthenticateAsync的签名,添加rememberClient和validationParameters形参
在该方法中添加生成Token的代码
if (rememberClient)
{
if (await settingManager.GetSettingValueAsync<bool>(AbpZeroSettingNames.UserManagement.TwoFactorLogin.IsRememberBrowserEnabled))
{
var expiration = TimeSpan.FromDays(7);
var tokenValidityKey = Guid.NewGuid().ToString("N");
var accessToken = CreateAccessToken(new[]
{
new Claim(USER_IDENTIFIER_CLAIM, user.ToUserIdentifier().ToString()),
new Claim(REMEMBER_CLIENT_TOKEN, tokenValidityKey)
}, validationParameters
);
await _userManager.AddTokenValidityKeyAsync(user, tokenValidityKey,
DateTime.Now.Add(expiration));
return accessToken;
}
}
校验Token
添加校验方法TwoFactorClientRememberedAsync,它表示校验结果是否允许跳过两步验证
public async Task<bool> TwoFactorClientRememberedAsync(UserIdentifier userIdentifier, string TwoFactorRememberClientToken, TokenValidationParameters validationParameters)
{
if (!await settingManager.GetSettingValueAsync<bool>(AbpZeroSettingNames.UserManagement.TwoFactorLogin.IsRememberBrowserEnabled))
{
return false;
}
if (string.IsNullOrWhiteSpace(TwoFactorRememberClientToken))
{
return false;
}
try
{
var tokenHandler = new JwtSecurityTokenHandler();
if (tokenHandler.CanReadToken(TwoFactorRememberClientToken))
{
try
{
SecurityToken validatedToken;
var principal = tokenHandler.ValidateToken(TwoFactorRememberClientToken, validationParameters, out validatedToken);
var userIdentifierString = principal.Claims.First(c => c.Type == TwoFactorAuthorizationManager.USER_IDENTIFIER_CLAIM);
if (userIdentifierString == null)
{
throw new SecurityTokenException(TwoFactorAuthorizationManager.USER_IDENTIFIER_CLAIM + " invalid");
}
var tokenValidityKeyInClaims = principal.Claims.First(c => c.Type == TwoFactorAuthorizationManager.REMEMBER_CLIENT_TOKEN);
var currentUserIdentifier = UserIdentifier.Parse(userIdentifierString.Value);
var user = _userManager.GetUserById(currentUserIdentifier.UserId);
var isValidityKetValid = AsyncHelper.RunSync(() => _userManager.IsTokenValidityKeyValidAsync(user, tokenValidityKeyInClaims.Value));
if (!isValidityKetValid)
{
throw new SecurityTokenException(REMEMBER_CLIENT_TOKEN + " invalid");
}
return userIdentifierString.Value == userIdentifier.ToString();
}
catch (Exception ex)
{
LogHelper.LogException(ex);
}
}
}
catch (Exception ex)
{
LogHelper.LogException(ex);
}
return false;
}
更改方法IsTwoFactorAuthRequiredAsync添加twoFactorRememberClientToken和validationParameters形参
添加对TwoFactorClientRememberedAsync的调用
public async Task<bool> IsTwoFactorAuthRequiredAsync(AbpLoginResult<tenant, user=""> loginResult, string TwoFactorRememberClientToken, TokenValidationParameters validationParameters)
{
if (!await settingManager.GetSettingValueAsync<bool>(AbpZeroSettingNames.UserManagement.TwoFactorLogin.IsEnabled))
{
return false;
}
if (!loginResult.User.IsTwoFactorEnabled)
{
return false;
}
if ((await _userManager.GetValidTwoFactorProvidersAsync(loginResult.User)).Count <= 0)
{
return false;
}
if (await TwoFactorClientRememberedAsync(loginResult.User.ToUserIdentifier(), TwoFactorRememberClientToken, validationParameters))
{
return false;
}
return true;
}
修改认证EndPoint
在TokenAuthController的Authenticate方法中,找到校验代码片段,对以上两个方法的调用传入实参
...
await userManager.InitializeOptionsAsync(loginResult.Tenant?.Id);
string twoFactorRememberClientToken = null;
if (await twoFactorAuthorizationManager.IsTwoFactorAuthRequiredAsync(loginResult, model.RememberClientToken, validationParameters))
{
if (string.IsNullOrEmpty(model.TwoFactorAuthenticationToken))
{
return new AuthenticateResultModel
{
RequiresTwoFactorAuthenticate = true,
UserId = loginResult.User.Id,
TwoFactorAuthenticationProviders = await userManager.GetValidTwoFactorProvidersAsync(loginResult.User),
};
}
else
{
twoFactorRememberClientToken = await twoFactorAuthorizationManager.TwoFactorAuthenticateAsync(loginResult.User, model.TwoFactorAuthenticationToken, model.TwoFactorAuthenticationProvider, model.RememberClient, validationParameters);
}
}
完整的TwoFactorAuthorizationManager代码如下:
public class TwoFactorAuthorizationManager : ITransientDependency
{
public const string USER_IDENTIFIER_CLAIM = "USER_IDENTIFIER_CLAIM";
public const string REMEMBER_CLIENT_TOKEN = "REMEMBER_CLIENT_TOKEN";
private readonly UserManager _userManager;
private readonly ISettingManager settingManager;
private readonly SmsCaptchaManager smsCaptchaManager;
private readonly EmailCaptchaManager emailCaptchaManager;
public TwoFactorAuthorizationManager(
UserManager userManager,
ISettingManager settingManager,
SmsCaptchaManager smsCaptchaManager,
EmailCaptchaManager emailCaptchaManager)
{
this._userManager = userManager;
this.settingManager = settingManager;
this.smsCaptchaManager = smsCaptchaManager;
this.emailCaptchaManager = emailCaptchaManager;
}
public async Task<bool> IsTwoFactorAuthRequiredAsync(AbpLoginResult<tenant, user=""> loginResult, string TwoFactorRememberClientToken, TokenValidationParameters validationParameters)
{
if (!await settingManager.GetSettingValueAsync<bool>(AbpZeroSettingNames.UserManagement.TwoFactorLogin.IsEnabled))
{
return false;
}
if (!loginResult.User.IsTwoFactorEnabled)
{
return false;
}
if ((await _userManager.GetValidTwoFactorProvidersAsync(loginResult.User)).Count <= 0)
{
return false;
}
if (await TwoFactorClientRememberedAsync(loginResult.User.ToUserIdentifier(), TwoFactorRememberClientToken, validationParameters))
{
return false;
}
return true;
}
public async Task<bool> TwoFactorClientRememberedAsync(UserIdentifier userIdentifier, string TwoFactorRememberClientToken, TokenValidationParameters validationParameters)
{
if (!await settingManager.GetSettingValueAsync<bool>(AbpZeroSettingNames.UserManagement.TwoFactorLogin.IsRememberBrowserEnabled))
{
return false;
}
if (string.IsNullOrWhiteSpace(TwoFactorRememberClientToken))
{
return false;
}
try
{
var tokenHandler = new JwtSecurityTokenHandler();
if (tokenHandler.CanReadToken(TwoFactorRememberClientToken))
{
try
{
SecurityToken validatedToken;
var principal = tokenHandler.ValidateToken(TwoFactorRememberClientToken, validationParameters, out validatedToken);
var userIdentifierString = principal.Claims.First(c => c.Type == TwoFactorAuthorizationManager.USER_IDENTIFIER_CLAIM);
if (userIdentifierString == null)
{
throw new SecurityTokenException(TwoFactorAuthorizationManager.USER_IDENTIFIER_CLAIM + " invalid");
}
var tokenValidityKeyInClaims = principal.Claims.First(c => c.Type == TwoFactorAuthorizationManager.REMEMBER_CLIENT_TOKEN);
var currentUserIdentifier = UserIdentifier.Parse(userIdentifierString.Value);
var user = _userManager.GetUserById(currentUserIdentifier.UserId);
var isValidityKetValid = AsyncHelper.RunSync(() => _userManager.IsTokenValidityKeyValidAsync(user, tokenValidityKeyInClaims.Value));
if (!isValidityKetValid)
{
throw new SecurityTokenException(REMEMBER_CLIENT_TOKEN + " invalid");
}
return userIdentifierString.Value == userIdentifier.ToString();
}
catch (Exception ex)
{
LogHelper.LogException(ex);
}
}
}
catch (Exception ex)
{
LogHelper.LogException(ex);
}
return false;
}
public async Task<string> TwoFactorAuthenticateAsync(User user, string token, string provider, bool rememberClient, TokenValidationParameters validationParameters)
{
if (provider == "Email")
{
var isValidate = await emailCaptchaManager.VerifyCaptchaAsync(token, CaptchaPurpose.TWO_FACTOR_AUTHORIZATION);
if (!isValidate)
{
throw new UserFriendlyException("验证码错误");
}
}
else if (provider == "Phone")
{
var isValidate = await smsCaptchaManager.VerifyCaptchaAsync(token, CaptchaPurpose.TWO_FACTOR_AUTHORIZATION);
if (!isValidate)
{
throw new UserFriendlyException("验证码错误");
}
}
else
{
throw new UserFriendlyException("验证码提供者错误");
}
if (rememberClient)
{
if (await settingManager.GetSettingValueAsync<bool>(AbpZeroSettingNames.UserManagement.TwoFactorLogin.IsRememberBrowserEnabled))
{
var expiration = TimeSpan.FromDays(7);
var tokenValidityKey = Guid.NewGuid().ToString("N");
var accessToken = CreateAccessToken(new[]
{
new Claim(USER_IDENTIFIER_CLAIM, user.ToUserIdentifier().ToString()),
new Claim(REMEMBER_CLIENT_TOKEN, tokenValidityKey)
}, validationParameters
);
await _userManager.AddTokenValidityKeyAsync(user, tokenValidityKey,
DateTime.Now.Add(expiration));
return accessToken;
}
}
return null;
}
private string CreateAccessToken(IEnumerable<claim> claims, TokenValidationParameters validationParameters)
{
var now = DateTime.UtcNow;
var expiration = TimeSpan.FromDays(7);
var signingCredentials = new SigningCredentials(validationParameters.IssuerSigningKey, SecurityAlgorithms.HmacSha256);
var jwtSecurityToken = new JwtSecurityToken(
issuer: validationParameters.ValidIssuer,
audience: validationParameters.ValidAudience,
claims: claims,
notBefore: now,
expires: now.Add(expiration),
signingCredentials: signingCredentials
);
return new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken);
}
public async Task SendCaptchaAsync(long userId, string provider)
{
var user = await _userManager.FindByIdAsync(userId.ToString());
if (user == null)
{
throw new UserFriendlyException("找不到用户");
}
if (provider == "Email")
{
if (!user.IsEmailConfirmed)
{
throw new UserFriendlyException("未绑定邮箱");
}
await emailCaptchaManager.SendCaptchaAsync(user.Id, user.EmailAddress, CaptchaPurpose.TWO_FACTOR_AUTHORIZATION);
}
else if (provider == "Phone")
{
if (!user.IsPhoneNumberConfirmed)
{
throw new UserFriendlyException("未绑定手机号");
}
await smsCaptchaManager.SendCaptchaAsync(user.Id, user.PhoneNumber, CaptchaPurpose.TWO_FACTOR_AUTHORIZATION);
}
else
{
throw new UserFriendlyException("验证码提供者错误");
}
}
}
至此我们就完成了后端部分的开发
修改前端
登录
在两步验证的页面中添加一个checkbox,用于选择是否记住客户端
<el-checkbox v-model="loginForm.rememberClient">
7天内不再要求两步验证
</el-checkbox>
JavaScript部分添加对rememberClientToken的处理,存储于cookie中,即便在网页刷新后也能保持免两步验证的状态
const rememberClientTokenKey = "main_rememberClientToken";
const setRememberClientToken = (rememberClientToken: string) =>
Cookies.set(rememberClientTokenKey, rememberClientToken);
const cleanRememberClientToken = () => Cookies.remove(rememberClientTokenKey);
const getRememberClientToken = () => Cookies.get(rememberClientTokenKey);
在请求body中添加rememberClientToken, rememberClient的值
var rememberClientToken = getRememberClientToken();
var rememberClient=this.loginForm.rememberClient;
userNameOrEmailAddress = userNameOrEmailAddress.trim();
await request(`${this.host}api/TokenAuth/Authenticate`, "post", {
userNameOrEmailAddress,
password,
twoFactorAuthenticationToken,
twoFactorAuthenticationProvider,
rememberClientToken,
rememberClient
})
请求成功后,返回报文中包含rememberClientToken,将其存储于cookie中
setRememberClientToken(data.rememberClientToken);
登出
登出的逻辑不用做其他的修改,只需要将页面的两步验证的token清空即可,
this.loginForm.twoFactorAuthenticationToken = "";
this.loginForm.password = "";
rememberClientToken是存储于cookie中的,当用户登出时不需要清空cookie中的rememberClientToken,以便下次登录跳过两步验证
除非在浏览器设置中清空cookie,下次登录时,rememberClientToken就会失效。
最终效果

项目地址
Github:matoapp-samples</tenant,></tenant,>
用Abp实现两步验证(Two-Factor Authentication,2FA)登录(三):免登录验证的更多相关文章
- SpringBoot注册登录(三):注册--验证账号密码是否符合格式及后台完成注册功能
SpringBoot注册登录(一):User表的设计点击打开链接SpringBoot注册登录(二):注册---验证码kaptcha的实现点击打开链接 SpringBoot注册登录(三):注册 ...
- 挑子学习笔记:两步聚类算法(TwoStep Cluster Algorithm)——改进的BIRCH算法
转载请标明出处:http://www.cnblogs.com/tiaozistudy/p/twostep_cluster_algorithm.html 两步聚类算法是在SPSS Modeler中使用的 ...
- 两步验证Authy时间同步问题
Authy是我常用的软件之一,通常用于Google的两步验证,或者是其他基于Google两步验证的原理的衍生程序.比如Namesilo.印象笔记等均有使用. 先说说什么是两步验证. 两步验证 两步验证 ...
- Google 推出全新的两步验证机制
近日 Google 在官方的 Apps Updates 博客公布了全新的两步验证功能--Google 提示,新的功能通过与 Google App 联动,进一步将验证确认工作缩减到仅有两步,同时支持 A ...
- SecureCRT两步验证自动登录脚本
简介 用于解决 Google Authenticator 的两步验证登录.涉及到密码,不建议脚本保存到公共环境. 安装oathtool Mac $ brew install oath-toolkit ...
- 两步验证杀手锏:Java 接入 Google 身份验证器实战
两步验证 大家应该对两步验证都熟悉吧?如苹果有自带的两步验证策略,防止用户账号密码被盗而锁定手机进行敲诈,这种例子屡见不鲜,所以苹果都建议大家开启两步验证的. Google 的身份验证器一般也是用于登 ...
- 使用KeePass管理两步验证
目录 使用KeePass管理两步验证 两步验证 KeePass中管理两步验证 KeeTrayTOTP插件使用 使用KeePass管理两步验证 文:铁乐与猫 2018-9-9 KeePass 是一款管理 ...
- OPTAUTH 两步验证详解
先贴图: 在对外网开放的后台管理系统中,使用静态口令进行身份验证可能会存在如下问题: (1) 为了便于记忆,用户多选择有特征作为密码,所有静态口令相比动态口令而言,容易被猜测和破解: (2) 黑客可以 ...
- 为Linux服务器的SSH登录启用Google两步验证
对于Linux服务器而言使用密钥登录要比使用密码登录安全的多,毕竟当前网上存在多个脚本到处进行爆破. 这类脚本都是通过扫描IP端的开放端口并使用常见的密码进行登录尝试,因此修改端口号也是非常有必要的. ...
- Authenticator App 两步验证会不会造成亚马逊账号关联?
今天听人说,因为用Authenticator App做亚马逊两步验证造成了帐号关联…… 我给大家解释一下Authenticator的实现原理,作为计算机专业科班出身的我,此次从各方面了解并经过自己亲测 ...
随机推荐
- swing编程
在界面中放入图片:路径以项目路径为准 img包一般放在项目文件夹下 package adapter;import java.awt.Color;import java.awt.Dimension;i ...
- 三级菜单python编码及高级编码
# -*- coding: utf-8 -*- # @Time : 2020/7/31 0:13 # @Author : Breeze # @FileName: 三级菜单.py menu = { '北 ...
- 【cs231n】knn作业笔记
完成了assignment-1中knn相关内容的作业,记录一下遇到的知识点和问题 knn.ipynb的内容大致包括: 1.数据集的建立 主要是通过切片函数,如下图选取前5000张图片和其标记作为训练数 ...
- Excel 多表头导入导出(借助Aspose)
需求中Excell多表头,完成导入导出. Aspose 代码实现多表头方式借助代码比较繁琐, 借助模板方式. 简化逻辑. 注意,aspose从0开始索引. 导入部分代码: 实现选择导入Excel,导入 ...
- logstash从MySQL导入数据到ES
下载安装 一定要对应ES版本(5.x,6.x,7.x) win下不用安装解压即用 , 解压目录不能带有空格和中文 , 否则会有奇奇怪怪的报错无法运行 win下要给logstash文件夹赋予管理员权限 ...
- Spyglass CDC工具使用(三)
最近一直在搞CDC (clock domain crossing) 方面的事情,现在就CDC的一些知识点进行总结. 做CDC检查使用的是Spyglass工具.以下内容转载自:Spyglass之CDC检 ...
- 通过网页或者移动设备链接跳转qq(tim)添加好友(群)
首先需要去qq群官方,然后点记加群组件,然后选择群,复制对应的代码即可 登录到QQ群官网 点击加群组件 选择群,选择网页还是移动设备 复制代码 示例: <html> <head> ...
- C#向其实进程子窗体发送指令
近日,想在自己的软件简单控制其它软件的最大化最小化,想到直接向进程发送指令,结果一直无效,经过Spy++发现,原来快捷方式在子窗体上,所以需要遍历子窗体在发送指令,以下为参考代码: 1 [DllImp ...
- Oracle 取Group By 第一条
select *from (select emp.*,row_number() over(partition by deptno order by rownum) cn from emp)where ...
- You need to run build with JDK or have tools.jar on the classpath.If this occures during eclipse build make sure you run eclipse under JDK as well 错误
我打开项目报错是这样的 pom.xml jdk配置什么的都是好的 但是还是报错 解决错误 : 1.打开你eclipse的根目录,找到eclipse.ini 这个文件夹打开 2.打开是这个样子 ...