当前位置: 首页 > 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;又或者放在虚拟机或者服务器上与朋友进行…...

装饰模式(Decorator Pattern)重构java邮件发奖系统实战

前言 现在我们有个如下的需求&#xff0c;设计一个邮件发奖的小系统&#xff0c; 需求 1.数据验证 → 2. 敏感信息加密 → 3. 日志记录 → 4. 实际发送邮件 装饰器模式&#xff08;Decorator Pattern&#xff09;允许向一个现有的对象添加新的功能&#xff0c;同时又不改变其…...

Lombok 的 @Data 注解失效,未生成 getter/setter 方法引发的HTTP 406 错误

HTTP 状态码 406 (Not Acceptable) 和 500 (Internal Server Error) 是两类完全不同的错误&#xff0c;它们的含义、原因和解决方法都有显著区别。以下是详细对比&#xff1a; 1. HTTP 406 (Not Acceptable) 含义&#xff1a; 客户端请求的内容类型与服务器支持的内容类型不匹…...

RocketMQ延迟消息机制

两种延迟消息 RocketMQ中提供了两种延迟消息机制 指定固定的延迟级别 通过在Message中设定一个MessageDelayLevel参数&#xff0c;对应18个预设的延迟级别指定时间点的延迟级别 通过在Message中设定一个DeliverTimeMS指定一个Long类型表示的具体时间点。到了时间点后&#xf…...

黑马Mybatis

Mybatis 表现层&#xff1a;页面展示 业务层&#xff1a;逻辑处理 持久层&#xff1a;持久数据化保存 在这里插入图片描述 Mybatis快速入门 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/6501c2109c4442118ceb6014725e48e4.png //logback.xml <?xml ver…...

【JVM】- 内存结构

引言 JVM&#xff1a;Java Virtual Machine 定义&#xff1a;Java虚拟机&#xff0c;Java二进制字节码的运行环境好处&#xff1a; 一次编写&#xff0c;到处运行自动内存管理&#xff0c;垃圾回收的功能数组下标越界检查&#xff08;会抛异常&#xff0c;不会覆盖到其他代码…...

YSYX学习记录(八)

C语言&#xff0c;练习0&#xff1a; 先创建一个文件夹&#xff0c;我用的是物理机&#xff1a; 安装build-essential 练习1&#xff1a; 我注释掉了 #include <stdio.h> 出现下面错误 在你的文本编辑器中打开ex1文件&#xff0c;随机修改或删除一部分&#xff0c;之后…...

汽车生产虚拟实训中的技能提升与生产优化​

在制造业蓬勃发展的大背景下&#xff0c;虚拟教学实训宛如一颗璀璨的新星&#xff0c;正发挥着不可或缺且日益凸显的关键作用&#xff0c;源源不断地为企业的稳健前行与创新发展注入磅礴强大的动力。就以汽车制造企业这一极具代表性的行业主体为例&#xff0c;汽车生产线上各类…...

P3 QT项目----记事本(3.8)

3.8 记事本项目总结 项目源码 1.main.cpp #include "widget.h" #include <QApplication> int main(int argc, char *argv[]) {QApplication a(argc, argv);Widget w;w.show();return a.exec(); } 2.widget.cpp #include "widget.h" #include &q…...

AspectJ 在 Android 中的完整使用指南

一、环境配置&#xff08;Gradle 7.0 适配&#xff09; 1. 项目级 build.gradle // 注意&#xff1a;沪江插件已停更&#xff0c;推荐官方兼容方案 buildscript {dependencies {classpath org.aspectj:aspectjtools:1.9.9.1 // AspectJ 工具} } 2. 模块级 build.gradle plu…...

python报错No module named ‘tensorflow.keras‘

是由于不同版本的tensorflow下的keras所在的路径不同&#xff0c;结合所安装的tensorflow的目录结构修改from语句即可。 原语句&#xff1a; from tensorflow.keras.layers import Conv1D, MaxPooling1D, LSTM, Dense 修改后&#xff1a; from tensorflow.python.keras.lay…...