Java登录管理功能的自我理解(尚庭公寓)

news2024/12/23 9:32:44

登录管理

背景知识

1. 认证方案概述

有两种常见的认证方案,分别是基于Session的认证和基于Token的认证,下面逐一进行介绍

  • 基于Session

    基于Session的认证流程如下图所示

    在这里插入图片描述

    该方案的特点

    • 登录用户信息保存在服务端内存(Session对象)中,若访问量增加,单台节点压力会较大
    • 随用户规模增大,若后台升级为集群,则需要解决集群中各服务器登录状态共享的问题。

在这里插入图片描述

这里的集群中各服务器登录状态共享问题,我们用一个例子来演示:
假设一个电商网站使用了一个由三台服务器组成的集群来处理用户请求。当用户在浏览商品、添加购物车和结账时,这些请求可能会被负载均衡器分配到不同的服务器上。如果没有共享登录状态的机制:
-用户在服务器A上登录,并添加商品到购物车。
-用户的下一个请求被分配到服务器B,但服务器B没有用户的登录状态信息,导致用户被视为未登录状态,购物车数据丢失。


  • 基于Token

    基于Token的认证流程如下图所示

    在这里插入图片描述

    该方案的特点

    • 登录状态保存在客户端(Token),服务器没有存储开销
    • 客户端发起的每个请求自身均携带登录状态,所以即使后台为集群,也不会面临登录状态共享的问题。

2. Token详解

本项目采用基于Token的登录方案,下面详细介绍Token这一概念。

我们所说的Token,通常指JWT(JSON Web TOKEN)。JWT是一种轻量级的安全传输方式,用于在两个实体之间传递信息,通常用于身份验证和信息传递。

JWT是一个字符串,如下图所示,该字符串由三部分组成,三部分由.分隔。三个部分分别被称为

  • header(头部)
  • payload(负载)
  • signature(签名)

在这里插入图片描述

各部分的作用如下

  • Header(头部)

    Header部分是由一个JSON对象经过base64url编码得到的,这个JSON对象用于保存JWT 的类型(typ)、签名算法(alg)等元信息,例如

{
  "alg": "HS256",
  "typ": "JWT"
}

Payload(负载)

也称为 Claims(声明),也是由一个JSON对象经过base64url编码得到的,用于保存要传递的具体信息。JWT规范定义了7个官方字段,如下:

  • iss (issuer):签发人
  • exp (expiration time):过期时间
  • sub (subject):主题
  • aud (audience):受众
  • nbf (Not Before):生效时间
  • iat (Issued At):签发时间
  • jti (JWT ID):编号

除此之外,我们还可以自定义任何字段,例如

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}
  • Signature(签名)

    由头部、负载和秘钥一起经过(header中指定的签名算法)计算得到的一个字符串,用于防止消息被篡改。


登录流程

后台管理系统的登录流程如下图所示
在这里插入图片描述

UUID(Universally Unique Identifier,通用唯一标识符)保证了每个验证码请求的唯一性。即使同时有多个用户请求验证码,UUID也能确保每个用户的请求对应唯一的验证码,避免了重复和冲突。UUID使得每次生成的验证码都是独立的,即使是同一个用户在不同时间请求验证码,也会生成不同的UUID,避免验证码被重用或篡改。

根据上述登录流程,可分析出,登录管理共需三个接口,分别是获取图形验证码登录获取登录用户个人信息,除此之外,我们还需为所有受保护的接口增加验证JWT合法性的逻辑,这一功能可通过HandlerInterceptor来实现。


接口开发

首先在LoginController中注入LoginService,如下

@Tag(name = "后台管理系统登录管理")
@RestController
@RequestMapping("/admin")
public class LoginController {

    @Autowired
    private LoginService loginService;
}

1. 获取图形验证码

  • 查看响应的数据结构

    查看web-admin模块下的com.atguigu.lease.web.admin.vo.login.CaptchaVo,内容如下

@Data
@Schema(description = "图像验证码")
@AllArgsConstructor
public class CaptchaVo {

    @Schema(description="验证码图片信息")
    private String image;

    @Schema(description="验证码key")
    private String key;
}

配置所需依赖

  • 验证码生成工具

    本项目使用开源的验证码生成工具EasyCaptcha,其支持多种类型的验证码,例如gif、中文、算术等,并且简单易用,具体内容可参考其官方文档。

    common模块的pom.xml文件中增加如下内容

<dependency>
    <groupId>com.github.whvcse</groupId>
    <artifactId>easy-captcha</artifactId>
</dependency>

Redis

common模块的pom.xml中增加如下内容

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

application.yml中增加如下配置

spring:
  data:
    redis:
      host: localhost
      port: 6379
      database: 0

注意:上述hostport需根据实际情况进行修改

编写Controller层逻辑

LoginController中增加如下内容

@Operation(summary = "获取图形验证码")
@GetMapping("login/captcha")
public Result<CaptchaVo> getCaptcha() {
    CaptchaVo captcha = loginService.getCaptcha();
    return Result.ok(captcha);
}

编写Service层逻辑

  • LoginService中增加如下内容
CaptchaVo getCaptcha();
  • LoginServiceImpl中增加如下内容
@Autowired
private StringRedisTemplate redisTemplate;

@Override
public CaptchaVo getCaptcha() {
    // 创建一个特定规格的验证码对象,规格为130x48像素,包含4个字符
    SpecCaptcha specCaptcha = new SpecCaptcha(130, 48, 4);
    // 生成验证码文本,转换为小写以避免大小写敏感问题
    String code = specCaptcha.text().toLowerCase();
    // 生成一个唯一的键来存储验证码,键的前缀标识是“admin:login”
    String key = RedisConstant.ADMIN_LOGIN_PREFIX + UUID.randomUUID();

    // 将验证码文本存储在Redis中,设置过期时间为登录验证码的预定义生存时间
    stringRedisTemplate.opsForValue().set(key, code, RedisConstant.ADMIN_LOGIN_CAPTCHA_TTL_SEC , TimeUnit.SECONDS);

    // 返回一个包含验证码图像Base64编码和存储键的实体
    return new CaptchaVo(specCaptcha.toBase64(), key);
}

知识点

  • 本项目Reids中的key遵循以下命名规范:项目名:功能模块名:其他,例如admin:login:123456

  • spring-boot-starter-data-redis已经完成了StringRedisTemplate的自动配置,我们直接注入即可。

  • 为方便管理,可以将Reids相关的一些值定义为常量,例如key的前缀、TTL时长,内容如下。大家可将这些常量统一定义在一个RedisConstant类中

public class RedisConstant {
    public static final String ADMIN_LOGIN_PREFIX = "admin:login:";
    public static final Integer ADMIN_LOGIN_CAPTCHA_TTL_SEC = 60;
    public static final String APP_LOGIN_PREFIX = "app:login:";
    public static final Integer APP_LOGIN_CODE_RESEND_TIME_SEC = 60;
    public static final Integer APP_LOGIN_CODE_TTL_SEC = 60 * 10;
    public static final String APP_ROOM_PREFIX = "app:room:";
}

2. 登录接口

  • 登录校验逻辑

    用户登录的校验逻辑分为三个主要步骤,分别是校验验证码校验用户状态校验密码,具体逻辑如下

    • 前端发送usernamepasswordcaptchaKeycaptchaCode请求登录。
    • 判断captchaCode是否为空,若为空,则直接响应验证码为空;若不为空进行下一步判断。
    • 根据captchaKey从Redis中查询之前保存的code,若查询出来的code为空,则直接响应验证码已过期;若不为空进行下一步判断。
    • 比较captchaCodecode,若不相同,则直接响应验证码不正确;若相同则进行下一步判断。
    • 根据username查询数据库,若查询结果为空,则直接响应账号不存在;若不为空则进行下一步判断。
    • 查看用户状态,判断是否被禁用,若禁用,则直接响应账号被禁;若未被禁用,则进行下一步判断。
    • 比对password和数据库中查询的密码,若不一致,则直接响应账号或密码错误,若一致则进行入最后一步。
    • 创建JWT,并响应给浏览器。
  • 接口逻辑实现

    • 查看请求数据结构

      查看web-admin模块下的com.atguigu.lease.web.admin.vo.login.LoginVo,具体内容如下

@Data
@Schema(description = "后台管理系统登录信息")
public class LoginVo {

    @Schema(description="用户名")
    private String username;

    @Schema(description="密码")
    private String password;

    @Schema(description="验证码key")
    private String captchaKey;

    @Schema(description="验证码code")
    private String captchaCode;
}

配置所需依赖

登录接口需要为登录成功的用户创建并返回JWT,本项目使用开源的JWT工具Java-JWT,配置如下,具体内容可参考官方文档。

  • 引入Maven依赖

    common模块的pom.xml文件中增加如下内容

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
</dependency>

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <scope>runtime</scope>
</dependency>

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <scope>runtime</scope>
</dependency>

创建JWT工具类

common模块下创建com.atguigu.lease.common.utils.JwtUtil工具类,内容如下

public class JwtUtil {

    private static long tokenExpiration = 60 * 60 * 1000L;
    private static SecretKey tokenSignKey = Keys.hmacShaKeyFor("M0PKKI6pYGVWWfDZw90a0lTpGYX1d4AQ".getBytes());

    public static String createToken(Long userId, String username) {
        String token = Jwts.builder().
                setSubject("USER_INFO").
                setExpiration(new Date(System.currentTimeMillis() + tokenExpiration)).
                claim("userId", userId).
                claim("username", username).
                signWith(tokenSignKey).
                compact();
        return token;
    }
}

编写Controller层逻辑

LoginController中增加如下内容

@Operation(summary = "登录")
@PostMapping("login")
public Result<String> login(@RequestBody LoginVo loginVo) {
    String token = loginService.login(loginVo);
    return Result.ok(token);
}

编写Service层逻辑

  • LoginService中增加如下内容
String login(LoginVo loginVo);

LoginServiceImpl中增加如下内容

@Override
public String login(LoginVo loginVo) {
    //1.判断是否输入了验证码
    if (!StringUtils.hasText(loginVo.getCaptchaCode())) {
        throw new LeaseException(ResultCodeEnum.ADMIN_CAPTCHA_CODE_NOT_FOUND);
    }

    //2.校验验证码
    String code = redisTemplate.opsForValue().get(loginVo.getCaptchaKey());
    if (code == null) {
        throw new LeaseException(ResultCodeEnum.ADMIN_CAPTCHA_CODE_EXPIRED);
    }

    if (!code.equals(loginVo.getCaptchaCode().toLowerCase())) {
        throw new LeaseException(ResultCodeEnum.ADMIN_CAPTCHA_CODE_ERROR);
    }

    //3.校验用户是否存在
    LambdaQueryWrapper<SystemUser> systemUserLambdaQueryWrapper = new LambdaQueryWrapper<>();
    systemUserLambdaQueryWrapper.eq(SystemUser::getUsername, loginVo.getUsername());
    SystemUser systemUser = systemUserMapper.selectOne(systemUserLambdaQueryWrapper);

    if (systemUser == null) {
        throw new LeaseException(ResultCodeEnum.ADMIN_ACCOUNT_NOT_EXIST_ERROR);
    }

    //4.校验用户是否被禁
    if (systemUser.getStatus() == BaseStatus.DISABLE) {
        throw new LeaseException(ResultCodeEnum.ADMIN_ACCOUNT_DISABLED_ERROR);
    }

    //5.校验用户密码,这里不要忘记对密码进行处理,数据库中的密码都是经过加密处理的
    if (!systemUser.getPassword().equals(DigestUtils.md5Hex(loginVo.getPassword()))) {
        throw new LeaseException(ResultCodeEnum.ADMIN_ACCOUNT_ERROR);
    }

    //6.创建并返回TOKEN
    return JwtUtil.createToken(systemUser.getId(), systemUser.getUsername());
}

写完之后,启动项目却接收到了一个失败的响应:

在这里插入图片描述

发现报空指针异常:

在这里插入图片描述

那么password为什么为空呢?
我们回到SystemUser类中,可以看到…
在这里插入图片描述

select=false意味着当执行查询操作时,该字段(在这个例子中是password)将不会被包含在查询结果中,也就是没有给前端反馈password,systemUserMapper.selectOne()查询到的password就为null。

找到原因之后,最通用的做法就是不使用通用的select语句,自定义一个mapper:

编写Mapper层逻辑

  • LoginMapper中增加如下内容
SystemUser selectOneByUsername(String username);

LoginMapper.xml中增加如下内容

<select id="selectOneByUsername" resultType="com.atguigu.lease.model.entity.SystemUser">
    select id,
           username,
           password,
           name,
           type,
           phone,
           avatar_url,
           additional_info,
           post_id,
           status
    from system_user
    where is_deleted = 0
      and username = #{username}
</select>

编写HandlerInterceptor

我们需要为所有受保护(指登录之后才能访问)的接口增加校验JWT合法性的逻辑。我们可以用一个拦截器来实现,具体实现如下

  • JwtUtil中增加parseToken方法,检验其合法性,内容如下
// 令牌过期时间为1小时
private static long tokenExpiration = 60 * 60 * 1000L;
private static SecretKey tokenSignKey = Keys.hmacShaKeyFor("M0PKKI6pYGVWWfDZw90a0lTpGYX1d4AQ".getBytes());


public static void parseToken(String token){
    try{
        JwtParser jwtParser = Jwts.parserBuilder().setSigningKey(tokenSignKey ).build();
        jwtParser.parseClaimsJws(token);
    }catch (ExpiredJwtException e){
        throw new LeaseException(ResultCodeEnum.TOKEN_EXPIRED);
    }catch (JwtException e){
        throw new LeaseException(ResultCodeEnum.TOKEN_INVALID);
    }
}

编写HandlerInterceptor

web-admin模块中创建com.atguigu.lease.web.admin.custom.interceptor.AuthenticationInterceptor类,内容如下,有关HanderInterceptor的相关内容,可参考官方文档。

@Component
public class AuthenticationInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token = request.getHeader("access-token");
        JwtUtil.parseToken(token);
        return true;
    }
}

注意:我们约定,前端登录后,后续请求都将JWT,放置于HTTP请求的Header中,其Header的key为access-token

注册HandlerInterceptor

web-admin模块com.atguigu.lease.web.admin.custom.config.WebMvcConfiguration中增加如下内容

@Autowired
private AuthenticationInterceptor authenticationInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(this.authenticationInterceptor).addPathPatterns("/admin/**").excludePathPatterns("/admin/login/**");
}

3. 获取登录用户个人信息

  • 查看请求和响应的数据结构

    • 响应的数据结构

      查看web-admin模块下的com.atguigu.lease.web.admin.vo.system.user.SystemUserInfoVo,内容如下

@Schema(description = "员工基本信息")
@Data
public class SystemUserInfoVo {

    @Schema(description = "用户姓名")
    private String name;

    @Schema(description = "用户头像")
    private String avatarUrl;
}

请求的数据结构

按理说,前端若想获取当前登录用户的个人信息,需要传递当前用户的id到后端进行查询。但是由于请求中携带的JWT中就包含了当前登录用户的id,故请求个人信息时,就无需再传递id

修改JwtUtil中的parseToken方法

由于需要从Jwt中获取用户id,因此需要为parseToken 方法增加返回值,如下

public static Claims parseToken(String token){

    if (token==null){
        throw new LeaseException(ResultCodeEnum.ADMIN_LOGIN_AUTH);
    }

    try{
        JwtParser jwtParser = Jwts.parserBuilder().setSigningKey(secretKey).build();
        return jwtParser.parseClaimsJws(token).getBody();
    }catch (ExpiredJwtException e){
        throw new LeaseException(ResultCodeEnum.TOKEN_EXPIRED);
    }catch (JwtException e){
        throw new LeaseException(ResultCodeEnum.TOKEN_INVALID);
    }
}

编写ThreadLocal工具类

理论上我们可以在Controller方法中,使用@RequestHeader获取JWT,然后在进行解析,如下

@Operation(summary = "获取登陆用户个人信息")
@GetMapping("info")
public Result<SystemUserInfoVo> info(@RequestHeader("access-token") String token) {
    Claims claims = JwtUtil.parseToken(token);
    Long userId = claims.get("userId", Long.class);
    SystemUserInfoVo userInfo = service.getLoginUserInfo(userId);
    return Result.ok(userInfo);
}

上述代码的逻辑没有任何问题,但是这样做,JWT会被重复解析两次(一次在拦截器中,一次在该方法中)。为避免重复解析,通常会在拦截器将Token解析完毕后,将结果保存至ThreadLocal中,这样一来,我们便可以在整个请求的处理流程中进行访问了。

在这里插入图片描述

common模块中创建com.atguigu.lease.common.login.LoginUserHolder工具类

public class LoginUserHolder {
    public static ThreadLocal<LoginUser> threadLocal = new ThreadLocal<>();

    public static void setLoginUser(LoginUser loginUser) {
        threadLocal.set(loginUser);
    }

    public static LoginUser getLoginUser() {
        return threadLocal.get();
    }

    public static void clear() {
        threadLocal.remove();
    }
}

同时在common模块中创建com.atguigu.lease.common.login.LoginUser

@Data
@AllArgsConstructor
public class LoginUser {

    private Long userId;
    private String username;
}

修改AuthenticationInterceptor拦截器

@Component
public class AuthenticationInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String token = request.getHeader("access-token");

        Claims claims = JwtUtil.parseToken(token);
        Long userId = claims.get("userId", Long.class);
        String username = claims.get("username", String.class);
        // 把解析出来的userid和username放到ThreadLocal中
        LoginUserHolder.setLoginUser(new LoginUser(userId, username));

        return true;

    }

	// 拦截器执行完之后执行, 清理线程变量
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        LoginUserHolder.clear();
    }
}

编写Controller层逻辑

LoginController中增加如下内容

@Operation(summary = "获取登陆用户个人信息")
@GetMapping("info")
public Result<SystemUserInfoVo> info() {
    SystemUserInfoVo userInfo = loginService.getLoginUserInfo(LoginUserHolder.getLoginUser().getUserId());
    return Result.ok(userInfo);
}

编写Service层逻辑

LoginService中增加如下内容

@Override
public SystemUserInfoVo getLoginUserInfo(Long userId) {
    SystemUser systemUser = systemUserMapper.selectById(userId);
    SystemUserInfoVo systemUserInfoVo = new SystemUserInfoVo();
    systemUserInfoVo.setName(systemUser.getName());
    systemUserInfoVo.setAvatarUrl(systemUser.getAvatarUrl());
    return systemUserInfoVo;
}

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

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

相关文章

Django 一对多关系

1&#xff0c;创建 Django 应用 Test/app9 django-admin startapp app9 2&#xff0c;注册应用 Test/Test/settings.py 3&#xff0c;添加应用路由 Test/Test/urls.py from django.contrib import admin from django.urls import path, includeurlpatterns [path(admin/,…

安装KB5039212更新卡在25% 或者 96% 进度

系统之家7月1日消息&#xff0c;微软在6月11日的补丁星期二活动中&#xff0c;为Windows 11系统推出了KB5039212更新。然而&#xff0c;部分用户在Windows社区中反映&#xff0c;安装过程中出现失败&#xff0c;进度条在25%或96%时卡住。对于遇到此类问题的Windows 11用户&…

YOLOv8改进 | 主干网络 | C2f融合动态卷积模块ODConv

&#x1f4a1;&#x1f4a1;&#x1f4a1;本专栏所有程序均经过测试&#xff0c;可成功执行&#x1f4a1;&#x1f4a1;&#x1f4a1; 专栏目录 &#xff1a;《YOLOv8改进有效涨点》专栏介绍 & 专栏目录 | 目前已有40篇内容&#xff0c;内含各种Head检测头、损失函数Loss、…

Linux CentOS 7 离线安装.NET环境

下载 下载.NET 例如&#xff1a; aspnetcore-runtime-6.0.15-linux-x64.tar.gz 复制 复制到如下目录&#xff1a; /usr/local/dotnet/aspnetcore-runtime-6.0.15-linux-x64.tar.gz 解压 cd /usr/local/dotnet/ tar -zxvf aspnetcore-runtime-6.0.15-linux-x64.tar.gz 创建…

非标设备行业的数智化项目管理

近年来&#xff0c;中国制造快速发展&#xff0c;企业迫切需要加快转型升级。与传统制造业相比&#xff0c;高端制造业具有明显的优势&#xff1a;高技术、高附加值、低污染、低排放、竞争优势强。一方面&#xff0c;企业对于生产效率和自动化水平的要求不断提高&#xff0c;期…

esp12实现的网络时钟校准

网络时间的获取是通过向第三方服务器发送GET请求获取并解析出来的。 在本篇博客中&#xff0c;网络时间的获取是一种自动的行为&#xff0c;当系统成功连接WiFi获取到网络天气后&#xff0c;系统将自动获取并解析得到时间和日期&#xff0c;为了减少误差每两分钟左右进行一次校…

qt可点击的QLabel

需求——问题与思路 使用wpf实现一个可点击的超链接label相当简单&#xff08;如下图&#xff09;&#xff0c;但是qt的QLabel不会响应点击事件&#xff0c;那就从QLabel继承一个类&#xff0c;然后在该类中重写mousePressEvent函数&#xff0c;并在该函数中对左键点击事件做响…

人工智能——常用数学基础之线代中的矩阵

1. 矩阵的本质&#xff1a; 矩阵本质上是一种数学结构&#xff0c;它由按照特定规则排列的数字组成&#xff0c;通常被表示为一个二维数组。矩阵可以用于描述一组数据&#xff0c;或者表示某种关系&#xff0c;比如线性变换。 在人工智能中&#xff0c;矩阵常被用来表示数据集…

沉浸感拉满的三模游戏外设神器!谷粒金刚3 Pro游戏手柄开箱试玩

沉浸感拉满的三模游戏外设神器&#xff01;谷粒金刚3 Pro游戏手柄开箱试玩 哈喽小伙伴们好&#xff0c;我是Stark-C~ 对于喜欢打游戏的玩家来说&#xff0c;一款得力的游戏外设绝对是提升游戏体验&#xff0c;增加游戏乐趣的重要神器&#xff01;而在众多的外设中&#xff0c…

全同态加密在大模型应用中应用

密码学简介 上文的图例基本展示了常见加密体系。加密体系&#xff0c;如果用比较正式的描述方法&#xff0c;无疑是做了三件事&#xff1a; 首先&#xff0c;通过一个生成算法 &#x1d43e;&#x1d452;&#x1d466;&#x1d43a;&#x1d452;&#x1d45b;(1&#x1d70…

32.哀家要长脑子了!

1.299. 猜数字游戏 - 力扣&#xff08;LeetCode&#xff09; 公牛还是挺好数的&#xff0c;奶牛。。。妈呀&#xff0c;一朝打回解放前 抓本质抓本质&#xff0c;有多少位非公牛数可以通过重新排列转换公牛数字&#xff0c;意思就是&#xff0c;当这个数不是公牛数字时&#x…

ctfshow web入门 sqli-libs web552--web560

web552 宽字节注入 嗯原理我就不讲了&#xff0c;还是有点复杂后面有时间讲讲 总而言之就是用汉字把\的转义作用抵消了然后正常注入即可 ?id-1包 union select 1,2,3--?id-1包union select 1,(select group_concat(table_name) from information_schema.tables where tab…

ChatGPT-4o医学应用、论文撰写、数据分析与可视化、机器学习建模、病例自动化处理、病情分析与诊断支持

2022年11月30日&#xff0c;可能将成为一个改变人类历史的日子——美国人工智能开发机构OpenAI推出了聊天机器人ChatGPT-3.5&#xff0c;将人工智能的发展推向了一个新的高度。2023年11月7日&#xff0c;OpenAI首届开发者大会被称为“科技界的春晚”&#xff0c;吸引了全球广大…

如何使用pytest组织自动化测试用例结构?

如何组织自动化测试工程的目录结构&#xff1f;这篇文章介绍了我是如何组织整个自动化工程目录结构的&#xff0c;本篇介绍下我是如何利用pytest框架组织一个测试用例文件的。 用例文件组织原则 整个testsuite目录下整体上按照特性模块划分目录&#xff0c;每个目录下可以只包…

Python容器 之 列表--下标和切片

列表的切片 得到是 新的列表字符串的切片 得到是 新的字符串 如果下标 不存在会报错 list1 [1, 3.14, "hello", False] print(list1)# 获取 列表中 第一个数据 print(list1[0]) # 1# 获取列表中的最后一个数据 print(list1[-1]) # [False]# 获取中间两个数 即 3.1…

币界网快讯,比特币7月份看牛预测

今日数字货币市场全面开启反弹&#xff0c;比特币从 60,000 美元大关飙升至 63,700 美元&#xff0c;预示着 7 月牛市的到来。在此之前&#xff0c;上周曾短暂跌破 60,000 美元&#xff0c;但受到 BTC 现货 ETF 流入的 7,300 万美元的推动——这是两周以来最大的流入。 BTC价格…

Linux下SUID提权学习 - 从原理到使用

目录 1. 文件权限介绍1.1 suid权限1.2 sgid权限1.3 sticky权限 2. SUID权限3. 设置SUID权限4. SUID提权原理5. SUID提权步骤6. 常用指令的提权方法6.1 nmap6.2 find6.3 vim6.4 bash6.5 less6.6 more6.7 其他命令的提权方法 1. 文件权限介绍 linux的文件有普通权限和特殊权限&a…

Redis基础教程(六):redis 哈希(Hash)

&#x1f49d;&#x1f49d;&#x1f49d;首先&#xff0c;欢迎各位来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里不仅可以有所收获&#xff0c;同时也能感受到一份轻松欢乐的氛围&#xff0c;祝你生活愉快&#xff01; &#x1f49d;&#x1f49…

【火猫】cs2 donk登顶2024年上半年ADR榜

1、HLTV发布数据统计&#xff0c;donk以55张地图中96.8的ADR成功登顶上半年ADR榜单第一。紧随其后的是法国巨星ZywOo&#xff0c;以68图89.2的ADR排名第二。G2当家狙击手m0NESY以73图85的ADR排名第三。 2、白俄罗斯爆料人harumi透露&#xff1a;“Falcons战队向BOROS支付了6个月…

每日复盘-20240701

今日关注&#xff1a; 20240701 六日涨幅最大: ------1--------301182--------- 凯旺科技 五日涨幅最大: ------1--------300977--------- 深圳瑞捷 四日涨幅最大: ------1--------300977--------- 深圳瑞捷 三日涨幅最大: ------1--------300461--------- 田中精机 二日涨幅最…