用Abp实现两步验证(Two-Factor Authentication,2FA)登录(三):免登录验证

这篇具有很好参考价值的文章主要介绍了用Abp实现两步验证(Two-Factor Authentication,2FA)登录(三):免登录验证。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

@

目录
  • 原理
  • 修改请求报文
  • 配置JwtBearerOptions
  • 生成Token
  • 校验Token
  • 修改认证EndPoint
  • 修改前端
    • 登录
    • 登出
  • 最终效果
  • 项目地址

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

常见的网页上提示“7天免登录验证”或“信任此设备,7天内无需两步验证”等内容。
这样可以提高用户的体验。但同时也会带来一定的安全风险,因此需要用户自己决定是否开启。
用Abp实现两步验证(Two-Factor Authentication,2FA)登录(三):免登录验证

原理

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

用Abp实现两步验证(Two-Factor Authentication,2FA)登录(三):免登录验证

为了安全,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就会失效。

最终效果

用Abp实现两步验证(Two-Factor Authentication,2FA)登录(三):免登录验证

项目地址

Github:matoapp-samples文章来源地址https://www.toymoban.com/news/detail-411702.html

到了这里,关于用Abp实现两步验证(Two-Factor Authentication,2FA)登录(三):免登录验证的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 基于TOTP算法的Github两步验证2FA(双因子)机制Python3.10实现

    从今年(2023)三月份开始,Github开始强制用户开启两步验证2FA(双因子)登录验证,毫无疑问,是出于安全层面的考虑,毕竟Github账号一旦被盗,所有代码仓库都会毁于一旦,关于双因子登录的必要性请参见:别让你的服务器(vps)沦为肉鸡(ssh暴力破解),密钥验证、双向因子登录值

    2024年02月08日
    浏览(36)
  • 两步轻松实现ChatGPT联网

            在谷歌应用商店网站下载 “webChatGPT” 插件         Chrome 应用商店 - webchatgpt (google.com)         登录ChatGPT会发现搜索栏新增了很多东西。         Web access按钮打开:使得ChatGPT可以联网搜索。         3results:代表GPT只会展现3条符合的信息。        

    2023年04月11日
    浏览(27)
  • 【 AI 两步实现文本 转 语音】

    基于hugging face 中 XTTS-v2 模型做文本转语音,此模型支持17种语言 https://huggingface.curated.co/ 或者 https://hf-mirror.com/models 找到models处下载XTTS-V2 如果你全程可以联网(/huggingface.co)直接步骤2 搜索模型XTTS-V2,选Files and Versions下载所有文件,其中红框选出来的是训练好的模型 如果不

    2024年02月19日
    浏览(29)
  • 企业如何两步实现数据资产化?

    企业完成建设数据存储和算力基础平台后,再将数据资源归集,下一步就需要将数据资源转化为数据资产。那么什么样的数据资源是数据资产?企业数据管理者需要提升数据质量、消除数据孤岛,并逐步积累数据价值?本篇将从数据平台建设和团队建设两个角度来介绍如何实

    2024年02月05日
    浏览(32)
  • 两步实现Pandas合并相同索引行的秘籍

    在Pandas处理数据的过程中,我们常常会遇到需要对相同索引行进行汇总和统计的情况。那么如何高效地实现DataFrame相同索引行的合并呢? 在Pandas中,可以使用.groupby()和.agg()方法合并相同索引行。 例如,有这样一张DataFrame: 要合并2020-01-01这天的行,可以这样操作: 此时categ

    2024年02月06日
    浏览(28)
  • 【Python Flask+Nginx】实现HTTP、WS (两步实现,简单易懂)

    目录 一、创建Flask应用  二、部署Nginx 2.1 下载Nginx  2.2 修改Nginx配置文件 2.3 启动Nginx 三、测试         首先我写了如下一个基于Flask的Demo,该Demo包含两个接口一个是 HTTP 接口(http://127.0.0.1:5000),一个是 Websocket 接口(ws://127.0.0.1:5000/test) 如果调用HTTP接口,会返回一个

    2024年02月11日
    浏览(22)
  • vue 两步实现点击导航栏,滚动页面到指定位置的功能

    当编写好html部分后,我们创建一个可以获取当前滚轮位置的方法handleScroll(),并在mounted钩子函数中添加该方法的监听事件 创建好监听事件后,我们手动滑动网页右侧滚动条到各版块位置,并打印当前板块的滚轮高度 1、具体思路   ① 获取到当前滚轮位置后,计算其位置与

    2024年02月08日
    浏览(27)
  • 用Abp实现找回密码和密码强制过期策略

    @ 目录 重置密码 找回密码 发送验证码 校验验证码 发送重置密码链接 创建接口 密码强制过期策略 改写接口 Vue网页端开发 重置密码页面 忘记密码控件 密码过期提示 项目地址 用户找回密码,确切地说是 重置密码 ,为了保证用户账号安全,原始密码将不再以明文的方式找回

    2023年04月14日
    浏览(23)
  • Ubuntu的SSH安全配置,查看SSH登录日志文件,修改默认端口,UFW配置防火墙,禁止root用户登录,禁用密码登陆,使用RSA私钥登录,使用 Fail2ban 工具,使用两步验证(2FA)

    环境是Ubuntu 22.04 LTS 不出意外会看到很多类似如下的日志 然后可以统计有多少人在暴力破解root密码错误登录,展示错误次数和ip 因为腾讯云还有个默认用户Ubuntu,也可以一起看看,或是查看一下自己其他用户的错误登录 统计有多少暴力猜用户名的 这台才买回来3天就被扫了

    2024年02月14日
    浏览(41)
  • abp(net core)+easyui+efcore实现仓储管理系统——模块管理升级(六十)

    abp(net core)+easyui+efcore实现仓储管理系统——ABP总体介绍(一) abp(net core)+easyui+efcore实现仓储管理系统——解决方案介绍(二) abp(net core)+easyui+efcore实现仓储管理系统——领域层创建实体(三)   abp(net core)+easyui+efcore实现仓储管理系统——定义仓储并实现 (四) abp(net core)+easyui+efcore实

    2023年04月09日
    浏览(40)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包