@
目录原理修改请求报文配置JwtBearerOptions生成Token校验Token修改认证EndPoint修改前端登录登出最终效果项目地址免登录验证是用户在首次两步验证通过后,在常用的设备(浏览器)中,在一定时间内不需要再次输入验证码直接登录。
常见的网页上提示“7天免登录验证”或“信任此设备,7天内无需两步验证”等内容。这样可以提高用户的体验。但同时也会带来一定的安全风险,因此需要用户自己决定是否开启。
【资料图】
常用的实现方式是在用户登录成功后,生成一个随机的字符串Token,将此Token保存在用户浏览器的 cookie 中,同时将这个字符串保存在用户的数据库中。当用户再次访问时,如果 cookie 中的字符串和数据库中的字符串相同,则免登录验证通过。流程图如下:
为了安全,Token采用对称加密传输存储,同时参与校验的还有用户Id,以进一步验证数据一致性。Token存储于数据库中并设置过期时间(ExpireDate)认证机制由JSON Web Token(JWT)实现,通过自定义Payload声明中添加Token和用户Id字段,实现校验。
下面来看代码实现:
修改请求报文项目添加对Microsoft.AspNetCore.Authentication.JwtBearer包的引用
在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 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(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 TwoFactorClientRememberedAsync(UserIdentifier userIdentifier, string TwoFactorRememberClientToken, TokenValidationParameters validationParameters){ if (!await settingManager.GetSettingValueAsync(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 IsTwoFactorAuthRequiredAsync(AbpLoginResult loginResult, string TwoFactorRememberClientToken, TokenValidationParameters validationParameters){ if (!await settingManager.GetSettingValueAsync(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 IsTwoFactorAuthRequiredAsync(AbpLoginResult loginResult, string TwoFactorRememberClientToken, TokenValidationParameters validationParameters) { if (!await settingManager.GetSettingValueAsync(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 TwoFactorClientRememberedAsync(UserIdentifier userIdentifier, string TwoFactorRememberClientToken, TokenValidationParameters validationParameters) { if (!await settingManager.GetSettingValueAsync(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 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(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 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,用于选择是否记住客户端
7天内不再要求两步验证 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
X 关闭
- 1、用Abp实现两步验证(Two-Factor Authentication,2FA)登录(三):免登录验证-精彩看点
- 2、天天亮点!算力需求强劲!CPO概念股、通信设备商、运营商齐涨逻辑在哪
- 3、【全球新视野】民意传声筒 | 凝心聚力解难题 “六步一哨”暖民心
- 4、热门:弟弟打工供2个姐姐4年大学生活费,二姐回应:他每月赚7000,给我和大姐每人1500,小时候误会了爸妈偏心
- 5、有风来丨巴西总统来了,不谈足球谈吃的
- 6、天天通讯!学校统一买的100元保险住院能报销吗?具体报销比例是多少?
- 7、最新:设置新密码6一16位_设置新密码
- 8、华大基因:技术创新到国际化升级,打造精准医学行业样本 每日信息
- 9、孕妇早期吃什么最好最有营养_孕妇早期吃什么最好
- 10、涉垄断两年收三罚单 津药药业被罚超1亿
-
包头稀土高新区一机宏远电器试验检测中心跻身国家认可实验室行列
近日,一机集团宏远电器公司试验检测中心通过中国合格评定委员会及国防科技工业实验室认可委员会评定,取得中国合格评定委员会实验室认可证书
-
世界热门:海清厉害了!能不能轻松穿出高级感?简直就是一本活生生的穿衣造型教科书
说到海清,我们总能想到《媳妇的美好时代》里个性鲜明的小媳妇毛豆豆。她的精湛演技令人印象深刻,被称为“国民”儿媳,成为了儿媳的职业管家
-
沈阳:三孩家庭每月发500元,至孩子3岁止
据沈阳市卫健委网站4月11日消息,沈阳市卫生健康委员会、沈阳市财政局日前印发《沈阳市发放三孩育儿补贴实施方案(试行)》
-
马龙新乡冠军赛次轮出局 林昀儒这场赢在准备更充分-世界快报
世界排名第四位的马龙以1比3不敌中国台北新锐林昀儒,成为国乒在本站比赛中首位出局的选手。11日晚,马龙在第二轮比赛中迎来了与林昀儒的第七
-
环球视讯!软银CEO孙正义料将签署Arm纳斯达克上市协议
据报道,软银集团首席执行官孙正义拟签署协议,让芯片设计公司Arm最早在今年秋季在纳斯达克上市。投资者预计该公司上市的估值在300亿美元到700
-
天天信息:韩国4月前10天芯片出口下降近40%:1TB SSD三星降599没人买 国产崛起
韩国4月前10天芯片出口下降近40%:1TBSSD三星降599没人买国产崛起
-
无规矩不成方圆|天天报资讯
无规矩不成方圆。众神参战,GPT盲眼狂奔。
-
4月11日基金净值:国投中证500指数量化增强A最新净值2.0981,跌0.1%
4月11日,国投中证500指数量化增强A最新单位净值为2 0981元,累计净值为2 0981元,较前一交易日下跌0 1%。历史数据显示该基金近1个月上涨1 72%
-
最新消息:修丽可AGE面霜怎么样,效果真实评测
修丽可AGE面霜怎么样,效果真实评测【买前必看】
-
真三国无双5psp版攻略_psp真三国无双5攻略
1、我们进入游戏之后。2、打开修改器就可以修改真三国无双。以上就是【真三国无双5psp版攻略,psp真三国无双5攻略】相

