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

初探 Spring Boot Starter Security:构建更安全的Spring Boot应用

引言

Spring Boot 作为 Java 生态系统下的热门框架,以其简洁和易上手著称。而在构建 Web 应用程序时,安全性始终是开发者必须重视的一个方面。Spring Boot Starter Security 为开发者提供了一个简单但功能强大的安全框架,使得实现身份验证和授权变得相对容易。

本文将带你深入了解如何使用 Spring Boot Starter Security 来构建一个安全的 Spring Boot 应用,包括基本配置、常见用例以及一些技巧和最佳实践。

目录

  1. 什么是 Spring Boot Starter Security?
  2. 初始设置
    • 添加依赖
    • 基本配置
  3. 基本概念
    • 认证与授权
    • Filter 和 SecurityContext
  4. 示例:创建一个简单的安全应用
    • 设定用户角色
    • 自定义登录页面
    • 基于角色的访问控制
  5. 高级配置
    • 自定义 UserDetailsService
    • 自定义 Security Configuration
    • 使用 JWT 进行身份验证
  6. 综合示例:构建一个完整的安全应用
    • 项目结构
    • 代码实现
    • 测试和验证
  7. 最佳实践与常见问题
    • 安全最佳实践
    • 常见问题及解决方案
  8. 结论

1. 什么是 Spring Boot Starter Security?

Spring Boot Starter Security 是一个简化的 Spring Security 集成包,使得我们可以非常容易地在 Spring Boot 应用中添加强大的安全功能。它提供了一套灵活的工具和配置,用于实现认证和授权,使得应用程序更加安全。

2. 初始设置

添加依赖

首先,我们需要在 pom.xml 文件中添加 Spring Boot Starter Security 的依赖:

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

基本配置

在添加依赖后,Spring Security 会自动为我们的应用添加一些默认的安全配置,例如 HTTP Basic Authentication(基于 HTTP 的基础身份验证)。这意味着,我们可以立即看到应用要求用户进行身份验证。

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

此时,运行应用后,您会看到 Spring Boot 自动生成了一个密码,并在控制台输出。

3. 基本概念

认证与授权

  • 认证(Authentication):验证用户的身份。
  • 授权(Authorization):确定用户是否有权访问某个资源。

Filter 和 SecurityContext

Spring Security 通过一系列的过滤器(Filters)来处理安全逻辑。这些过滤器会拦截每个请求,并应用相应的认证和授权逻辑。所有安全相关的信息都会被存储在 SecurityContext 中,从而使得后续的请求处理可以基于这些信息进行访问控制。

4. 示例:创建一个简单的安全应用

设定用户角色

我们可以通过创建一个配置类来设定用户角色:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.inMemoryAuthentication().withUser("user").password(passwordEncoder().encode("password")).roles("USER").and().withUser("admin").password(passwordEncoder().encode("admin")).roles("ADMIN");}@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().antMatchers("/admin/**").hasRole("ADMIN").antMatchers("/user/**").hasRole("USER").and().formLogin();}
}

在上面的配置中,我们创建了两个用户(user 和 admin),并且设置了不同的角色(USER 和 ADMIN)。此外,我们还定义了不同 URL 路径对应的访问权限。

自定义登录页面

我们可以自定义一个登录页面,以增强用户体验:

<!DOCTYPE html>
<html>
<head><title>Login Page</title>
</head>
<body><h2>Login</h2><form method="post" action="/login"><div><label>Username: </label><input type="text" name="username"></div><div><label>Password: </label><input type="password" name="password"></div><div><button type="submit">Login</button></div></form>
</body>
</html>

WebSecurityConfig 中,我们需要指定这个自定义登录页面:

@Override
protected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().antMatchers("/admin/**").hasRole("ADMIN").antMatchers("/user/**").hasRole("USER").and().formLogin().loginPage("/login").permitAll();
}

基于角色的访问控制

上述配置已经体现了基于角色的基本访问控制。我们规定了 /admin/** 路径只能由拥有 ADMIN 角色的用户访问,而 /user/** 路径只能由拥有 USER 角色的用户访问。

5. 高级配置

自定义 UserDetailsService

有时候,我们需要从数据库加载用户信息。我们可以通过实现 UserDetailsService 接口来自定义加载用户的逻辑:

@Service
public class CustomUserDetailsService implements UserDetailsService {@Autowiredprivate UserRepository userRepository;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {User user = userRepository.findByUsername(username);if (user == null) {throw new UsernameNotFoundException("User not found.");}return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRoles()));}
}

自定义 Security Configuration

除了基本配置外,有些时候我们需要更灵活的配置。例如,我们可以完全覆盖默认的 Spring Security 配置:

@Configuration
@EnableWebSecurity
public class CustomSecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate CustomUserDetailsService userDetailsService;@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.csrf().disable().authorizeRequests().anyRequest().authenticated().and().formLogin().loginPage("/login").permitAll().and().logout().permitAll();}
}

使用 JWT 进行身份验证

JWT(JSON Web Token)是一种更加轻便的授权机制,我们可以采用它来替代 Session Cookie 进行身份验证。实现 JWT 需要进行以下几步:

  1. 添加 jwt 相关的依赖;
  2. 创建 token 提供者;
  3. 创建过滤器来验证 token ;
添加 JWT 依赖

pom.xml 中添加以下依赖:

<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version>
</dependency>
创建 TokenProvider
@Component
public class TokenProvider {private final String jwtSecret = "yourSecretKey";private final long jwtExpirationMs = 3600000;public String generateToken(Authentication authentication) {String username = authentication.getName();Date now = new Date();Date expiryDate = new Date(now.getTime() + jwtExpirationMs);return Jwts.builder().setSubject(username).setIssuedAt(now).setExpiration(expiryDate).signWith(SignatureAlgorithm.HS512, jwtSecret).compact();}public String getUsernameFromToken(String token) {return Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody().getSubject();}public boolean validateToken(String authToken) {try {Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);return true;} catch (SignatureException | MalformedJwtException | ExpiredJwtException | UnsupportedJwtException | IllegalArgumentException e) {e.printStackTrace();}return false;}
}
创建 JWT 过滤器
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {@Autowiredprivate TokenProvider tokenProvider;@Autowiredprivate CustomUserDetailsService customUserDetailsService;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)throws ServletException, IOException {try {String jwt = getJwtFromRequest(request);if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {String username = tokenProvider.getUsernameFromToken(jwt);UserDetails userDetails = customUserDetailsService.loadUserByUsername(username);UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));SecurityContextHolder.getContext().setAuthentication(authentication);}} catch (Exception ex) {logger.error("Could not set user authentication in security context", ex);}filterChain.doFilter(request, response);}private String getJwtFromRequest(HttpServletRequest request) {String bearerToken = request.getHeader("Authorization");if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {return bearerToken.substring(7);}return null;}
}
调整 Security Configuration
@EnableWebSecurity
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate JwtAuthenticationFilter jwtAuthenticationFilter;@Overrideprotected void configure(HttpSecurity http) throws Exception {http.cors().and().csrf().disable().authorizeRequests().antMatchers("/login", "/signup").permitAll().anyRequest().authenticated();http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);}
}

6. 综合示例:构建一个完整的安全应用

接下里,我们将创建一个功能更全的示例应用,结合之前介绍的各种配置,实现用户注册、登录、基于角色的访问控制和 JWT 身份验证。

项目结构

src└── main├── java│    └── com.example.security│         ├── controller│         ├── model│         ├── repository│         ├── security│         ├── service│         └── SecurityApplication.java└── resources├── templates└── application.yml

代码实现

模型类
@Entity
public class User {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;private String username;private String password;private String roles;  // e.g., "USER, ADMIN"// getters and setters
}
Repository
@Repository
public interface UserRepository extends JpaRepository<User, Long> {User findByUsername(String username);
}
UserDetailsService 实现
@Service
public class CustomUserDetailsService implements UserDetailsService {@Autowiredprivate UserRepository userRepository;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {User user = userRepository.findByUsername(username);if (user == null) {throw new UsernameNotFoundException("User not found.");}return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRoles()));}
}
安全配置
@EnableWebSecurity
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate JwtAuthenticationFilter jwtAuthenticationFilter;@Autowiredprivate CustomUserDetailsService customUserDetailsService;@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(customUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.cors().and().csrf().disable().authorizeRequests().antMatchers("/login", "/signup").permitAll().anyRequest().authenticated();http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);}
}
控制器
@RestController
public class AuthController {@Autowiredprivate AuthenticationManager authenticationManager;@Autowiredprivate CustomUserDetailsService userDetailsService;@Autowiredprivate TokenProvider tokenProvider;@PostMapping("/login")public ResponseEntity<?> authenticateUser(@RequestBody LoginRequest loginRequest) {Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getUsername(),loginRequest.getPassword()));SecurityContextHolder.getContext().setAuthentication(authentication);String jwt = tokenProvider.generateToken(authentication);return ResponseEntity.ok(new JwtAuthenticationResponse(jwt));}@PostMapping("/signup")public ResponseEntity<?> registerUser(@RequestBody SignUpRequest signUpRequest) {if(userRepository.existsByUsername(signUpRequest.getUsername())) {return new ResponseEntity<>(new ApiResponse(false, "Username is already taken!"), HttpStatus.BAD_REQUEST);}// Creating user's accountUser user = new User();user.setUsername(signUpRequest.getUsername());user.setPassword(passwordEncoder.encode(signUpRequest.getPassword()));user.setRoles("USER");userRepository.save(user);return ResponseEntity.ok(new ApiResponse(true, "User registered successfully"));}
}

测试和验证

我们已经完成了一个简单但是功能齐全的 Spring Boot 安全应用。可以通过以下步骤进行测试和验证:

  1. 启动应用
  2. 通过 /signup 端点进行用户注册
  3. 通过 /login 端点进行用户登录,并获取 JWT token
  4. 使用获取的 JWT token 访问其他受保护的端点

7. 最佳实践和常见问题

安全最佳实践

  • 使用强加密算法:如 BCryptPasswordEncoder 对密码进行加密存储。
  • 避免硬编码密码或密钥:将敏感信息存储在安全的配置文件或环境变量中。
  • 启用 CSRF 保护:对于需要借助表单提交的应用保持 CSRF 保护。
  • 定期更新依赖:检查依赖库的安全更新,避免使用有已知漏洞的库。
  • 输入验证:在用户输入点进行严格的输入验证,防止XSS和SQL注入等攻击。

常见问题及解决方案

问题1:为什么自定义登录页面不显示?

解决方案:确保在 WebSecurityConfig 中设置了 .loginPage("/login").permitAll(); 并且路径正确。

问题2:身份验证失败,显示 “Bad credentials”。

解决方案:确认用户名和密码是否正确,以及整体加密方式一致。

问题3:为什么 JWT 从请求中提取失败?

解决方案:确认请求头格式是否正确,Authorization: Bearer <token>,并且确保 JWT 过滤器在安全配置中正确添加。

结论

Spring Boot Starter Security 为开发者提供了丰富且灵活的安全配置选项,使得安全性实现变得相对简单。在本文中,我们探讨了基本概念和常见用例,并通过构建一个完整的示例应用,展示了其强大的功能。希望这些内容能帮助你在构建安全的 Spring Boot 应用时游刃有余。

通过对 Spring Boot Starter Security 的深入了解和实践,我们不仅增强了应用的安全性,还为用户提供了更为可靠的使用体验。继续学习和实践,你将在开发和维护安全应用的道路上走得更远。

相关文章:

初探 Spring Boot Starter Security:构建更安全的Spring Boot应用

引言 Spring Boot 作为 Java 生态系统下的热门框架&#xff0c;以其简洁和易上手著称。而在构建 Web 应用程序时&#xff0c;安全性始终是开发者必须重视的一个方面。Spring Boot Starter Security 为开发者提供了一个简单但功能强大的安全框架&#xff0c;使得实现身份验证和…...

【无标题】思科交换路由中路由引入实验指南

路由引入是网络设计中的一个重要概念&#xff0c;它允许不同路由协议之间的路由信息交换。在思科网络设备中&#xff0c;路由引入可以增强网络的连通性和效率。本文将介绍路由引入的基本概念&#xff0c;并通过一个实验来演示如何在思科路由器中实现路由引入。 ## 路由引入的基…...

基于yolov2深度学习网络的昆虫检测算法matlab仿真,并输出昆虫数量和大小判决

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 5.算法完整程序工程 1.算法运行效果图预览 2.算法运行软件版本 matlab2022A 3.部分核心程序 .......................................................... for i 1:12 % 遍历结…...

Java进阶学习笔记2——static

static&#xff1a; 叫静态&#xff0c;可以修饰成员变量、成员方法。 成员变量按照有无static修饰&#xff0c;分为两种&#xff1a; 类变量&#xff1a;有static修饰&#xff0c;属于类&#xff0c;在计算机中只有一份&#xff0c;会被类的全部对象共享。静态成员变量。 实…...

spring boot集成Knife4j

文章目录 一、Knife4j是什么&#xff1f;二、使用步骤1.引入依赖2.新增相关的配置类3.添加配置信息4.新建测试类5. 启动项目 三、其他版本集成时常见异常1. Failed to start bean ‘documentationPluginsBootstrapper2.访问地址后报404 一、Knife4j是什么&#xff1f; 前言&…...

redis核心面试题一(架构原理+RDB+AOF)

文章目录 0. redis与mysql区别1. redis是单线程架构还是多线程架构2. redis单线程为什么这么快3. redis过期key删除策略4. redis主从复制架构原理5. redis哨兵模式架构原理6. redis高可用集群架构原理7. redis持久化之RDB8. redis持久化之AOF9. redis持久化之混合持久化 0. red…...

STM32F1之SPI通信·软件SPI代码编写

目录 1. 简介 2. 硬件电路 移位示意图 3. SPI时序基本单元 3.1 起始条件 3.2 终止条件 3.3 交换一个字节&#xff08;模式0&#xff09; 3.4 交换一个字节&#xff08;模式1&#xff09; 3.5 交换一个字节&#xff08;模式2&#xff09; 3.6 交换一个字节&a…...

实战:生成个性化词云的Python实践【7个案例】

文本挖掘与可视化&#xff1a;生成个性化词云的Python实践【7个案例】 词云&#xff08;Word Cloud&#xff09;&#xff0c;又称为文字云或标签云&#xff0c;是一种用于文本数据可视化的技术&#xff0c;通过不同大小、颜色和字体展示文本中单词的出现频率或重要性。在词云中…...

云存储与云计算详解

1. 云存储与云计算概述 1.1 云存储 云存储&#xff08;Cloud Storage&#xff09;是指通过互联网将数据存储在远程服务器上&#xff0c;用户可以随时随地访问和管理这些数据。云存储的优点包括高可扩展性、灵活性和成本效益。 1.2 云计算 云计算&#xff08;Cloud Computin…...

【飞舞的花瓣】飞舞的花瓣代码||樱花代码||表白代码(完整代码)

关注微信公众号「ClassmateJie」有完整代码以及更多惊喜等待你的发现。 简介/效果展示 这段代码是一个HTML页面&#xff0c;其中包含一个canvas元素和相关的JavaScript代码。这个页面创建了一个飘落花瓣的动画效果。 代码【获取完整代码关注微信公众号「ClassmateJie」回复“…...

网络安全的重要组成部分:数据库审计

数据库审计&#xff08;简称DBAudit&#xff09;以安全事件为中心&#xff0c;以全面审计和精确审计为基础&#xff0c;实时记录网络上的数据库活动&#xff0c;对数据库操作进行细粒度审计的合规性管理&#xff0c;对数据库遭受到的风险行为进行实时告警。它通过对用户访问数据…...

gc和gccgo编译器

Go 语言有两个主要的编译器&#xff0c;分别是 Go 编译器&#xff08;通常简称为 gc&#xff09;和 GCCGO。它们之间有一些重要的异同点&#xff1a; gc 编译器&#xff1a; gc 是 Go 语言的官方编译器&#xff0c;由 Go 语言的开发团队维护。它是 Go 语言最常用的编译器&#…...

开放重定向漏洞

开放重定向漏洞 1.开放重定向漏洞概述2.攻击场景&#xff1a;开放重定向上传 svg 文件3.常见的注入参数 1.开放重定向漏洞概述 开放重定向漏洞&#xff08;Open Redirect&#xff09;是指Web应用程序接受用户提供的输入&#xff08;通常是URL参数&#xff09;&#xff0c;并将…...

基于YoloV4汽车多目标跟踪计数

欢迎大家点赞、收藏、关注、评论啦 &#xff0c;由于篇幅有限&#xff0c;只展示了部分核心代码。 文章目录 一项目简介 二、功能三、系统四. 总结 一项目简介 一、项目背景与意义 随着城市交通的快速发展&#xff0c;交通流量和车辆密度的不断增加&#xff0c;对交通管理和控…...

交叉编译程序,提示 incomplete type “struct sigaction“ is not allowed

问题描述 incomplete type "struct sigaction" is not allowed解决办法 在代码的最顶端添加如下代码即可 #define _XOPEN_SOURCE此定义不是简单的宏定义&#xff0c;是使程序符合系统环境的不可缺少的部分 _XOPEN_SOURCE为了实现XPG&#xff1a;The X/Open Porta…...

叶面积指数(LAI)数据、NPP数据、GPP数据、植被覆盖度数据获取

引言 多种卫星遥感数据反演叶面积指数&#xff08;LAI&#xff09;产品是地理遥感生态网推出的生态环境类数据产品之一。产品包括2000-2009年逐8天数据&#xff0c;值域是-100-689之间&#xff0c;数据类型为32bit整型。该产品经过遥感数据获取、计算归一化植被指数、解译植被类…...

光环P3O不错的一个讲座

光环P3O不错的一个讲座&#xff0c;地址&#xff1a;https://apphfuydjku5721.h5.xiaoeknow.com/v2/course/alive/l_663dc840e4b0694c62c32d1d?app_idapphfuydJkU5721&share_fromu_5c987304d8515_wH2E5HgCgx&share_type5&share_user_idu_5c987304d8515_wH2E5HgCgx…...

Typescnipt 学习笔记

TypeScript 学习笔记 一、什么是 TypeScript TypeScript 是一种由微软开发的开源编程语言&#xff0c;它是 JavaScript 的一个超集。它添加了静态类型和面向对象的特性&#xff0c;并提供了更强大的工具和功能&#xff0c;以增强 JavaScript 的开发体验。 二、为什么要学习 …...

如何在 Ubuntu 24.04 (桌面版) 上配置静态IP地址 ?

如果你想在你的 Ubuntu 24.04 桌面有一个持久的 IP 地址&#xff0c;那么你必须配置一个静态 IP 地址。当我们安装 Ubuntu 时&#xff0c;默认情况下 DHCP 是启用的&#xff0c;如果网络上可用&#xff0c;它会尝试从 DHCP 服务器获取 IP 地址。 在本文中&#xff0c;我们将向…...

小恐龙跳一跳源码

小恐龙跳一跳源码是前两年就火爆过一次的小游戏源码&#xff0c;不知怎么了今年有火爆了&#xff0c;所以今天就吧这个源码分享出来了&#xff01;有喜欢的直接下载就行&#xff0c;可以本地单机直接点击index.html进行运行&#xff0c;又或者放在虚拟机或者服务器上与朋友进行…...

ubuntu清理垃圾

windows和ubuntu 双系统&#xff0c;ubuntu 150GB&#xff0c;开发用&#xff0c;基本不装太多软件。但是磁盘基本用完。 1、查看home目录 sudo du -h -d 1 $HOME | grep -v K 上面的命令查看$HOME一级目录大小&#xff0c;发现 .cache 有26GB&#xff0c;.local 有几个GB&am…...

如何使用CodeRider插件在IDEA中生成代码

一、环境搭建与插件安装 1.1 环境准备 名称要求说明操作系统Windows 11JetBrains IDEIntelliJ IDEA 2025.1.1.1 (Community Edition)硬件配置推荐16GB内存50GB磁盘空间 1.2 插件安装流程 步骤1&#xff1a;市场安装 打开IDEA&#xff0c;进入File → Settings → Plugins搜…...

Steam爬取相关游戏评测

## 因为是第一次爬取Steam。所以作为一次记录发出&#xff1b;有所错误欢迎指出。 无时间指定爬取 import requests import time import csv import osappid "553850" # 这里你也可以改成 #appid int(input()) max_reviews 10000 # 想爬多少条 # max_reviews…...

SpringCloud——Nacos

1、核心功能&#xff1a; 服务注册与发现&#xff1a; 服务实例可动态注入到Nacos中&#xff0c;消费者通过服务名发现可用实例。 // 启用EnableDiscoveryClient注解启用Nacos SpringBootApplication EnableDiscoveryClient public class UserServiceApplication {public st…...

centos挂载目录满但实际未满引发系统宕机

测试服务器应用系统突然挂了&#xff0c;经过排查发现是因为磁盘“满了”导致的&#xff0c;使用df -h查看磁盘使用情况/home目录使用率已经到了100%,但使用du -sh /home查看发现实际磁盘使用还不到1G&#xff0c;推测有进程正在写入或占用已删除的大文件&#xff08;Linux 系统…...

2. Web网络基础 - 协议端口

深入解析协议端口与netstat命令&#xff1a;网络工程师的实战指南 在网络通信中&#xff0c;协议端口是服务访问的门户。本文将全面解析端口概念&#xff0c;并通过netstat命令实战演示如何监控网络连接状态。 一、协议端口核心知识解析 1. 端口号的本质与分类 端口范围类型说…...

LeetCode--25.k个一组翻转链表

解题思路&#xff1a; 1.获取信息&#xff1a; &#xff08;1&#xff09;给定一个链表&#xff0c;每k个结点一组进行翻转 &#xff08;2&#xff09;余下不足k个结点&#xff0c;则不进行交换 2.分析题目&#xff1a; 其实就是24题的变题&#xff0c;24题是两两一组进行交换&…...

因泰立科技H1X激光雷达:因泰立科技为智慧工业注入新动力

在当今工业领域&#xff0c;精准测量与高效作业是推动产业升级的关键因素。因泰立科技推出的H1X三维轮廓扫描激光雷达&#xff0c;凭借其卓越的性能和广泛的应用场景&#xff0c;正成为智慧工业中不可或缺的高科技装备。 产品简介 H1X三维轮廓扫描激光雷达是因泰立科技基于二维…...

asp.net mvc如何简化控制器逻辑

在ASP.NET MVC中&#xff0c;可以通过以下方法简化控制器逻辑&#xff1a; ASP.NET——MVC编程_aspnet mvc-CSDN博客 .NET/ASP.NET MVC Controller 控制器&#xff08;IController控制器的创建过程&#xff09; https://cloud.tencent.com/developer/article/1015115 【转载…...

免费批量PDF转Word工具

免费批量PDF转Word工具 工具简介 这是一款简单易用的批量PDF转Word工具&#xff0c;支持&#xff1a; 批量转换多个PDF文件保留原始格式和布局快速高效的转换速度完全免费使用 工具地址 下载链接 网盘下载地址&#xff1a;点击下载 提取码&#xff1a;8888 功能特点 ✅…...