课程项目设计--spring security--认证管理功能--宿舍管理系统--springboot后端

写在前面:
还要实习,每次时间好少呀,进度会比较慢一点
本文主要实现是用户管理相关功能。
前文项目建立

文章目录

  • 验证码功能
    • 验证码配置
    • 验证码生成工具类
    • 添加依赖
    • 功能测试
    • 编写controller接口
    • 启动项目
  • security配置
    • 拦截器配置
      • 验证码拦截器
    • jwt拦截器
    • 思考
  • 用户登录
    • jwt管理
    • 验证
  • 用户注销
  • 流程小结
    • 验证码
    • jwt令牌管理
    • 登录
    • 注销

验证码功能

验证码采用的是hutool工具的验证码
hutool官方地址

工具模板采用有来开源组织

验证码配置

yml配置

CaptchaConfig:
  #  验证码缓存过期时间(单位:秒)
  ttl: 120l
  # 验证码内容长度
  length: 4
  # 验证码宽度
  width: 120
  # 验证码高度
  height: 40
  # 验证码字体
  font-name: Verdana
  # 验证码字体大小
  fontSize: 20

配置类

/**
 * EasyCaptcha 配置类
 * 
 * @author haoxr
 * @since 2023/03/24
 */
@ConfigurationProperties(prefix = "easy-captcha")
@Configuration
@Data
public class CaptchaConfig {

    // 验证码类型
    private CaptchaTypeEnum type = CaptchaTypeEnum.ARITHMETIC;

    // 验证码缓存过期时间(单位:秒)
    @Value("${captcha.ttl}")
    private long ttl;

    // 内容长度
    @Value("${captcha.length}")
    private int length;
    // 宽度
    @Value("${captcha.width}")
    private int width;
    // 验证码高度
    @Value("${captcha.height}")
    private int height;

    // 验证码字体
    @Value("${captcha.font-name}")
    private String fontName;

    // 字体风格
    private Integer fontStyle = Font.PLAIN;

    // 字体大小
    @Value("${captcha.font-size}")
    private int fontSize;

}

验证码生成工具类

@Component
@RequiredArgsConstructor
public class EasyCaptchaProducer {
    private final CaptchaConfig captchaConfig;

    public Captcha getCaptcha() {
        Captcha captcha;
        int width = captchaConfig.getWidth();
        int height = captchaConfig.getHeight();
        int length = captchaConfig.getLength();
        String fontName = captchaConfig.getFontName();

        switch (captchaConfig.getType()) {
            case ARITHMETIC -> {
                captcha = new ArithmeticCaptcha(width, height);
                captcha.setLen(2);
            }
            case CHINESE -> {
                captcha = new ChineseCaptcha(width, height);
                captcha.setLen(length);
            }
            case CHINESE_GIF -> {
                captcha = new ChineseGifCaptcha(width, height);
                captcha.setLen(length);
            }
            case GIF -> {
                captcha = new GifCaptcha(width, height);//最后一位是位数
                captcha.setLen(length);
            }
            case SPEC -> {
                captcha = new SpecCaptcha(width, height);
                captcha.setLen(length);
            }
            default -> throw new RuntimeException("验证码配置信息错误!正确配置查看 CaptchaTypeEnum ");
        }
        captcha.setFont(new Font(fontName, captchaConfig.getFontStyle(), captchaConfig.getFontSize()));
        return captcha;
    }


}

添加依赖

        <!-- Java8 之后JavaScript引擎nashorn被移除导致验证码解析报错-->
        <dependency>
            <groupId>org.openjdk.nashorn</groupId>
            <artifactId>nashorn-core</artifactId>
            <version>${nashorn.version}</version>
        </dependency>

功能测试

        Captcha captcha = easyCaptchaProducer.getCaptcha();
        try (OutputStream ops = new FileOutputStream("d://captcha.jpg")) {
            captcha.out(ops);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(captcha.text());

测试结果
在这里插入图片描述
在这里插入图片描述

编写controller接口

@Tag(name = "01-认证中心")
@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
@Slf4j
public class AuthController {

    private final EasyCaptchaService easyCaptchaService;

    @Operation(summary = "获取验证码")
    @GetMapping("/captcha")
    public Result<CaptchaResult> getCaptcha() {
        CaptchaResult captcha = easyCaptchaService.getCaptcha();
        return Result.success(captcha);
    }
}

启动项目

记住这里,这是你spring security 的密码
在这里插入图片描述

生成http

通过base64转图片的在线工具可以看到
在这里插入图片描述
说明编写成功了。

security配置

在上面我们默认的是spring security 自动的密码。我们现在需要自己设置密码。

spring security 框架捏,不太好说这玩意。挺忘记了。
不过spring boot3使用的是spring security6.0版本和以前的有很大差别,6.0通过配置bean来进行。所以也还好,反正都是从头学。
首先需要配置security的配置类

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {


    // 密码编码器
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    /**
     * 不走过滤器链的放行配置
     * 默认放行静态资源、登录接口、验证码接口、Swagger接口文档
     */
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web) -> web.ignoring()
                .requestMatchers(
                        "/auth/captcha",
                        "/webjars/**",
                        "/doc.html",
                        "/swagger-resources/**",
                        "/swagger-ui/**",
                        "/ws/**"
                );
    }
}
    /**
     * 认证管理器
     *
     * @param authenticationConfiguration 认证配置
     * @return 认证管理器
     * @throws Exception 异常
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(requestMatcherRegistry ->
                        requestMatcherRegistry.requestMatchers(SecurityConstants.LOGIN_PATH).permitAll()
                                .anyRequest().authenticated())
                .sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .exceptionHandling(httpSecurityExceptionHandlingConfigurer ->
                        httpSecurityExceptionHandlingConfigurer
                                .authenticationEntryPoint(authenticationEntryPoint)
                                .accessDeniedHandler(accessDeniedHandler))
                .csrf(AbstractHttpConfigurer::disable);

        // 验证码校验过滤器
        http.addFilterBefore(new VerifyCodeFilter(), UsernamePasswordAuthenticationFilter.class);
        // JWT 校验过滤器
        http.addFilterBefore(new JwtAuthenticationFilter(jwtTokenManager), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

这里还用到了2个拦截器

拦截器配置

验证码拦截器

需求:对登录请求进行拦截,如果是登录则需要先校验验证码是否正常,如果正确则放行。其他请求则直接放行。

public class VerifyCodeFilter extends OncePerRequestFilter {
    private static final AntPathRequestMatcher LOGIN_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(SecurityConstants.LOGIN_PATH, "POST");

    public static final String VERIFY_CODE_PARAM_KEY = "verifyCode";
    public static final String VERIFY_CODE_KEY_PARAM_KEY = "verifyCodeKey";
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 如果是登录请求则校验验证码
        if (LOGIN_PATH_REQUEST_MATCHER.matches(request)){
            String code = request.getParameter(VERIFY_CODE_PARAM_KEY);
            String verifyCodeKey = request.getParameter(VERIFY_CODE_KEY_PARAM_KEY);

            // 由于这个不是bean,不能通过注入的方式获取,所以通过SpringUtil工具类获取
            RedisTemplate redisTemplate = SpringUtil.getBean("redisTemplate", RedisTemplate.class);
            String cacheCode =  Convert.toStr(redisTemplate.opsForValue().get(SecurityConstants.VERIFY_CODE_CACHE_PREFIX + verifyCodeKey));
            if (cacheCode == null) {
                // 验证码过期
                ResponseUtils.writeErrMsg(response, ResultCode.VERIFY_CODE_TIMEOUT);
                return;
            }
            if (!StrUtil.equals(cacheCode,code)) {
                // 验证码错误
                ResponseUtils.writeErrMsg(response, ResultCode.VERIFY_CODE_ERROR);
                return;
            }
        }
        filterChain.doFilter(request, response);
    }
}

jwt拦截器

需求:处理登录请求以外的请求,每次需要验证jwt令牌,如果没问题则在该线程请求附加权限身份。

public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private static final AntPathRequestMatcher LOGIN_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(SecurityConstants.LOGIN_PATH, "POST");

    private final JwtTokenManager tokenManager;
    public JwtAuthenticationFilter(JwtTokenManager jwtTokenManager) {
        this.tokenManager = jwtTokenManager;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        if (!LOGIN_PATH_REQUEST_MATCHER.matches(request)) {
            String jwt = RequestUtils.resolveToken(request);
            if (StringUtils.hasText(jwt) && SecurityContextHolder.getContext().getAuthentication() == null) {
                try {
                    Claims claims = this.tokenManager.parseAndValidateToken(jwt);
                    Authentication authentication = this.tokenManager.getAuthentication(claims);
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                } catch (Exception e) {
                    ResponseUtils.writeErrMsg(response, ResultCode.TOKEN_INVALID);
                }
            } else {
                ResponseUtils.writeErrMsg(response, ResultCode.TOKEN_INVALID);
            }
        }
        chain.doFilter(request, response);
    }
}

思考

这2个拦截器一个需要登录一个除去登录,那么是不是可以放到一个拦截器里面去。各走各的。这样也明确一点。也不用2个拦截器找了。

如果改了记得改securityFilterChain

用户登录

需求:输入用户名和密码,验证用户身份。
需要写一个类继承UserDetails
在这里插入图片描述

另一个实现类继承SysUserService(SysUserDetailsService)
在这里插入图片描述
这2个一个是存储一个是查询。然后会自动和输入的username以及password进行比对
验证流程后面总结一个spring security的文。

SysUserDetailsService作用是查询该用户名的角色信息并返回UserDetails。

查询,调用SysUserService根据用户名查询所有的
在这里插入图片描述
由于认证信息需要角色信息和权限所以我们需要联表查询角色信息。
在依据角色信息查询权限。

        select u.id userId,
               u.name username,
               u.password,
               u.role,
               u.avatar,
               u.email,
               u.status,
               r.code
        from sys_user u
                 left join sys_user_role sur on u.id = sur.user_id
                 left join sys_role r on sur.role_id = r.id
        where u.name = #{username}
          AND u.deleted = 0

然后在依据角色查询权限
不过我感觉这个type硬编码挺严重的,也算学习一下这种mybatis里面枚举了。
如果没用角色则m.id = -1让其没权限。

<select id="listRolePerms" resultType="java.lang.String">
        select distinct m.perm
        from sys_menu m
        inner join sys_role_menu rm on m.id = rm.menu_id
        inner join sys_role r on r.id = rm.role_id
        where m.type = '${@com.yu.common.enums.MenuTypeEnum@BUTTON.getValue()}'
        and m.perm is not null
        <choose>
            <when test="roles!=null and roles.size()>0">
                and r.code in
                <foreach collection="roles" item="role" open="(" close=")" separator=",">
                    #{role}
                </foreach>
            </when>
            <otherwise>
                and m.id = -1
            </otherwise>
        </choose>
    </select>

controller验证,很明确的流程就是封装输入的,然后进行验证。失败了会报错返回。
成功则生成token将权限放入redis,将角色,用户名,id封装进jwt
然后进行返回。接下来查看jwtTokenManager.createToken

    @Operation(summary = "登录")
    @PostMapping("/login")
    public Result<LoginResult> login(
            @Parameter(description = "用户名", example = "admin") @RequestParam String username,
            @Parameter(description = "密码", example = "123456") @RequestParam String password
    ) {
        // 存储username和password
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
                username.toLowerCase().trim(),
                password
        );
        // 验证用户名和密码
        Authentication authentication = authenticationManager.authenticate(authenticationToken);
        // 生成token
        String accessToken = jwtTokenManager.createToken(authentication);
        // 返回token
        LoginResult loginResult = LoginResult.builder()
                .tokenType("Bearer")
                .accessToken(accessToken)
                .build();
        return Result.success(loginResult);
    }
    @Schema(description ="登录响应对象")
    @Builder
    public static record LoginResult(
            @Schema(description = "访问token")
            String accessToken,

            @Schema(description = "token 类型",example = "Bearer")
            String tokenType,

            @Schema(description = "刷新token")
            String refreshToken,

            @Schema(description = "过期时间(单位:毫秒)")
            Long expires
    ) {
    }

jwt管理

采用hutool工具包进行jwt管理,以前用过java-jwt的,这次试试hutool。

    /**
     * 创建token
     *
     * @param authentication auth info
     * @return token
     */
    public String createToken(Authentication authentication) {
        SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal();

        // 角色放入JWT的claims
        Set<String> roles = userDetails.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority).collect(Collectors.toSet());

        // 权限数据多放入Redis
        Set<String> perms = userDetails.getPerms();
        redisTemplate.opsForValue().set(SecurityConstants.USER_PERMS_CACHE_PREFIX + userDetails.getUserId(), perms);

        Map<String, Object> claims = Map.of(
                JWTPayload.ISSUED_AT, DateTime.now(),
                JWTPayload.EXPIRES_AT, DateTime.now().offset(DateField.SECOND, tokenTtl),
                "jti", IdUtil.fastSimpleUUID(),
                "userId", userDetails.getUserId(),
                "username", userDetails.getUsername(),
                "authorities", roles);

        return JWTUtil.createToken(claims, getSecretKeyBytes());
    }

验证

http测试:
之前测试挺头疼的。
需要先发送验证码的。
然后去base64转图片(后面直接打印了结果了)
进行测试
在这里插入图片描述
成功
在这里插入图片描述
后面去vue3前端测了。用的是有来开源vue3-element-admin修改。
成功了!
在这里插入图片描述

用户注销

从jwt中获取我们设置的jti唯一表示
然后需要将redis中的删除就可以了

    @Operation(summary = "注销", security = {@SecurityRequirement(name = SecurityConstants.TOKEN_KEY)})
    @DeleteMapping("/logout")
    public Result<String> logout(HttpServletRequest request) {
        String token = RequestUtils.resolveToken(request);
        if (StrUtil.isNotBlank(token)) {
            Claims claims = jwtTokenManager.getTokenClaims(token);
            String jti = StrUtil.toString(claims.getClaim("jti"));

            Date expiration = jwtTokenManager.getExpiration(claims);
            if (expiration != null) {
                // 有过期时间,在token有效时间内存入黑名单,超出时间移除黑名单节省内存占用
                long ttl = (expiration.getTime() - System.currentTimeMillis());
                redisTemplate.opsForValue().set(SecurityConstants.BLACK_TOKEN_CACHE_PREFIX + jti, null, ttl, TimeUnit.MILLISECONDS);
            } else {
                // 无过期时间,永久加入黑名单
                redisTemplate.opsForValue().set(SecurityConstants.BLACK_TOKEN_CACHE_PREFIX + jti, null);
            }
        }
        SecurityContextHolder.clearContext();
        return Result.success("注销成功");
    }

流程小结

验证码

获取随机验证码

  • 验证码接口放行,无视security
  • 存放redis用,key = SecurityConstants.VERIFY_CODE_CACHE_PREFIX +verifyCodeKey(生成)

验证验证码

  • 拦截登录请求
  • 查询redis
    • 如果null,则过期
    • 如果错误,则返回
    • 正确放行

jwt令牌管理

  • 拦截所有除了登录的请求
  • 从jwt中解析获取Authentication
  • 放入线程中

登录

  • 框架校验

    • 获取认证信息,依据user和role表获取角色基本信息和角色
    • 依据角色获取权限
    • Authentication存放id,用户名,密码,是否启用,权限,角色,数据权限
  • 依据Authentication生成jwt

    • 存放jti随机id,userid,用户名,角色信息,权限数据
    • 过期时间5小时

注销

  • 拉黑jwt的jti

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Leetcode每日一题:1267. 统计参与通信的服务器

原题 这里有一幅服务器分布图&#xff0c;服务器的位置标识在 m * n 的整数矩阵网格 grid 中&#xff0c;1 表示单元格上有服务器&#xff0c;0 表示没有。 如果两台服务器位于同一行或者同一列&#xff0c;我们就认为它们之间可以进行通信。 请你统计并返回能够与至少一台其…

musl libc ldso 动态加载研究笔记:动态库的加载次序与初始化次序

前言 musl ldso 是按照什么次序加载动态链接的应用程序的共享库的&#xff1f;如果共享库之间有依赖&#xff0c; musl ldso 如何处理先初始化哪个 共享库&#xff1f; musl ldso 的代码可以在 musl 官方代码&#xff1a; ldso\dlstart.c 与 ldso\dynlink.c&#xff0c;其中动…

ETLCloud轻量级数据中台解决方案

引言 随着信息时代的到来&#xff0c;数据已经成为企业的重要资源&#xff0c;如何高效地管理、分析和应用数据变得尤为关键。然而&#xff0c;许多企业在构建数据中台时面临着高昂的成本、复杂的架构和漫长的实施周期等问题。为了解决这些挑战&#xff0c;我们推出了ETLCloud…

使用高斯滤波器进行表面开放轮廓过滤研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

【官方中文文档】Mybatis-Spring #目录

目录 此页面用于在GitHub上呈现索引。 NOTE: 由于链接目标是在使用maven-site-plugin转换为html的假设下指定的&#xff0c;因此在GitHub上的呈现中有一个锚点已损坏。 简介入门SqlSessionFactoryBean事务使用 SqlSession注入映射器Spring Boot使用 MyBatis APISpring Batch示…

Linux虚拟机安装(Ubuntu 20)

最近这段时间使用VMWare安装了一下Ubuntu版本的Linux虚拟机&#xff0c;在这里记录一下安装时参考的文章以及需要注意的细节 参考链接&#xff1a; VMware虚拟机下安装Ubuntu20.04&#xff08;保姆级教程&#xff09; 一、安装VMWare 下载链接&#xff1a;VMware Workstatio…

【TI毫米波雷达笔记】SOC外设初始化配置及驱动(以IWR6843AOP为例)

【TI毫米波雷达笔记】SOC外设初始化配置及驱动&#xff08;以IWR6843AOP为例&#xff09; 最基本的工程建立好以后 需要给SOC进行初始化配置 SOC_Cfg socCfg; //SOC配置结构体Task_Params taskParams; //任务参数SOC_Handle socHandle;ESM_init(0U); …

git介绍+集成到IDEA中+使用gitee

目录 git介绍 本地工作流程 IDEA集git 添加到暂存区 添加到本地仓库 gitee使用 添加到远程仓库 git介绍 git是一个开源的分布式版本控制工具&#xff0c;效率高。可以记录历史代码&#xff0c;多人代码共享 知识小点&#xff1a; 集中式版本控制&#xff1a;使用中央存…

SpringBoot案例-文件上传

目录 简介 文件上传前端页面三要素 服务端接收文件 小结 本地储存 实现 代码优化 小结 阿里云OSS 阿里云 阿里云OSS 使用第三方服务--通用思路 准备工作 参照官方SDK代码&#xff0c;编写入门程序 集成使用 阿里云OSS-使用步骤 阿里云OSS使用步骤 参照SDK编写入…

vue2 vue中的常用指令

一、为什么要学习Vue 1.前端必备技能 2.岗位多&#xff0c;绝大互联网公司都在使用Vue 3.提高开发效率 4.高薪必备技能&#xff08;Vue2Vue3&#xff09; 二、什么是Vue 概念&#xff1a;Vue (读音 /vjuː/&#xff0c;类似于 view) 是一套 **构建用户界面 ** 的 渐进式 …

mysql(八)事务隔离级别及加锁流程详解

目录 MySQL 锁简介什么是锁锁的作用锁的种类共享排他锁共享锁排它锁 粒度锁全局锁表级锁页级锁行级锁种类 意向锁间隙临键记录锁记录锁间隙锁 加锁的流程锁的内存结构加锁的基本流程根据主键加锁根据二级索引加锁根据非索引字段查询加锁加锁规律 锁信息查看查看锁的sql语句 数据…

c语言函数指针和指针函数的区别,以及回调函数的使用。

函数指针是什么&#xff0c;函数指针本质也是指针&#xff0c;不过是指向函数的指针&#xff0c;存储的是函数的地址。 指针函数是什么,指针函数其实就是返回值是指针的函数&#xff0c;本质是函数。 函数指针是如何定义的呢&#xff0c;如下 void (*pfun)(int a,int b) 这…

stm32之4.时钟体系

3.时钟体系(给单片机提供一个非常稳定的频率信号) ①可以使用三种不同的时钟源来驱动系统时钟&#xff08;SYSCLK&#xff09;&#xff0c;CPU运行的频率为168MHZ&#xff1b; HSI(RC振荡器时钟&#xff0c;也就是高速内部时钟&#xff0c;一般来说很少用&#xff0c;因为精度…

从VMware Workstation的虚拟机导入到esxi主机中

从VMware Workstation的虚拟机导入到esxi主机中 是从VMware Workstation中的虚拟机导入部署到ESXI主机中使用&#xff0c;使用真实环境导出和部署的过程&#xff0c;我为了帮助到有些初学者仅供参考&#xff0c;我做了知识共享。 1、打开VMware Workstation中的一个虚拟机&…

iis站点备份以及端口号查找

文件地址 %windir%\system32\inetsrv\config

遥感云大数据在灾害、水体与湿地领域典型案例实践及GPT模型

近年来遥感技术得到了突飞猛进的发展&#xff0c;航天、航空、临近空间等多遥感平台不断增加&#xff0c;数据的空间、时间、光谱分辨率不断提高&#xff0c;数据量猛增&#xff0c;遥感数据已经越来越具有大数据特征。遥感大数据的出现为相关研究提供了前所未有的机遇&#xf…

大数据——spark一文全知道

1、spark概述 spark是专为大规模数据处理而设计的快速通用计算引擎&#xff0c;与Hadoop的MapReduce功能类似&#xff0c;但它是基于内存的分布式计算框架&#xff0c;存储还是采用HDFS。 MapReduce和Spark的区别 MapReduce的MapReduce之间需要通过磁盘进行数据传递&#xf…

疫情下社区管理系统的设计与实现(论文+源码)_kaic

疫情下社区管理系统 摘 要&#xff1a;新冠疫情下的社区人员管理系统是基于SpringBoot搭建的一套前后端分离系统。面向疫情下的社区管理人员和社区用户&#xff0c;主要用于进行社区服务&#xff0c;进行高效的社区人员管理。具有一定的经济效益和社会效益。本文分析了新冠疫情…

基于Java+SpringBoot+vue前后端分离华强北商城二手手机管理系统设计实现

博主介绍&#xff1a;✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专…

使用 Sahi 实现 Web 自动化测试

Sahi 是 Tyto Software 旗下的一个基于业务的开源 Web 应用自动化测试工具。Sahi 运行为一个代理服务器&#xff0c;并通过注入 JavaScript 来访问 Web 页面中的元素。Sahi 支持 HTTPS 并且独立于 Web 站点&#xff0c;简单小巧却功能强大。它相对于 Selenium 等自动化测试工具…