当前位置: 首页 > news >正文

9.SpringSecurity核心过滤器-SecurityContextPersistenceFilter

SpringSecurity核心过滤器-SecurityContextPersistenceFilter

一、SpringSecurity中的核心组件

  在SpringSecurity中的jar分为4个,作用分别为

jar作用
spring-security-coreSpringSecurity的核心jar包,认证和授权的核心代码都在这里面
spring-security-config如果使用Spring Security XML名称空间进行配置或Spring Security的<br />Java configuration支持,则需要它
spring-security-web用于Spring Security web身份验证服务和基于url的访问控制
spring-security-test测试单元

1.SecurityContextHolder

  首先来看看在spring-security-core中的SecurityContextHolder,这个是一个非常基础的对象,存储了当前应用的上下文SecurityContext,而在SecurityContext可以获取Authentication对象。也就是当前认证的相关信息会存储在Authentication对象中。

image.png

  默认情况下,SecurityContextHolder是通过 ThreadLocal来存储对应的信息的。也就是在一个线程中我们可以通过这种方式来获取当前登录的用户的相关信息。而在SecurityContext中就只提供了对Authentication对象操作的方法

public interface SecurityContext extends Serializable {Authentication getAuthentication();void setAuthentication(Authentication authentication);}

xxxStrategy的各种实现

image.png

策略实现说明
GlobalSecurityContextHolderStrategy把SecurityContext存储为static变量
InheritableThreadLocalSecurityContextStrategy把SecurityContext存储在InheritableThreadLocal中<br />InheritableThreadLocal解决父线程生成的变量传递到子线程中进行使用
ThreadLocalSecurityContextStrategy把SecurityContext存储在ThreadLocal中

2.Authentication

  Authentication是一个认证对象。在Authentication接口中声明了如下的相关方法。

public interface Authentication extends Principal, Serializable {// 获取认证用户拥有的对应的权限Collection<? extends GrantedAuthority> getAuthorities();// 获取哦凭证Object getCredentials();// 存储有关身份验证请求的其他详细信息。这些可能是 IP地址、证书编号等Object getDetails();// 获取用户信息 通常是 UserDetails 对象Object getPrincipal();// 是否认证boolean isAuthenticated();// 设置认证状态void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;}

image.png

  基于上面讲解的三者的关系我们在项目中如此来获取当前登录的用户信息了。

    public String hello(){Authentication authentication = SecurityContextHolder.getContext().getAuthentication();Object principal = authentication.getPrincipal();if(principal instanceof UserDetails){UserDetails userDetails = (UserDetails) principal;System.out.println(userDetails.getUsername());return "当前登录的账号是:" + userDetails.getUsername();}return "当前登录的账号-->" + principal.toString();}

  调用 getContext()返回的对象是 SecurityContext接口的一个实例,这个对象就是保存在线程中的。接下来将看到,Spring Security中的认证大都返回一个 UserDetails的实例作为principa。

3.UserDetailsService

  在上面的关系中我们看到在Authentication中存储当前登录用户的是Principal对象,而通常情况下Principal对象可以转换为UserDetails对象。UserDetails是Spring Security中的一个核心接口。它表示一个principal,但是是可扩展的、特定于应用的。可以认为 UserDetails是数据库中用户表记录和Spring Security在 SecurityContextHolder中所必须信息的适配器。

public interface UserDetails extends Serializable {// 对应的权限Collection<? extends GrantedAuthority> getAuthorities();// 密码String getPassword();// 账号String getUsername();// 账号是否过期boolean isAccountNonExpired();// 是否锁定boolean isAccountNonLocked();// 凭证是否过期boolean isCredentialsNonExpired();// 账号是否可用boolean isEnabled();}

  而这个接口的默认实现就是 User

image.png

  那么这个UserDetails对象什么时候提供呢?其实在我们前面介绍的数据库认证的Service中我们就用到了,有一个特殊接口 UserDetailsService,在这个接口中定义了一个loadUserByUsername的方法,接收一个用户名,来实现根据账号的查询操作,返回的是一个 UserDetails对象。

public interface UserDetailsService {UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;}

  UserDetailsService接口的实现有如下:

image.png

  Spring Security提供了许多 UserDetailsSerivice接口的实现,包括使用内存中map的实现(InMemoryDaoImpl 低版本 InMemoryUserDetailsManager)和使用JDBC的实现(JdbcDaoImpl)。但在实际开发中我们更喜欢自己来编写,比如UserServiceImpl我们的案例

/*** 用户的Service*/
public interface UserService extends UserDetailsService {}/*** UserService接口的实现类*/
@Service
public class UserServiceImpl implements UserService {@AutowiredUserMapper userMapper;/*** 根据账号密码验证的方法* @param username* @return* @throws UsernameNotFoundException*/@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {SysUser user = userMapper.queryByUserName(username);System.out.println("---------->"+user);if(user != null){// 账号对应的权限List<SimpleGrantedAuthority> authorities = new ArrayList<>();authorities.add(new SimpleGrantedAuthority("ROLE_USER"));// 说明账号存在 {noop} 非加密的使用UserDetails details = new User(user.getUserName(),user.getPassword(),true,true,true,true,authorities);return details;}throw new UsernameNotFoundException("账号不存在...");}
}

4.GrantedAuthority

  我们在Authentication中看到不光关联了Principal还提供了一个getAuthorities()方法来获取对应的GrantedAuthority对象数组。和权限相关,后面在权限模块详细讲解

public interface GrantedAuthority extends Serializable {String getAuthority();}

上面介绍到的核心对象小结:

核心对象作用
SecurityContextHolder用于获取SecurityContext
SecurityContext存放了Authentication和特定于请求的安全信息
Authentication特定于Spring Security的principal
GrantedAuthority对某个principal的应用范围内的授权许可
UserDetail提供从应用程序的DAO或其他安全数据源构建Authentication对象所需的信息
UserDetailsService接受String类型的用户名,创建并返回UserDetail

而这块和SpringSecurity的运行关联我们就需要来看看SecurityContextPersistenceFilter的作用了

二、SecurityContextPersistenceFilter

  首先在Session中维护一个用户的安全信息就是这个过滤器处理的。从request中获取session,从Session中取出已认证用户的信息保存在SecurityContext中,提高效率,避免每一次请求都要解析用户认证信息,方便接下来的filter直接获取当前的用户信息。

1.SecutiryContextRepository

  SecutiryContextRepository接口非常简单,定义了对SecurityContext的存储操作,在该接口中定义了如下的几个方法

public interface SecurityContextRepository {/*** 获取SecurityContext对象*/SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder);/*** 存储SecurityContext*/void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response);/*** 判断是否存在SecurityContext对象*/boolean containsContext(HttpServletRequest request);}

  默认的实现是HttpSessionSecurityContextRepository。也就是把SecurityContext存储在了HttpSession中。对应的抽象方法实现如下:

  先来看看loadContext方法

	public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {// 获取对有的Request和Response对象HttpServletRequest request = requestResponseHolder.getRequest();HttpServletResponse response = requestResponseHolder.getResponse();// 获取HttpSession对象HttpSession httpSession = request.getSession(false);// 从HttpSession中获取SecurityContext对象SecurityContext context = readSecurityContextFromSession(httpSession);if (context == null) {// 如果HttpSession中不存在SecurityContext对象就创建一个// SecurityContextHolder.createEmptyContext();// 默认是ThreadLocalSecurityContextHolderStrategy存储在本地线程中context = generateNewContext();if (this.logger.isTraceEnabled()) {this.logger.trace(LogMessage.format("Created %s", context));}}// 包装Request和Response对象SaveToSessionResponseWrapper wrappedResponse = new SaveToSessionResponseWrapper(response, request,httpSession != null, context);requestResponseHolder.setResponse(wrappedResponse);requestResponseHolder.setRequest(new SaveToSessionRequestWrapper(request, wrappedResponse));return context;}

  然后再来看看saveContext方法。

	@Overridepublic void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {SaveContextOnUpdateOrErrorResponseWrapper responseWrapper = WebUtils.getNativeResponse(response,SaveContextOnUpdateOrErrorResponseWrapper.class);Assert.state(responseWrapper != null, () -> "Cannot invoke saveContext on response " + response+ ". You must use the HttpRequestResponseHolder.response after invoking loadContext");responseWrapper.saveContext(context);}

继续进入

@Overrideprotected void saveContext(SecurityContext context) {// 获取Authentication对象final Authentication authentication = context.getAuthentication();// 获取HttpSession对象HttpSession httpSession = this.request.getSession(false);// String springSecurityContextKey = HttpSessionSecurityContextRepository.this.springSecurityContextKey;// See SEC-776if (authentication == null|| HttpSessionSecurityContextRepository.this.trustResolver.isAnonymous(authentication)) {if (httpSession != null && this.authBeforeExecution != null) {// SEC-1587 A non-anonymous context may still be in the session// SEC-1735 remove if the contextBeforeExecution was not anonymoushttpSession.removeAttribute(springSecurityContextKey);this.isSaveContextInvoked = true;}if (this.logger.isDebugEnabled()) {if (authentication == null) {this.logger.debug("Did not store empty SecurityContext");}else {this.logger.debug("Did not store anonymous SecurityContext");}}return;}httpSession = (httpSession != null) ? httpSession : createNewSessionIfAllowed(context, authentication);// If HttpSession exists, store current SecurityContext but only if it has// actually changed in this thread (see SEC-37, SEC-1307, SEC-1528)if (httpSession != null) {// We may have a new session, so check also whether the context attribute// is set SEC-1561if (contextChanged(context) || httpSession.getAttribute(springSecurityContextKey) == null) {// HttpSession 中存储SecurityContexthttpSession.setAttribute(springSecurityContextKey, context);this.isSaveContextInvoked = true;if (this.logger.isDebugEnabled()) {this.logger.debug(LogMessage.format("Stored %s to HttpSession [%s]", context, httpSession));}}}}

最后就是containsContext方法

	@Overridepublic boolean containsContext(HttpServletRequest request) {// 获取HttpSessionHttpSession session = request.getSession(false);if (session == null) {return false;}// 从session中能获取就返回true否则falsereturn session.getAttribute(this.springSecurityContextKey) != null;}

2.SecurityContextPersistenceFilter

  然后我们来看看SecurityContextPersistenceFilter的具体处理逻辑

	private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)throws IOException, ServletException {// 同一个请求之处理一次if (request.getAttribute(FILTER_APPLIED) != null) {chain.doFilter(request, response);return;}// 更新状态request.setAttribute(FILTER_APPLIED, Boolean.TRUE);// 是否提前创建 HttpSessionif (this.forceEagerSessionCreation) {// 创建HttpSessionHttpSession session = request.getSession();if (this.logger.isDebugEnabled() && session.isNew()) {this.logger.debug(LogMessage.format("Created session %s eagerly", session.getId()));}}// 把Request和Response对象封装为HttpRequestResponseHolder对象HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);// 获取SecurityContext对象SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);try {// SecurityContextHolder绑定SecurityContext对象SecurityContextHolder.setContext(contextBeforeChainExecution);if (contextBeforeChainExecution.getAuthentication() == null) {logger.debug("Set SecurityContextHolder to empty SecurityContext");}else {if (this.logger.isDebugEnabled()) {this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", contextBeforeChainExecution));}}// 结束交给下一个过滤器处理chain.doFilter(holder.getRequest(), holder.getResponse());}finally {// 当其他过滤器都处理完成后SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();// 移除SecurityContextHolder中的SecuritySecurityContextHolder.clearContext();// 把this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());// 存储Context在HttpSession中request.removeAttribute(FILTER_APPLIED);this.logger.debug("Cleared SecurityContextHolder to complete request");}}

  通过上面的代码逻辑其实我们就清楚了在SpringSecurity中的认证信息的流转方式了。首先用户的认证状态Authentication是存储在SecurityContext中的,而每个用户的SecurityContext是统一存储在HttpSession中的。一次请求流转中我们需要获取当前的认证信息是通过SecurityContextHolder来获取的,默认是在ThreadLocal中存储的。

image.png

相关文章:

9.SpringSecurity核心过滤器-SecurityContextPersistenceFilter

SpringSecurity核心过滤器-SecurityContextPersistenceFilter 一、SpringSecurity中的核心组件 在SpringSecurity中的jar分为4个&#xff0c;作用分别为 jar作用spring-security-coreSpringSecurity的核心jar包&#xff0c;认证和授权的核心代码都在这里面spring-security-co…...

23种设计模式-桥接模式

概念 桥接模式是一种结构型设计模式&#xff0c;它通过将抽象与其实现分离来解耦。它使用接口&#xff08;抽象类&#xff09;作为桥梁&#xff0c;将一个抽象类与其实现类的代码分别独立开来&#xff0c;从而使它们可以各自独立地变化。桥接模式的核心思想是“组合优于继承”…...

TCP PMTU 静态路由

HTTP协议 --- 超文本传输协议TCP --- 80端口超文本 --- 包含有超链接link和多媒体元素标记的文本TCP协议是一种面向连接的可靠性传输协议面向连接&#xff1a;数据在传输前&#xff0c;收发双方建立一条逻辑通道。可靠性确认机制&#xff1a;传输确认&#xff0c;每接受一个数据…...

Android动画——属性动画

在属性动画中&#xff0c;常用到的API有ValueAnimator&#xff0c;ObjectAnimator。ValueAnimator&#xff1a;时间引擎&#xff0c;负责计算各个帧的属性值&#xff0c;基本上其他属性动画都会直接或间接继承它&#xff1b;ObjectAnimator&#xff1a; ValueAnimator 的子类&a…...

华为OD机试真题Python实现【寻找连续区间】真题+解题思路+代码(20222023)

寻找连续区间 题目 给定一个含有N个正整数的数组, 求出有多少个连续区间(包括单个正整数), 它们的和大于等于x。 🔥🔥🔥🔥🔥👉👉👉👉👉👉 华为OD机试(Python)真题目录汇总 ## 输入 第一行两个整数N x (0 < N <= 100000 ,0 <= x <=…...

15. 三数之和

给你一个整数数组 nums &#xff0c;判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i ! j、i ! k 且 j ! k &#xff0c;同时还满足 nums[i] nums[j] nums[k] 0 。请 你返回所有和为 0 且不重复的三元组。 注意&#xff1a;答案中不可以包含重复的三元组。 示例 …...

40-Golang中的文件

Golang中的文件基本介绍文件的打开和关闭读文件操作应用实例写文件操作实例判断文件是否存在基本介绍 文件在程序中是以流的形式存在的 流&#xff1a;数据在数据源(文件)和程序(内存)之间经历的路程 输入流&#xff1a;数据从数据源到程序之间的路径 输出流&#xff1a;数据…...

Springboot整合RabbitMQ并使用

1、Springboot整合RabbitMQ 1、引入场景启动器 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-amqp</artifactId> </dependency>引入AMQP场景启动器之后&#xff0c;RabbitAutoConfiguratio…...

Java中方法引用(引用静态方法、引用成员方法(引用其他类的成员方法、引用本类的成员方法、引用父类的成员方法)、引用构造方法、其他调用方式、小练习)

方法引用&#xff1a;把已经存在的方法拿过来用&#xff0c;当作函数式接口中抽象方法的方法体 我们前面学到Arrays工具类中的sort方法&#xff0c;当我们需要指定排序规则时&#xff0c;需要传递Comparator接口的实现类对象&#xff0c;我们之前使用匿名内部类类的形式作为参…...

整理了100道关于Python基础知识的练习题,记得收藏~

实例1.数字组合 题目&#xff1a; 有四个数字&#xff1a;1、2、3、4&#xff0c;能组成多少个互不相同且无重复数字的三位数&#xff1f;各是多少&#xff1f; 程序分析&#xff1a; 遍历全部可能&#xff0c;把有重复的剃掉。 total0 for i in range(1,5):for j in range(…...

OSG三维渲染引擎编程学习之七十七:“第七章:OSG场景图形交互” 之 “7.8 场景交互”

目录 第七章 OSG场景图形交互 7.8 场景交互 7.8.1 osgGA库 7.8.2 事件消息处理 7.8.3 程序抓图示例...

797.差分

输入一个长度为 n的整数序列。 接下来输入 m个操作&#xff0c;每个操作包含三个整数 l,r,c&#xff0c;表示将序列中 [l,r] 之间的每个数加上 c。 请你输出进行完所有操作后的序列。 输入格式 第一行包含两个整数 n和 m。 第二行包含 n个整数&#xff0c;表示整数序列。 …...

为什么说要慎用BeanUtils,因为性能真的拉跨

1 背景之前在专栏中讲过“不推荐使用属性拷贝工具”&#xff0c;推荐直接定义转换类和方法使用 IDEA 插件自动填充 get / set 函数。不推荐的主要理由是&#xff1a;有些属性拷贝工具性能有点差有些属性拷贝工具有“BUG”使用属性拷贝工具容易存在一些隐患&#xff08;后面例子…...

【项目设计】高并发内存池(六)[细节优化+测试]

&#x1f387;C学习历程&#xff1a;入门 博客主页&#xff1a;一起去看日落吗持续分享博主的C学习历程博主的能力有限&#xff0c;出现错误希望大家不吝赐教分享给大家一句我很喜欢的话&#xff1a; 也许你现在做的事情&#xff0c;暂时看不到成果&#xff0c;但不要忘记&…...

同模块设置不同应用主题方案

有时候公司内部会有不同应用但是有部分模块功能一样&#xff0c;只根据应用角色有些细节逻辑区分的场景。这时候往往采用模块化采用以应用至不同的APP。如果APP主题不一致&#xff0c;该如果解决。 方案&#xff1a; 在不同应用的config.gradle 下面根据不同应用定义不同的a…...

centos7 安装 hyperf

​​​​​​PHP > 7.4 Swoole PHP 扩展 > 4.5&#xff0c;并关闭了 Short Name OpenSSL PHP 扩展 JSON PHP 扩展 PDO PHP 扩展 Redis PHP 扩展 Protobuf PHP 扩展 composer create-project hyperf/hyperf-skeleton 推荐安装项 全部选n php.ini [swoole] extens…...

RZ/G2UL核心板-40℃低温启动测试

1. 测试对象HD-G2UL-EVM基于HD-G2UL-CORE工业级核心板设计&#xff0c;一路千兆网口、一路CAN-bus、 3路TTL UART、LCD、WiFi、CSI 摄像头接口等&#xff0c;接口丰富&#xff0c;适用于工业现场应用需求&#xff0c;亦方便用户评估核心板及CPU的性能。HD-G2UL-CORE系列工业级核…...

PyQt5可视化 7 饼图和柱状图实操案例 ①Qt项目的创建

目录 一、新建Qt项目 二、添加组件和布局 三、添加资源 1. 新建资源文件 2. 添加图标资源 四、frameHead 1. toolBtnGenData 2. toolBtnCounting 3. comboTheme 4. comboAnimation 5. Horizontal Spacer 6. toolBtnQuit 7. 设置toolBtnQuit的功能 8. frameHead的…...

0104路径搜索和单点路径-无向图-数据结构和算法(Java)

文章目录2 单点路径2.1 API2.2 算法实现后记2 单点路径 单点路径。给定一幅图和一个起点s&#xff0c;回答“从s到给定的目的顶点v是否存在一条路径&#xff1f;如果有&#xff0c;找出这条路径。”等类似问题。 2.1 API 单点路径问题在图的处理邻域中十分重要。根据标准设计…...

Maxscale读写分离实施文档

Maxscale介绍 MaxScale是maridb开发的一个mysql数据中间件&#xff0c;其配置简单&#xff0c;能够实现读写分离&#xff0c;并且可以根据主从状态实现写库的自动切换。 使用Maxscale无需对业务代码进行修改&#xff0c;其自带的读写分离模块&#xff0c;能够解析SQL语句&…...

day52 ResNet18 CBAM

在深度学习的旅程中&#xff0c;我们不断探索如何提升模型的性能。今天&#xff0c;我将分享我在 ResNet18 模型中插入 CBAM&#xff08;Convolutional Block Attention Module&#xff09;模块&#xff0c;并采用分阶段微调策略的实践过程。通过这个过程&#xff0c;我不仅提升…...

电脑插入多块移动硬盘后经常出现卡顿和蓝屏

当电脑在插入多块移动硬盘后频繁出现卡顿和蓝屏问题时&#xff0c;可能涉及硬件资源冲突、驱动兼容性、供电不足或系统设置等多方面原因。以下是逐步排查和解决方案&#xff1a; 1. 检查电源供电问题 问题原因&#xff1a;多块移动硬盘同时运行可能导致USB接口供电不足&#x…...

Python实现prophet 理论及参数优化

文章目录 Prophet理论及模型参数介绍Python代码完整实现prophet 添加外部数据进行模型优化 之前初步学习prophet的时候&#xff0c;写过一篇简单实现&#xff0c;后期随着对该模型的深入研究&#xff0c;本次记录涉及到prophet 的公式以及参数调优&#xff0c;从公式可以更直观…...

如何在看板中有效管理突发紧急任务

在看板中有效管理突发紧急任务需要&#xff1a;设立专门的紧急任务通道、重新调整任务优先级、保持适度的WIP&#xff08;Work-in-Progress&#xff09;弹性、优化任务处理流程、提高团队应对突发情况的敏捷性。其中&#xff0c;设立专门的紧急任务通道尤为重要&#xff0c;这能…...

C++ 求圆面积的程序(Program to find area of a circle)

给定半径r&#xff0c;求圆的面积。圆的面积应精确到小数点后5位。 例子&#xff1a; 输入&#xff1a;r 5 输出&#xff1a;78.53982 解释&#xff1a;由于面积 PI * r * r 3.14159265358979323846 * 5 * 5 78.53982&#xff0c;因为我们只保留小数点后 5 位数字。 输…...

【C++从零实现Json-Rpc框架】第六弹 —— 服务端模块划分

一、项目背景回顾 前五弹完成了Json-Rpc协议解析、请求处理、客户端调用等基础模块搭建。 本弹重点聚焦于服务端的模块划分与架构设计&#xff0c;提升代码结构的可维护性与扩展性。 二、服务端模块设计目标 高内聚低耦合&#xff1a;各模块职责清晰&#xff0c;便于独立开发…...

Fabric V2.5 通用溯源系统——增加图片上传与下载功能

fabric-trace项目在发布一年后,部署量已突破1000次,为支持更多场景,现新增支持图片信息上链,本文对图片上传、下载功能代码进行梳理,包含智能合约、后端、前端部分。 一、智能合约修改 为了增加图片信息上链溯源,需要对底层数据结构进行修改,在此对智能合约中的农产品数…...

【笔记】WSL 中 Rust 安装与测试完整记录

#工作记录 WSL 中 Rust 安装与测试完整记录 1. 运行环境 系统&#xff1a;Ubuntu 24.04 LTS (WSL2)架构&#xff1a;x86_64 (GNU/Linux)Rust 版本&#xff1a;rustc 1.87.0 (2025-05-09)Cargo 版本&#xff1a;cargo 1.87.0 (2025-05-06) 2. 安装 Rust 2.1 使用 Rust 官方安…...

【学习笔记】erase 删除顺序迭代器后迭代器失效的解决方案

目录 使用 erase 返回值继续迭代使用索引进行遍历 我们知道类似 vector 的顺序迭代器被删除后&#xff0c;迭代器会失效&#xff0c;因为顺序迭代器在内存中是连续存储的&#xff0c;元素删除后&#xff0c;后续元素会前移。 但一些场景中&#xff0c;我们又需要在执行删除操作…...

探索Selenium:自动化测试的神奇钥匙

目录 一、Selenium 是什么1.1 定义与概念1.2 发展历程1.3 功能概述 二、Selenium 工作原理剖析2.1 架构组成2.2 工作流程2.3 通信机制 三、Selenium 的优势3.1 跨浏览器与平台支持3.2 丰富的语言支持3.3 强大的社区支持 四、Selenium 的应用场景4.1 Web 应用自动化测试4.2 数据…...