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

news2024/11/15 13:28:59

文章目录

    • 原理
    • 修改请求报文
    • 配置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包的引用

<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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/431080.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Go 语言 入门 基于 GoLand 2023.1 创建第一个Go程序

文章目录Go 语言 入门go 语言是什么?go 入门 需要 学习 什么?Go 语言有 那些优势?Go 语言 与 Java 语言 对比Go 语言 与Python 语言 对比Go 语言 基于 GoLand 创建第一个程序1. 打开 GoLand2.选择 new3. 选择SDK版本4. 输入项目名称,点击 cteate项目创建完成下一步,创建一个…

SpringBoot-心跳机制+redis实现网站实时在线人数统计

在社交网站中&#xff0c;通常需要实时统计某个网站的在线人数&#xff0c;通过该指标来实时帮助运营人员更好的维护网站业务&#xff1a; 先说一下目前在市面上主流的做法再加上我自己查阅的资料总结&#xff1a; 创建一个session监听器&#xff0c;在用户登录时即创建一个s…

WEB集群之反向代理,动静分离,NFS,mysql,MHA高可用

目录 第一章实验架构 1.1.实验图谱架构 1.2.实验前环境部署 第二章实验步骤 2.1.在ha01&#xff0c;ha02上部署keeplived,lvs-dr 2.2.Slave01,slave02安装nginx 2.3.LVS负载均衡 2.4.搭建动态网页 2.5.nginx反向代理 2.6.部署NFS 2.7.安装mysql 2.8.安装mha 2.9.主…

教你安装 CodeWhisperer: 一款个人免费的类似GitHubCopilot能代码补全的 AI 编程助手

1、官网 AI Code Generator - Amazon CodeWhisperer - AWS 官方扩展安装教程 2、安装VSCode 下载安装VSCode 3、VSCode安装CodeWhisperer插件 安装VSCode插件 - AWS Toolkit主侧栏&#xff0c;点击AWS &#xff0c;展开CodeWhisperer&#xff0c;点击Start 在下拉菜单中点…

洗浴中心管理系统【GUI/Swing+MySQL】(Java课设)

系统类型 Swing窗口类型Mysql数据库存储数据 使用范围 适合作为Java课设&#xff01;&#xff01;&#xff01; 部署环境 jdk1.8Mysql8.0Idea或eclipsejdbc 运行效果 本系统源码地址&#xff1a; 更多系统资源库地址&#xff1a;骚戴的博客_CSDN_更多系统资源 更多系统…

煤矿井下人员精准定位系统,煤矿应急救援高效应用

煤矿行业的安全发展&#xff0c;事关数人民群众的生命财产安全。自2020年起&#xff0c;国家连续三年出台煤矿智能化建设及重大风险防控的相关指导政策&#xff0c;其中&#xff0c;2022年出台的《煤矿及重点非煤矿山重大灾害风险防控建设工作总体方案》中提到&#xff0c;要充…

52 openEuler搭建PostgreSQL数据库服务器-管理数据库角色

文章目录52 openEuler搭建PostgreSQL数据库服务器-管理数据库角色52.1 创建角色创建角色示例52.2 查看角色查看角色示例52.3 修改角色52.3.1 修改用户名52.3.2 修改用户示例52.3.3 修改用户密码52.3.4 修改角色密码示例52.4 删除角色删除角色示例52.5 角色授权角色授权示例52.6…

Python旅游好帮手:提前15天准备五一旅游景点详细数据

人生苦短&#xff0c;我用python 虽然还是有15天才放五一的假&#xff0c; 但是我的心早已经在旅游的路上了~ 本文源码&#xff1a;点击此处跳转文末名片获取 趁现在&#xff0c;先来用python做一个旅游攻略 知识点&#xff1a; requests parsel csv 第三方库&#x…

python查看时间序列数据的季节规律matplotlib画时间(10分钟为间隔)序列坐标

目录0 问题描述1. 案例12. 案例2参考资料0 问题描述 将多个时间序列数据&#xff0c;绘制到一张图上&#xff0c;每段时间序列数据一般只有几个月&#xff0c;少则 1 个月左右&#xff0c;想看它们的季节规律&#xff0c;需要去除年份&#xff0c;只看月份。 也就是横轴是1月…

44.CSS Grid布局概述

什么是CSS Grid&#xff1f; ● CSS Grid 是一组用于构建二维布局的 CSS 属性 ● CSS Grid 背后的主要思想是我们将容器元素划分为行和列&#xff0c;这些行和列可以填充是子元素。 ● 在两段式语境中&#xff0c;CSS网格允许我们写出更少的嵌套HTML和更容易阅读的CSS ● CSS …

二分查找原理及使用场景

建议使用左闭右开区间[l, r)查找。二分查找的最后&#xff0c;索引l&#xff0c;r会落到右区间第一个元素位置。因此但凡是能够见数组分成左右两个区间的都能应用二分查找法。 1、普通查值 常见问题方式&#xff1a;寻找含重复值的有序数组 [...,a, tar, tar, tar,.b....]&am…

AI制药 - RCSB PDB 数据集的多维度分析与整理 (1)

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://blog.csdn.net/caroline_wendy/article/details/130089781 整体&#xff1a; RCSB PDB 数据集是一个收集了蛋白质的三维结构信息的数据库&#xff0c;是世界蛋白质数据库&#xff08;…

SQL SERVER调Web Service时候权限错误的解决

日期 2023/4/15 18:00:00 日志 作业历史记录 (AIPACS) 步骤 ID 1 服务器 GOOGLE 作业名称 AIPACS 步骤名称 RUNWS 持续时间 00:00:00 SQL 严重性 16 SQL 消息 ID 15281 已通过电子邮件通知的操作员 已通过…

MATLAB 基于空间格网的点云抽稀 (3)

MATLAB 基于空间格网的点云抽稀 (3) 一、实现效果二、原理步骤三、代码实现四、重点函数与对象的解释说明4.1 indices= pcbin(incloud,[rowNum colNum LayerNum]);4.2 occupancyGrid = cellfun(@(c) ~isempty(c), indices);4.3 outpointIndex = [];4.4 outpointIndex(end+1) …

基于ubuntu18.04上搭建OpenWRT-rtd1619环境

下载OpwnWRT的源码 下载路径&#xff1a;https://gitee.com/yangquan3_admin/rtd1619 您需要以下工具来编译 OpenWrt&#xff0c;包名称因发行版而异。 在 Build System Setup 文档中可以找到包含特定于发行版的软件包的完整列表。 binutils bzip2 diff find flex gawk gcc-6…

【Linux进阶篇】系统网络附加存储

目录 &#x1f341;NFS &#x1f342;软件安装 &#x1f342;服务端配置 &#x1f342;客户端配置 &#x1f342;访问浏览器测试 &#x1f341;iscsi &#x1f342;服务器端安装软件 &#x1f342;服务器端配置iscsi &#x1f342;客户端软件安装配置 &#x1f341;常用的端口号…

这6个免费去水印工具,一定要码住!

现在很多平台会在用户保存图片/视频的时候自动给视频添加一个平台的水印&#xff0c;这在一定程度上影响了它的美观和使用。 下面我来分享几个图片/视频一键去水印方法&#xff0c;操作简单还不会损坏画质哦&#xff01; 1. Magic Eraser 这是一个魔术橡皮擦在线网站&#x…

一文了解API接口自动化测试:让你在人才市场上无往不利

目录&#xff1a;导读 引言 架构 接口测试 API自动化测试 前后端分离的开发模式 测试工作&#xff1a; 协议 网络分层 三次握手的设计(很重要) 问题&#xff1a; URL:统一资源定位符 HTTP协议 &#xff08;重点&#xff09;HTTP的完整请求流程&#xff1a; 通信模…

springboot项目集成JWT实现身份认证(权鉴)

一、什么是JWT JSON Web Token (JWT)&#xff0c;它是目前最流行的跨域身份验证解决方案。现在的项目开发一般都是前端端分离&#xff0c;这就涉及到跨域和权鉴问题。 二、JWT组成 由三部分组成&#xff1a;头部(Header)、载荷(Payload)与签名(signature) 头部&#xff08;Head…

[测试新人必看] 测试报告如何编写? 掌握这五十个测试报告模板

作为一个曾经是测试萌新的我&#xff0c;在首次接收到一个任务时总有一种忐忑慌张激动紧张期望的复杂情绪~~ 忐忑慌张紧张是怕自己做不好&#xff0c;得不到领导的赏识&#xff1b;激动期望是哇塞&#xff0c;我有任务了耶&#xff0c;终于有我的用武之地了~~~ 就好比今天的主题…