@

免登录验证是用户在首次两步验证通过后,在常用的设备(浏览器)中,在一定时间内不需要再次输入验证码直接登录。

常见的网页上提示“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 =&gt; c.Type == TwoFactorAuthorizationManager.USER_IDENTIFIER_CLAIM);
if (userIdentifierString == null)
{
throw new SecurityTokenException(TwoFactorAuthorizationManager.USER_IDENTIFIER_CLAIM + " invalid");
} var tokenValidityKeyInClaims = principal.Claims.First(c =&gt; c.Type == TwoFactorAuthorizationManager.REMEMBER_CLIENT_TOKEN); var currentUserIdentifier = UserIdentifier.Parse(userIdentifierString.Value); var user = _userManager.GetUserById(currentUserIdentifier.UserId);
var isValidityKetValid = AsyncHelper.RunSync(() =&gt; _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 &lt;= 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 &lt;= 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 =&gt; c.Type == TwoFactorAuthorizationManager.USER_IDENTIFIER_CLAIM);
if (userIdentifierString == null)
{
throw new SecurityTokenException(TwoFactorAuthorizationManager.USER_IDENTIFIER_CLAIM + " invalid");
} var tokenValidityKeyInClaims = principal.Claims.First(c =&gt; c.Type == TwoFactorAuthorizationManager.REMEMBER_CLIENT_TOKEN); var currentUserIdentifier = UserIdentifier.Parse(userIdentifierString.Value); var user = _userManager.GetUserById(currentUserIdentifier.UserId);
var isValidityKetValid = AsyncHelper.RunSync(() =&gt; _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) =&gt;
Cookies.set(rememberClientTokenKey, rememberClientToken);
const cleanRememberClientToken = () =&gt; Cookies.remove(rememberClientTokenKey);
const getRememberClientToken = () =&gt; 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)登录(三):免登录验证的更多相关文章

  1. SpringBoot注册登录(三):注册--验证账号密码是否符合格式及后台完成注册功能

    SpringBoot注册登录(一):User表的设计点击打开链接SpringBoot注册登录(二):注册---验证码kaptcha的实现点击打开链接      SpringBoot注册登录(三):注册 ...

  2. 挑子学习笔记:两步聚类算法(TwoStep Cluster Algorithm)——改进的BIRCH算法

    转载请标明出处:http://www.cnblogs.com/tiaozistudy/p/twostep_cluster_algorithm.html 两步聚类算法是在SPSS Modeler中使用的 ...

  3. 两步验证Authy时间同步问题

    Authy是我常用的软件之一,通常用于Google的两步验证,或者是其他基于Google两步验证的原理的衍生程序.比如Namesilo.印象笔记等均有使用. 先说说什么是两步验证. 两步验证 两步验证 ...

  4. Google 推出全新的两步验证机制

    近日 Google 在官方的 Apps Updates 博客公布了全新的两步验证功能--Google 提示,新的功能通过与 Google App 联动,进一步将验证确认工作缩减到仅有两步,同时支持 A ...

  5. SecureCRT两步验证自动登录脚本

    简介 用于解决 Google Authenticator 的两步验证登录.涉及到密码,不建议脚本保存到公共环境. 安装oathtool Mac $ brew install oath-toolkit ...

  6. 两步验证杀手锏:Java 接入 Google 身份验证器实战

    两步验证 大家应该对两步验证都熟悉吧?如苹果有自带的两步验证策略,防止用户账号密码被盗而锁定手机进行敲诈,这种例子屡见不鲜,所以苹果都建议大家开启两步验证的. Google 的身份验证器一般也是用于登 ...

  7. 使用KeePass管理两步验证

    目录 使用KeePass管理两步验证 两步验证 KeePass中管理两步验证 KeeTrayTOTP插件使用 使用KeePass管理两步验证 文:铁乐与猫 2018-9-9 KeePass 是一款管理 ...

  8. OPTAUTH 两步验证详解

    先贴图: 在对外网开放的后台管理系统中,使用静态口令进行身份验证可能会存在如下问题: (1) 为了便于记忆,用户多选择有特征作为密码,所有静态口令相比动态口令而言,容易被猜测和破解: (2) 黑客可以 ...

  9. 为Linux服务器的SSH登录启用Google两步验证

    对于Linux服务器而言使用密钥登录要比使用密码登录安全的多,毕竟当前网上存在多个脚本到处进行爆破. 这类脚本都是通过扫描IP端的开放端口并使用常见的密码进行登录尝试,因此修改端口号也是非常有必要的. ...

  10. Authenticator App 两步验证会不会造成亚马逊账号关联?

    今天听人说,因为用Authenticator App做亚马逊两步验证造成了帐号关联…… 我给大家解释一下Authenticator的实现原理,作为计算机专业科班出身的我,此次从各方面了解并经过自己亲测 ...

随机推荐

  1. Java的流程控制

    Scanner对象 next(); 一定要读取到有效字符后才可以结束输入. 对输入有效字符之前遇到的空白,next()方法会自动将其去掉(). 只有输入有效字符后才将其后面输入的空白作为分隔符或者结束 ...

  2. antv g6 出现 n.addEdge is not a function问题

    问题描述直接上图 解决方式就是将edge里面边的source和target对应的id换成字符串类型就行. 例如: edges: [ { id: 299, source": 3629.toSt ...

  3. php 验证身份证合法性

    function checkIdcard($num = '') { $length = strlen($num); if ($length == 15) { //如果是15位身份证 //15位身份证没 ...

  4. Keil MDK5编译时出现错误:error: L6002U: Could not open file …\obj\main.o: No such file or directory,

    原因:电脑系统用户名中存在中文字符 解决:用户环境变量,找到变量 TEMP 和 TMP,将变量值中的"%USERPROFILE%"使用"C:\user\default&q ...

  5. ideal中热部署JRebal的设置

    1.ideal中安装插件: 2.打开网址:https://www.guidgen.com/   打开链接获取新的GUID码 3.网址和UUID码拼接:http://127.0.0.1:8888/ca3 ...

  6. 2021SWPUCTF-WEB(一)

    gift_F12 给了一个网站,题目提示是F12,就F12找一下 ​ WLLMCTF{We1c0me_t0_WLLMCTF_Th1s_1s_th3_G1ft} jicao 一个代码,逻辑很简单 ​ 大 ...

  7. linux使用iperf3测试带宽

    1. https://www.alibabacloud.com/help/zh/express-connect/latest/test-the-performance-of-an-express-co ...

  8. Django中间件的介绍及使用

    1.中间件的理解: 是用来处理Django请求与响应的框架级别的钩子,处于wsgi模块与视图函数之间,在执行视图函数之前和之后所做      的动作,是一个轻量级.低级别的插件,作用于全局,使用不当很 ...

  9. SQL无法解决 equal to 运算中 "Chinese_PRC_CI_AS" 和 "SQL_Latin1_General_CP1_CI_AS" 之间的排序规则冲突

    在所在的SQL语句后面加上 COLLATE [排序规则]或者ALTER DATABASE [DBName] COLLATE Chinese_PRC_CI_AS

  10. 网络基础-分层思想和TCP/TP协议族

    一 .分层思想 首先,什么是分层?1984年国际标准化组织(iso)颁布了开放系统互联(osi)参考模型:一个开放式体系结构,将网络分成七层. 分层 功能 应用层 网络服务与最终用户的一个接口 表示层 ...