SpringSecurity-从入门到精通

news2024/11/24 14:27:21

SpringSecurity从入门到精通

  • 一、简介
    • 1.1 官网介绍
    • 1.2 认证与授权
  • 二、使用步骤
    • 2.1 快速入门
    • 2.2 认证流程
    • 2.3 相关概念
  • 三、解决问题
    • 3.1 思路分析
    • 3.2 准备工作
    • 3.3 具体实现
    • 3.4 加密存储
    • 3.5 登录接口
    • 3.6 认证过滤器

一、简介

1.1 官网介绍

SpringSecurity 是一个强大且高度自定义的认证和访问控制框架。这是保证基于 Spring 的应用程序安全的实际标准。

SpringSecurity 是一个专注于为 Java 应用程序提供身份验证和授权的框架。像所有 Spring 项目一样,Spring 安全性的真正力量在于它可以轻松扩展以满足自定义需求。

1.2 认证与授权

一般 Web 应用的需要进行认证授权

  • 认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户
  • 授权:经过认证后判断当前用户是否有权限进行某个操作

这也是 SpringSecurity 作为安全框架的核心功能。

二、使用步骤

2.1 快速入门

① 搭建 SpringBoot 工程

在这里插入图片描述

② 引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

③ 创建 Controller 层

@RestController
@RequestMapping("/test")
public class TestController {

    @GetMapping("user/list")
    public ResponseResult<List<User>> test(){
         User userOne = new User(1, "admin", "admin123", "18260109457");
         User userTwo = new User(2, "ry", "admin123", "13182469250");
         List<User> list = new ArrayList<>(2);
         list.add(userOne);
         list.add(userTwo);
         return ResponseResult.returnData(list);
    }
}

引入依赖后我们在尝试去访问之前的接口就会自动跳转到一个 SpringSecurity 的默认登陆页面,默认用户名是 user,密码会输出在控制台,必须登陆之后才能对接口进行访问。

在这里插入图片描述

登录密码

在这里插入图片描述

登录成功后,便可以访问接口

在这里插入图片描述

2.2 认证流程

1. 登录校验流程

在这里插入图片描述

2. 原理初探

想要知道如何实现自己的登陆流程就必须要先知道入门案例中 SpringSecurity 的流程。

3. SpringSecurity 完整流程

SpringSecurity 的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器。这里我们可以看看入门案例中的过滤器。

在这里插入图片描述

图中只展示了核心过滤器,其它的非核心过滤器并没有在图中展示。

UsernamePasswordAuthenticationFilter: 负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。
ExceptionTranslationFilter: 处理过滤器链中抛出的任何AccessDeniedExceptionAuthenticationExceptionFilterSecurityInterceptor: 负责权限校验的过滤器。

我们可以通过 Debug 查看当前系统中 SpringSecurity 过滤器链中有哪些过滤器及它们的顺序。

在这里插入图片描述

认证流程详解

在这里插入图片描述

Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。
AuthenticationManager接口: 定义了认证Authentication的方法。 
UserDetailsService接口: 加载用户特定数据的核心接口,里面定义了一个根据用户名查询用户信息的方法。
UserDetails接口: 提供核心用户信息,通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。

2.3 相关概念

1.主体,英文单词:principal
使用系统的用户或设备或从其他系统远程登录的用户等。简单说就是谁使用系统谁就是主体。

2.认证,英文单词:authentication
权限管理系统确认一个主体的身份,允许主体进入系统。简单说就是“主体”证明自己是谁。笼统的认为就是以前所做的登录操作。

3.授权,英文单词:authorization
将操作系统的“权力”“授予”“主体”,这样主体就具备了操作系统中特定功能的能力。
所以简单来说,授权就是给用户分配权限

三、解决问题

3.1 思路分析

在这里插入图片描述

登录
​①自定义登录接口  
1.调用ProviderManager的方法进行认证,如果认证通过生成jwt
2.把用户信息存入redis中

②自定义UserDetailsService 
1.在这个实现类中去查询数据库

校验
​①定义Jwt认证过滤器
1.获取token
2.解析token获取其中的userid
3.从redis中获取用户信息
4.存入SecurityContextHolder 

3.2 准备工作

1.添加依赖

<!--redis依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--fastjson依赖-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.15</version>
</dependency>
<!--jwt依赖-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>

2.添加配置类,工具类等信息
具体代码访问 gitee 代码仓库:https://gitee.com/Fh_1214/spring-security

3.3 具体实现

1.数据库校验用户
从之前的分析我们可以知道,我们可以自定义一个UserDetailsService,让 SpringSecurity 使用我们 UserDetailsService。我们自己的 UserDetailsService 可以从数据库中查询用户名和密码。

2.建立用户表

CREATE TABLE `sys_user` (
  `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `user_name` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '用户名',
  `nick_name` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '昵称',
  `password` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '密码',
  `status` CHAR(1) DEFAULT '0' COMMENT '账号状态(0正常 1停用)',
  `email` VARCHAR(64) DEFAULT NULL COMMENT '邮箱',
  `phonenumber` VARCHAR(32) DEFAULT NULL COMMENT '手机号',
  `sex` CHAR(1) DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)',
  `avatar` VARCHAR(128) DEFAULT NULL COMMENT '头像',
  `user_type` CHAR(1) NOT NULL DEFAULT '1' COMMENT '用户类型(0管理员,1普通用户)',
  `create_by` BIGINT(20) DEFAULT NULL COMMENT '创建人的用户id',
  `create_time` DATETIME DEFAULT NULL COMMENT '创建时间',
  `update_by` BIGINT(20) DEFAULT NULL COMMENT '更新人',
  `update_time` DATETIME DEFAULT NULL COMMENT '更新时间',
  `del_flag` INT(11) DEFAULT '0' COMMENT '删除标志(0代表未删除,1代表已删除)',
  PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='用户表'

3.引入MybatisPuls 和 mysql 依赖

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.4.0</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

4.配置数据库信息

server:
  port: 8091
spring:
  redis:
    host: 192.168.220.131
    port: 6379
    database: 0
    # 密码
    #password:
    timeout: 10s
    lettuce:
      pool:
        min-idle: 0
        max-idle: 8
        max-active: 8
        max-wait: -1ms
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.220.131:3306/spring_security?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: root
    password: root
mybatis-plus:
  mapper-locations: classpath:/mapper/**/*.xml
  global-config:
    db-config:
      id-type: auto
      logic-delete-value: 1
      logic-not-delete-value: 0

5.定义Mapper接口

public interface UserMapper extends BaseMapper<User> {

}

6.修改User实体类

类名上加@TableName(value = "sys_user") ,id字段上加 @TableId

7.配置Mapper扫描

@MapperScan("com.huang.tokendemo.mapper")
@SpringBootApplication
public class TokenDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(TokenDemoApplication.class, args);
    }

}

8.测试MP是否能正常使用

@SpringBootTest
public class TokenDemoApplicationTests {

    @Autowired
    private UserMapper userMapper;

    @Test
    public void testUserMapper(){
        List<User> users = userMapper.selectList(null);
        System.out.println(users);
    }

}

9.查询结果

在这里插入图片描述
10.登录校验

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        //查询用户信息
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getUserName,username);
        User user = userMapper.selectOne(queryWrapper);
        //如果没有查询到用户就抛出异常
        if(Objects.isNull(user)){
            throw new RuntimeException("用户名或者密码错误");
        }
        return new LoginUser(user);
    }
}
@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {

    private User user;

    public LoginUser(User user) {
        this.user = user;
    }
    @JSONField(serialize = false)
    private List<SimpleGrantedAuthority> authorities;
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUserName();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

现在数据库用户表中的密码字段是明文存储,且后端代码没有做密码加密处理,所以登录时使用明文密码登录就会报这个错误。

在这里插入图片描述

只需要在密码字段前面加上 {noop},即可登录成功,如下:

在这里插入图片描述

3.4 加密存储

实际项目中我们不会把密码明文存储在数据库中。

默认使用的 PasswordEncoder 要求数据库中的密码格式为:{id}password 。它会根据 id 去判断密码的加密方式。但是我们一般不会采用这种方式。所以就需要替换 PasswordEncoder。

我们一般使用 SpringSecurity 为我们提供的 BCryptPasswordEncoder。我们只需要使用把 BCryptPasswordEncoder 对象注入 Spring 容器中,SpringSecurity 就会使用该PasswordEncoder 来进行密码校验。

​我们可以定义一个 SpringSecurity 的配置类,SpringSecurity 要求这个配置类要继承WebSecurityConfigurerAdapter。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

BCryptPasswordEncoder 介绍

SpringSecurity 中的 BCryptPasswordEncoder 方法采用 SHA-256 + 随机盐 + 密钥对密码进行加密。SHA 系列是 Hash 算法,不是加密算法,使用加密算法意味着可以解密(这个与编码/解码一样),但是采用 Hash 处理,其过程是不可逆的。

  • 加密(encode): 注册用户时,使用SHA-256+随机盐+密钥把用户输入的密码进行hash处理,得到密码的hash值,然后将其存入数据库中。

  • 密码匹配(matches): 用户登录时,密码匹配阶段并没有进行密码解密(因为密码经过Hash处理,是不可逆的),而是使用相同的算法把用户输入的密码进行hash处理,得到密码的hash值,然后将其与从数据库中查询到的密码 hash 值进行比较。如果两者相同,说明用户输入的密码正确。

@Test void test(){
    PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
    // $2a$10$OxNG2v3B1238OFTgmIJdQOfQm4OAEXQSY6ILThj1C/pFEAgPv6.6K
    String password = passwordEncoder.encode("123456");
    boolean matches = passwordEncoder.matches("123456",
            "$2a$10$OxNG2v3B1238OFTgmIJdQOfQm4OAEXQSY6ILThj1C/pFEAgPv6.6K");
    System.out.println(matches);
}

注意:项目引入 BCryptPasswordEncoder 后,数据库中的密码字段应该为加密后的密文.否则在登陆时报错:用户名或密码错误。且后端控制台输出如下信息。

2023-04-28 14:50:20.784  WARN 37996 --- [nio-8091-exec-6] o.s.s.c.bcrypt.BCryptPasswordEncoder     : Encoded password does not look like BCrypt

3.5 登录接口

接下我们需要自定义登陆接口,然后让 SpringSecurity 对这个接口放行,让用户访问这个接口的时候不用登录也能访问。

​在接口中我们通过 AuthenticationManager 的 authenticate 方法来进行用户认证,所以需要在 SecurityConfig 中配置把 AuthenticationManager 注入容器。

​认证成功的话要生成一个 jwt,放入响应中返回。并且为了让用户下回请求时能通过 jwt 识别出具体的是哪个用户,我们需要把用户信息存入 redis,可以把用户 id 作为 key。

登录 Controller

@RestController
public class LoginController {

    @Resource
    private LoginService loginService;

    @PostMapping("/user/login")
    public ResponseResult<Map<String,String>> login(@RequestBody User user){
        //登录
        Map<String, String> login = loginService.login(user);
        return ResponseResult.success(login);
    }

}

登录 Service

@Service
public class LoginServiceImpl implements LoginService {

    @Resource
    private AuthenticationManager authenticationManager;

    @Resource
    private RedisCache redisCache;

    @Override
    public Map<String,String> login(User user) {
        //AuthenticationManager authenticate进行用户认证
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        //如果认证没通过,给出对应的提示
        if(Objects.isNull(authenticate)){
            throw new RuntimeException("登录失败");
        }
        //如果认证通过了,使用userid生成一个jwt jwt存入ResponseResult返回
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        String userid = loginUser.getUser().getId().toString();
        String jwt = JwtUtil.createJWT(userid);
        Map<String,String> map = new HashMap<>();
        map.put("token",jwt);
        //把完整的用户信息存入redis  userid作为key
        redisCache.setCacheObject("login_userId:"+userid,loginUser);
        return map;
    }

}

SecurityConfig 配置类

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/user/login").anonymous()
               .anyRequest().authenticated();
    }
    
    //创建BCryptPasswordEncoder注入容器
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

登陆成功后返回 token 令牌

在这里插入图片描述

3.6 认证过滤器

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Resource
    private RedisCache redisCache;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //获取token
        String token = request.getHeader("token");
        if (!StringUtils.hasText(token)) {
            //放行
            filterChain.doFilter(request, response);
            return;
        }
        //解析token
        String userid;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            userid = claims.getSubject();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("token非法");
        }
        //从redis中获取用户信息
        String redisKey = "login_userId:" + userid;
        LoginUser loginUser = redisCache.getCacheObject(redisKey);
        if(Objects.isNull(loginUser)){
            throw new RuntimeException("用户未登录");
        }
        //存入SecurityContextHolder
        //TODO 获取权限信息封装到Authentication中
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        //放行
        filterChain.doFilter(request, response);
    }
}

SecurityConfig 配置类

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/user/login").anonymous()
               .anyRequest().authenticated();
    }
        //添加过滤器
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

    //创建BCryptPasswordEncoder注入容器
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    
    @Resource
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

请求头未加 token 认证失败

在这里插入图片描述
请求头携带上 token 调试

在这里插入图片描述

调试放行,响应成功

在这里插入图片描述

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

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

相关文章

Stable Diffusion-生式AI的新范式

! 扩散模型&#xff08;Stable Diffusion)现在是生成图像的首选模型。由于扩散模型允许我们以提示( prompts)为条件生成图像&#xff0c;我们可以生成我们所选择的图像。在这些文本条件的扩散模型中&#xff0c;稳定扩散模型由于其开源性而最为著名。 在这篇文章中&#xff0…

通用智能的瓶颈及可能的解决途径

通用智能是指能够在各种不同的任务和环境中灵活地适应和执行任务的智能。通用智能与特定任务的智能相反&#xff0c;后者只能在特定领域或任务中表现出色。通用智能的理论基础是人工智能领域的通用人工智能&#xff08;AGI&#xff09;研究&#xff0c;旨在设计出能够像人类一样…

【Java笔试强训 5】

&#x1f389;&#x1f389;&#x1f389;点进来你就是我的人了博主主页&#xff1a;&#x1f648;&#x1f648;&#x1f648;戳一戳,欢迎大佬指点! 欢迎志同道合的朋友一起加油喔&#x1f93a;&#x1f93a;&#x1f93a; 目录 一、选择题 二、编程题 &#x1f525;统计回文…

Zynq-7000、FMQL45T900的GPIO控制(三)---linux管脚编号计算

本文主要对在Linux下使用zynq-7000或者FMQL45T900控制MIO/EMIO 首先内核配置项 如下&#xff0c;这个不用太多关注&#xff0c;一般都是默认打开的 CONFIG_GPIO_SYSFSy CONFIG_SYSVIPCy CONFIG_GPIO_ZYNQy两者的控制都是流程都是一样的&#xff0c;在细节上又区别 首先都在…

Go | 一分钟掌握Go | 9 - 通道

作者&#xff1a;Mars酱 声明&#xff1a;本文章由Mars酱编写&#xff0c;部分内容来源于网络&#xff0c;如有疑问请联系本人。 转载&#xff1a;欢迎转载&#xff0c;转载前先请联系我&#xff01; 前言 在Java中&#xff0c;多线程之间的通信方式有哪些&#xff1f;记得吗&…

浪潮之巅 OpenAI有可能是历史上第一个10万亿美元的公司

淘金时代很像 如果你那个时候去加州淘金&#xff0c;一大堆人会死掉&#xff0c;但是卖勺子的人、卖铲子的人永远可以赚钱。所谓的shove and pick business。 大模型是平台型机会。按照我们几天的判断&#xff0c;以模型为先的平台&#xff0c;将比以信息为先的平台体量更大。…

带你深入学习k8s--(四) 控制器(k8s核心)

目录 一、概念 1、什么是控制器 2、控制器执行流程 3、控制器类型 二、控制器的使用 1、ReplicaSet 2、Deployment 1、版本迭代 2、回滚 3、修改滚动更新策略 4、暂停与恢复 3、daemonset 4、job 5、cronjob 前言&#xff1a; 上一章我们说到&#xff0c;pod有…

C++——入门基础知识

0.关注博主有更多知识 C知识合集 目录 1.命名空间 1.1命名空间的定义 1.2命名空间的使用 1.3命名空间定义的补充 2.输入与输出 3.缺省参数 3.1全缺省参数 3.2半缺省参数 3.3缺省参数的补充 4.函数重载 4.1C为什么支持函数重载&#xff1f; &#xff15;.引用 5.…

Wine运行器3.2.1——Windows虚拟机模块支持非X86架构

不写太多啥了&#xff0c;详细介绍看这里就行&#xff1a;https://bbs.deepin.org/post/248098 更新内容 ※1、Windows 虚拟机安装工具支持非 X86 架构&#xff1b; ※2、应用打包器可以与星火应用商店配合构建 arm/all 全架构的 Wine 包&#xff1b; ※3、Windows 虚拟机安装…

【MATLAB图像处理实用案例详解(12)】——基于纹理特征的指纹识别方法

目录 一、指纹图像预处理1.1 图像对比度增强1.2 图像二值化1.3 图像滤波 二、指纹图像特征提取 指纹识别系统主要涉及4个步骤&#xff1a;指纹图像采集、图像预处理、特征提取、特征匹配。一开始&#xff0c;通过指纹读取设备取得图像&#xff0c;并对原始图像进行初步处理&…

《斯坦福数据挖掘教程·第三版》读书笔记(英文版) Chapter 2 MapReduce and the New Software Stack

来源&#xff1a;《斯坦福数据挖掘教程第三版》对应的公开英文书和PPT Chapter 2 MapReduce and the New Software Stack Computing cluster means large collections of commodity hardware, including conventional processors (“compute nodes”) connected by Ethernet …

学习HCIP的day.03

目录 OSPF&#xff1a;开放式最短路径优先协议 OSPF的数据包 -- 5种 OSPF的状态机 OSPF的工作过程 OSPF的基础配置 关于OSPF协议从邻居建立成为邻接的条件 OSPF的接口网络类型 OSPF&#xff1a;开放式最短路径优先协议 无类别链路状态型IGP协议&#xff1b;由于其基于拓…

golang Gin实现websocket

golang使用 Gin实现 websocket&#xff0c;这里笔者重新搭建一个项目 1、创建项目安装依赖 项目名为 go-gin-websocket 在指定文件夹下&#xff0c;新建项目文件夹 go-gin-websocket 进入项目文件夹&#xff0c;打开cmd窗口&#xff0c;在项目&#xff08;go-gin-websocket&a…

【Git 入门教程】第九节、Git的最佳实践

Git是一个强大的版本控制系统&#xff0c;可以帮助开发者管理和协调代码库。然而&#xff0c;正确使用Git并不总是容易。本文将介绍一些Git的最佳实践&#xff0c;以帮助开发者更好地利用Git来管理和协调代码库。 一、编写有意义的提交信息 在使用Git时&#xff0c;编写有意义…

Python编程IDE的选择

Python环境安装之后&#xff0c;接下来就是选择编写Python程序的编辑器了&#xff0c;这里就给大家推荐几种Python编辑器&#xff0c;我们简称这些编辑器为IDE。好的编程IDE可以提高编写代码效率&#xff0c;那咱话不多说&#xff0c;直接开始推荐&#xff01; IDLE 首先&…

有人USR-M100边缘主动上报电流数据到TCP服务器

前两天跟强哥配置了有人的USR-M100模块&#xff0c;实现了采集的电流信号主动上报服务器的功能&#xff0c;昨天去第一污水厂配置了1台、第二污水厂配置了5台、第三污水厂配置了1台&#xff0c;能够将数据上报到甲方的云平台&#xff0c;这里记录一下配置过程&#xff0c;方便以…

推荐4款免费好用的chatGPT平台

1 ShellGPT 这是一款出色的客户端&#xff0c;无需APIkey和科学上网即可访问chatGPT3.5以及绘画AI。项目的github地址如下&#xff1a;https://github.com/akl7777777/free-chatgpt-client-pub/&#xff0c;可在主页下载windows、linux和macOS的安装包&#xff0c;安装后即可使…

力扣刷题2023-04-30-1——题目:剑指 Offer II 007. 数组中和为 0 的三个数

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

React之动态路由创建以及解决刷新白屏问题

动态路由的创建和动态菜单的创建几乎类似&#xff0c;只不过的是&#xff0c;动态路由需要导入组件。这样才能完成跳转。 动态路由与动态菜单一样都需要封装一个转化函数&#xff0c;将后端传来的数据进行转换&#xff0c;转换成我们需要的格式。 需要导入的依赖 导入路由use…

kali: kali工具-Ettercap

kali工具-Ettercap ettercap工具&#xff1a; 用来进行arp欺骗&#xff0c;可以进行ARP poisoning&#xff08;arp投毒&#xff09;&#xff0c;除此之外还可以其他功能&#xff1a; ettercap工具的arp投毒可以截取web服务器、FTP服务器账号密码等信息&#xff0c;简略后打印出…