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

Spring Security 6 系列之九 - 集成JWT

之所以想写这一系列,是因为之前工作过程中使用Spring Security,但当时基于spring-boot 2.3.x,其默认的Spring Security是5.3.x。之后新项目升级到了spring-boot 3.3.0,结果一看Spring Security也升级为6.3.0,关键是其风格和内部一些关键Filter大改,导致在配置同样功能时,多费了些手脚,因此花费了些时间研究新版本的底层原理,这里将一些学习经验分享给大家。

注意由于框架不同版本改造会有些使用的不同,因此本次系列中使用基本框架是 spring-boo-3.3.0(默认引入的Spring Security是6.3.0),JDK版本使用的是19,所有代码都在spring-security-study项目上:https://github.com/forever1986/spring-security-study.git

目录

  • 1 JWT
    • 1.1 为什么使用JWT
    • 1.2 JWT格式
  • 2 Spring Security集成JWT
    • 2.1 项目新建
    • 2.2 返回异常处理类
    • 2.3 自定义基于数据库的用户
    • 2.4 Redis配置
    • 2.5 自定义登录和登出接口
    • 2.6 JWT配置
    • 2.7 SecurityConfig的配置
    • 2.8 测试

上两章中,我们讲了异常处理和前后端分离,其实都是为了本章做准备的。在系列6中我们说很多业务已经开始摒弃Session,而是采用JWT方式。这一章就从JWT开始讲,到最后实现一个Spring Security+JWT完整方案。

1 JWT

1.1 为什么使用JWT

最开始的前后端分离之间的交互是通过保持状态的Session方式,也就服务器通过生成Session信息保存在服务器中并返回给客户端,客户端(通常是浏览器)将信息存入Cookie里面,每次访问都是拿着Cookie访问(一般浏览器自动完成),服务器通过客户端传输的信息与服务器的Session进行比对验证。因为Session属于有状态的,因此有这么两个大缺点:

  • 缺点一:服务器要存储Session信息,如果并发量较大,对于服务器来说需要占用很大内存
  • 缺点二:Session信息默认存储在服务器上,这对于集群部署来说就不适用,还需要一个同一个存储的地方,比如Redis(系列6中已经讲过)

基于以上两个缺点,JWT就出现了。

  • JWT是一种无状态,也就是认证的时候,服务器给客户端一个token之后,服务器无需保留token
  • 由于无状态,因此解决集群部署问题,客户端携带token的访问,在任何服务器都可以鉴权
  • JWT包括3部分:header(声明和加密算法)、Payload(有效数据)、Signature(签名,是整个数据的认证信息,保证认证安全性和有效性)。

1.2 JWT格式

JWT 包含三部分数据:

Header:头部,通常头部有两部分信息:
声明类型(typ),这里是JWT
加密算法(alg),自定义

这部分会采用 Base64编码,得到数据。

Payload:载荷,就是设置想要存储的数据,你可以将部分信息放在token中,以下是规范中的内置对象:

  • iss (issuer):表示签发人
  • exp (expiration time):表示token过期时间
  • sub (subject):内容,可以使用json放置用户基本信息
  • aud (audience):受众
  • nbf (Not Before):生效时间
  • iat (Issued At):签发时间
  • jti (JWT ID):编号

这部分会采用 Base64编码,得到数据。

Signature:签名,是对数据进行认证的签名。根据前两步的数据+密钥 secret(密码可以是对称或非对称),通过 Header 中配置的加密算法(alg)进行验证。用于验证整个数据完整和可靠性。

以下是一个JWT数据,可以使用在线JWT解析工具解析:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJkMDk0MDBmODY5ZmY0MjgxOGYxOTFjZTE4ZDEwNjU2ZiIsInN1YiI6InVzZXI6NCIsImlzcyI6InNwcmluZy1zZWN1cml0eS1zdHVkeSIsImlhdCI6MTczNDQ4ODkzNywiZXhwIjoxNzM0NDg5MjM3fQ.X9V8DgX8ay8vQRLdA7tV8-MHKJxPv9e2gLqDs3Fpk9s

2 Spring Security集成JWT

代码参考lesson09子模块

本项目说明

  • 本项目依赖mysq和Redis,其中mysq中创建一个数据库,名为spring_security_study,创建用户表t_user(参考系列2)
  • 采用基于数据库用户的认证
  • 采用前后端分离,会自定义登录和登出接口,同时会屏蔽默认登录登出页面
  • 使用JWT生成token认证,因此会屏蔽默认Session管理,由于没有Session管理,因此我们需要自己做token管理。这里使用Redis存储token,同时自定义JwtAuthenticationTokenFilter过滤器,用于在访问时验证token,设置SecurityContext。

2.1 项目新建

1)新建lesson09子模块,其pom引入依赖如下:

<dependencies><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><!--Spring Boot 提供的 Security 启动器 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-spring-boot3-starter</artifactId></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>javax.xml.bind</groupId><artifactId>jaxb-api</artifactId></dependency></dependencies>

2)创建com.demo.lesson09包,并创建启动类SecurityLesson09Application

3)在controller包下,新增DemoController,用于测试

@RestController
public class DemoController {@GetMapping("/demo")public String demo() {return"demo";}}

4)在resources下新增yaml,配置Mysql、redis和mybatis配置

server:port: 8080
spring:# 配置数据源datasource:type: com.alibaba.druid.pool.DruidDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://127.0.0.1:3306/spring_security_study?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=trueusername: rootpassword: rootdruid:initial-size: 5min-idle: 5maxActive: 20maxWait: 3000timeBetweenEvictionRunsMillis: 60000minEvictableIdleTimeMillis: 300000validationQuery: select 'x'testWhileIdle: truetestOnBorrow: falsetestOnReturn: falsepoolPreparedStatements: falsefilters: stat,wall,slf4jconnectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000;socketTimeout=10000;connectTimeout=1200#redis配置data:redis:host: 127.0.0.1mybatis-plus:global-config:banner: falsemapper-locations: classpath:mappers/*.xmltype-aliases-package: com.demo.lesson05.entityconfiguration:cache-enabled: falselocal-cache-scope: statement

2.2 返回异常处理类

1)增加result包和handler包,里面的类参考lesson07子模块,拷贝以下类:

  • IResultCode
  • Result
  • ResultCode
  • DemoAccessDeniedHandler : 授权异常处理
  • DemoAuthenticationEntryPoint :认证异常处理

2.3 自定义基于数据库的用户

1)不清楚的可以回顾lesson03子模块,这里做了一些小改动

2) 在entity包下新增LoginUserDetails类,自定义类为了能够Redis序列化,之前使用Security默认的User,在Redis反序列化会报错

@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class LoginUserDetails implements UserDetails {private TUser tUser;@Override@JsonIgnorepublic Collection<? extends GrantedAuthority> getAuthorities() {return List.of();}@Overridepublic String getPassword() {return tUser.getPassword();}@Overridepublic String getUsername() {return tUser.getUsername();}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;}
}

3)定义entity包下的TUser和对应mapper包下的TUserMapper

@Data
public class TUser {@TableId(type = IdType.ASSIGN_ID)private Long id;private String username;private String password;private String email;private String phone;}
@Mapper
public interface TUserMapper {// 根据用户名,查询用户信息@Select("select * from t_user where username = #{username}")TUser selectByUsername(String username);}

4)在service包下,建立JdbcUserDetailsServiceImpl(实现UserDetailsService),用于基于数据库查询用户

@Service
public class JdbcUserDetailsServiceImpl implements UserDetailsService {@Autowiredprivate TUserMapper tUserMapper;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 查询自己数据库的用户信息TUser user = tUserMapper.selectByUsername(username);if(user == null){throw new UsernameNotFoundException(username);}return new LoginUserDetails(user);}}

2.4 Redis配置

1)在config包下,新增RedisConfiguration,用于Redis的序列化配置

@Configuration
public class RedisConfiguration {/*** 主要做redis配置。redis有2种不同的template(2种的key不能共享)* 1.StringRedisTemplate:以String作为存储方式:默认使用StringRedisTemplate,其value都是以String方式存储* 2.RedisTemplate:*    1)使用默认RedisTemplate时,其value都是根据jdk序列化的方式存储*    2)自定义Jackson2JsonRedisSerializer序列化,以json格式存储,其key与StringRedisTemplate共享,返回值是LinkedHashMap*    3)自定义GenericJackson2JsonRedisSerializer序列化,以json格式存储,其key与StringRedisTemplate共享,返回值是原先对象(因为保存了classname)*/@Bean@ConditionalOnMissingBean({RedisTemplate.class})public RedisTemplate redisTemplate(RedisConnectionFactory factory) {RedisTemplate<String, Object> template = new RedisTemplate();template.setConnectionFactory(factory);//本实例采用GenericJackson2JsonRedisSerializerObjectMapper om = new ObjectMapper();om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();template.setKeySerializer(stringRedisSerializer);template.setHashKeySerializer(stringRedisSerializer);template.setValueSerializer(jackson2JsonRedisSerializer);template.setHashValueSerializer(jackson2JsonRedisSerializer);template.afterPropertiesSet();return template;}@Bean@ConditionalOnMissingBean({StringRedisTemplate.class})public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {StringRedisTemplate template = new StringRedisTemplate();template.setConnectionFactory(factory);return template;}}

2.5 自定义登录和登出接口

1)在entity包下定义LoginDTO类,用于前后端参数传输

@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginDTO {private String username;private String password;
}

2)在service包下,定义LoginService接口以及实现类LoginServiceImpl,做登录和登出逻辑

public interface LoginService {Result<String> login(LoginDTO loginDTO);Result<String> logout();
}
@Service
public class LoginServiceImpl implements LoginService {// 注入AuthenticationManagerBuilder,用于获得authenticationManager@Autowiredprivate AuthenticationManagerBuilder authenticationManagerBuilder;@Autowiredprivate RedisTemplate redisTemplate;private static String PRE_KEY = "user:";@Overridepublic Result<String> login(LoginDTO loginDTO) {String username = loginDTO.getUsername();String password = loginDTO.getPassword();UsernamePasswordAuthenticationToken authenticationToken = UsernamePasswordAuthenticationToken.unauthenticated(username, password);try {Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);if(authentication!=null && authentication.isAuthenticated()){SecurityContextHolder.getContext().setAuthentication(authentication);LoginUserDetails user = (LoginUserDetails)authentication.getPrincipal();String subject = PRE_KEY + user.getTUser().getId();String token = JwtUtil.createToken(subject, 1000*60*5L);redisTemplate.opsForValue().set(subject, user, 1000*60*5L, TimeUnit.MILLISECONDS);return Result.success(token);}}catch (AuthenticationException e){return Result.failed(e.getLocalizedMessage());}catch (Exception e){e.printStackTrace();}return Result.failed("认证失败");}@Overridepublic Result<String> logout() {if(SecurityContextHolder.getContext().getAuthentication()!=null){LoginUserDetails user = (LoginUserDetails)SecurityContextHolder.getContext().getAuthentication().getPrincipal();if(user!=null){String key = PRE_KEY + user.getTUser().getId();redisTemplate.delete(key);}else {return Result.failed("登出失败,用户不存在");}}return Result.success("登出成功");}
}

3)在controller包下,新增LoginController ,对外发布登录和登出

@RestController
public class LoginController {@Autowiredprivate LoginService loginService;@PostMapping("/login")public Result<String> login(@RequestBody LoginDTO loginDTO) {return loginService.login(loginDTO);}@PostMapping("/logout")public Result<String> logout() {return loginService.logout();}}

2.6 JWT配置

1)在utils包下面新建JwtUtil工具类,用来生成token和验证token。

这里采用HS256对称加密,密钥我们这里就直接写在代码中,有兴趣朋友可以改为如AES非对称加密算法

/*** JWT工具类*/
public class JwtUtil {//有效期为public static final Long JWT_TTL = 60 * 60 * 1000L; // 60 * 60 *1000  一个小时//设置秘钥明文public static final String JWT_KEY = "moo";public static String getUUID(){return UUID.randomUUID().toString().replaceAll("-", "");}/*** 创建token*/public static String createToken(String subject) {return getJwtBuilder(subject, null, getUUID());// 设置过期时间}/*** 创建token*/public static String createToken(String subject, Long ttlMillis) {return getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间}/*** 创建token*/public static String createToken(String id, String subject, Long ttlMillis) {return getJwtBuilder(subject, ttlMillis, id);// 设置过期时间}/*** 解析token*/public static String parseJWT(String token) throws Exception {SecretKey secretKey = generalKey();return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();}private static String getJwtBuilder(String subject, Long ttlMillis, String uuid) {SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;SecretKey secretKey = generalKey();long nowMillis = System.currentTimeMillis();Date now = new Date(nowMillis);if(ttlMillis==null){ttlMillis=JwtUtil.JWT_TTL;}long expMillis = nowMillis + ttlMillis;Date expDate = new Date(expMillis);return Jwts.builder().setHeaderParam(Header.TYPE, Header.JWT_TYPE).setId(uuid)            //唯一的ID.setSubject(subject)    // 主题  可以是JSON数据.setIssuer("spring-security-study")       // 签发者.setIssuedAt(now)       // 签发时间.signWith(signatureAlgorithm, secretKey)    //使用HS256对称加密算法签名, 第二个参数为秘钥.setExpiration(expDate).compact();}/*** 生成加密后的秘钥 secretKey*/private static SecretKey generalKey() {byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");return key;}}

2)定义自己的Filter(JwtAuthenticationTokenFilter),通过该Filter进行token验证,并模拟设置SecurityContext。

我们知道访问资源最终在AuthorizationFilter过滤器会验证你的权限,是通过获取SecurityContext得到登录信息
但是使用前后端分离,且不使用Session管理,这就意味着登录之后,第二次请求Security会认为没有登录(这个可以去看看SecurityContextHolderFilter,会发现不再使用HttpSessionSecurityContextRepository)这也就意味着SecurityContext不会被设置
从Security Context原理中我们知道每个请求都是通过Filter过滤链验证,那么只需要我们在链中加入自己的认证,并设置SecurityContext,就能够达到一样的效果。
注意

  • 这里JwtAuthenticationTokenFilter 继承OncePerRequestFilter 而不是GenericFilterBean,只是为了保证每次请求只走一次JwtAuthenticationTokenFilter
  • JWT验证失败返回BadCredentialsException异常,这个在自定义异常处理讲过其原理,通过抛出BadCredentialsException可以被exceptionHandling处理
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {@Autowiredprivate RedisTemplate redisTemplate;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {// 过滤login接口if("/login".equals(request.getRequestURI())){filterChain.doFilter(request, response);return;}// 从请求头获取tokenString token = request.getHeader("access_token");// 检查获取到的token是否为空或空白字符串。(判断给定的字符串是否包含文本)if (!StringUtils.hasText(token)) {// 如果token为空,则直接放行请求到下一个过滤器,不做进一步处理并结束当前方法,不继续执行下面代码。filterChain.doFilter(request, response);return;}// 解析tokenString userAccount;try {userAccount = JwtUtil.parseJWT(token);} catch (Exception e) {e.printStackTrace();throw new BadCredentialsException("token非法");}// 临时缓存中 获取 键 对应 数据Object object = redisTemplate.opsForValue().get(userAccount);LoginUserDetails loginUser = (LoginUserDetails)object;if (Objects.isNull(loginUser)) {throw new BadCredentialsException("用户未登录");}// 将用户信息存入 SecurityConText// UsernamePasswordAuthenticationToken 存储用户名 密码 权限的集合UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, null);// SecurityContextHolder是Spring Security用来存储当前线程安全的认证信息的容器。// 将用户名 密码 权限的集合存入SecurityContextHolderSecurityContextHolder.getContext().setAuthentication(authenticationToken);// 放行filterChain.doFilter(request, response);}
}

2.7 SecurityConfig的配置

1)在config包下,配置SecurityConfig

@Configuration
@EnableWebSecurity
public class SecurityConfig {@Autowiredprivate JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests(auth->auth//允许/login访问.requestMatchers("/login").permitAll().anyRequest().authenticated())// 异常处理.exceptionHandling(handling-> handling.accessDeniedHandler(new DemoAccessDeniedHandler()).authenticationEntryPoint(new DemoAuthenticationEntryPoint()))// 禁用csrf,因为登录和登出是post请求,csrf会屏蔽掉post请求.csrf(AbstractHttpConfigurer::disable)// 添加到过滤器链路中,确保在AuthorizationFilter过滤器之前.addFilterBefore(jwtAuthenticationTokenFilter, AuthorizationFilter.class)// 由于采用token方式认证,因此可以关闭session管理.sessionManagement(SessionManagementConfigurer::disable)// 禁用原来登录页面.formLogin(AbstractHttpConfigurer::disable)// 禁用系统原有的登出.logout(LogoutConfigurer::disable);return http.build();}
}

2.8 测试

1)直接访问:http://127.0.0.1:8080/demo 会给出一个无权限的返回Json数据
在这里插入图片描述
2)访问登录接口:http://127.0.0.1:8080/login
在这里插入图片描述

3)再次访问:http://127.0.0.1:8080/demo ,注意加入第二步中的token
在这里插入图片描述

4)访问登出:http://127.0.0.1:8080/logout,注意加入第二步中的token
在这里插入图片描述
5)再次:http://127.0.0.1:8080/demo ,注意加入第二步中的token
在这里插入图片描述

结语:至此,我们就完整实现了基于JWT的Spring Security认证过程。这实际上已经接近生产使用情况。到目前为止,我们了解了认证、授权、前后端分离、JWT等内容,接下来,我们将会讲述Spring Security更高级的一些功能。

相关文章:

Spring Security 6 系列之九 - 集成JWT

之所以想写这一系列&#xff0c;是因为之前工作过程中使用Spring Security&#xff0c;但当时基于spring-boot 2.3.x&#xff0c;其默认的Spring Security是5.3.x。之后新项目升级到了spring-boot 3.3.0&#xff0c;结果一看Spring Security也升级为6.3.0&#xff0c;关键是其风…...

大数据实验三

Python and anaconda 实验三数据预处理和轨迹聚类参考地址&#xff1a; https://www.hifleet.com/wp/communities/data/hangyundashujujishukechengshiyanzhinanshujuyuchulijiguijijuleichixugengxinzhong#post-2212https://www.hifleet.com/wp/communities/data/hangyundas…...

《计算机网络(第7版)-谢希仁》期末考试复习题和答案(总结整理)

目录 前言&#xff1a; 一、选择题。 二、填空题。 三、名词解释。 四、简答题。 前言&#xff1a; 这个自动标题自己带了序号&#xff0c;一开始想全部选项和题号都改过来的&#xff0c;结果一看一百多个全是&#xff0c;懒得改了 一、选择题。 1、广域网覆盖的地理范围…...

学习笔记 --C#基础其他知识点(持续更新)

C#中的同步和异步《一》 以下理解借鉴博客&#xff1a;借鉴博客地址1 异步编程&#xff08;Asynchronous&#xff09; 允许任务在后台执行&#xff0c;而不会阻塞调用线程。C#使用async和await关键字 async Task AsynchronousMethod() {// 等待异步操作完成await Task.Delay…...

STM32 高级 谈一下IPV4/默认网关/子网掩码/DNS服务器/MAC

首先可以通过 winr->输入cmd->输入ipconfig 命令可以查看计算机的各种地址 IPV4&#xff1a;是互联网协议第 4 版&#xff08;Internet Protocol version 4&#xff09;所使用的地址。它是一个 32 位的二进制数字&#xff0c;通常被分为 4 个 8 位的部分&#xff…...

智能家居实训室中,STC单片机驱动的“互联网+”智能家居系统设计

一、引言 随着经济的快速发展&#xff0c;人们对家居环境的智能化、网络化需求日益增强&#xff0c;智能家居的研究也因此受到了国内外相关机构的广泛关注。STC单片机凭借其卓越的性能和广泛的应用领域&#xff0c;成为了智能家居系统设计的优选方案。作为一种先进的微控制器&…...

esp32学习:用虫洞ESP32S3-EYE开发板快速实现USB摄像头(UVC免驱)

直接上干货&#xff1a;实现一个USB摄像头&#xff0c;免驱UVC设备。 硬件准备&#xff1a; 乐官方推荐的Cam开发板就是乐鑫带摄像头OV2604的esp32-s3-eye&#xff0c;我们虫洞esp32-s3-eye完全兼容这个板子哦&#xff0c;虫洞ESP32-S3-EYE 人脸识别 esp-cam升级 OpenCV LVGL …...

Python 面向对象编程 五(结束)组合

Python 面向对象编程 五&#xff08;结束&#xff09;组合 组合 组合 组合是面向对象编程中另一个流行的概念&#xff0c;它与封装也有一定关系。简单地说&#xff0c;组成是指在一个对象中包含一个或多个对象&#xff0c;从而形成一个真实世界的对象。包含其他类对象的类称为…...

基于微信小程序的校园访客登记系统

基于微信小程序的校园访客登记系统 功能列表 用户端功能 注册与登录 &#xff1a;支持用户通过手机号短信验证码注册和登录。个人资料管理 &#xff1a;允许用户编辑和更新个人信息及其密码。站内信消息通知&#xff1a;通知公告。来访预约&#xff1a;提交来访预约支持车牌…...

docker 部署mysql8

在Docker中部署MySQL 8是一个相对简单的过程。以下是基本的步骤&#xff1a; 拉取MySQL 8镜像&#xff1a; 使用Docker命令拉取最新的MySQL 8镜像&#xff1a; docker pull registry.openanolis.cn/openanolis/mysql:8.0.30-8.6创建并运行MySQL容器&#xff1a; 创建并运行MySQ…...

聊一聊 C#前台线程 如何阻塞程序退出

一&#xff1a;背景 1. 讲故事 这篇文章起源于我的 C#内功修炼训练营里的一位朋友提的问题&#xff1a;后台线程的内部是如何运转的 ? &#xff0c;犹记得C# Via CLR这本书中 Jeffery 就聊到了他曾经给别人解决一个程序无法退出的bug&#xff0c;最后发现是有一个 Backgrond…...

【编译原理】往年题汇总(山东大学软件学院用)

&#x1f308; 个人主页&#xff1a;十二月的猫-CSDN博客 &#x1f525; 系列专栏&#xff1a; &#x1f3c0;编译原理_十二月的猫的博客-CSDN博客 &#x1f4aa;&#x1f3fb; 十二月的寒冬阻挡不了春天的脚步&#xff0c;十二点的黑夜遮蔽不住黎明的曙光 目录 1. 前言 2. …...

【原创学习笔记】近期项目中使用的西门子V20变频器总结(上篇)

现场V20 22kW变频器如图所示 进线分别为L1,L2,L3,PE线&#xff0c;出现分别为U,V,W接电机 在西门子官网查询手册后&#xff0c;查询可知可以通过多种方式控制变频器&#xff0c;比如&#xff1a;面板&#xff08;BOP&#xff09;控制&#xff0c;端子&#xff08;NPN/PNP&…...

IndexOf Apache Web For Liunx索引服务器部署及应用

Apache HTTP Server 是一款广泛使用的开源网页服务器软件,它支持多种协议,包括 HTTP、HTTPS、FTP 等 IndexOf 功能通常指的是在一个目录中自动生成一个索引页面的能力,这个页面会列出该目录下所有的文件和子目录。比如网上经常看到的下图展现的效果,那么接下来我们就讲一下…...

Pytorch | 利用PI-FGSM针对CIFAR10上的ResNet分类器进行对抗攻击

Pytorch | 利用PI-FGSM针对CIFAR10上的ResNet分类器进行对抗攻击 CIFAR数据集PI-FGSM介绍背景和动机算法原理算法流程 PI-FGSM代码实现PI-FGSM算法实现攻击效果 代码汇总pifgsm.pytrain.pyadvtest.py 之前已经针对CIFAR10训练了多种分类器&#xff1a; Pytorch | 从零构建AlexN…...

Casino Royale靶场wp

0x00 下载安装 https://download.vulnhub.com/casinoroyale/CasinoRoyale.ova 导入vmware启动 0x01 主机信息收集 0x02目录扫描 index.php 获取到一个域名 修改本地hosts 添加一行 路径&#xff1a;C:\Windows\System32\drivers\etc 192.168.2.20 casino-royale.local 点击…...

c/c++ 无法跳转定义

背景 对于嵌入式开发离不开交叉编译工作&#xff0c;采用vccode远程到虚拟机开发来说&#xff0c;总会遇到一个函数跳转问题。下面针对运用开发如何设置vscode保证函数能正确跳转大函数定义。 一、安装c/c插件 安装C/C Extension Pack插件&#xff0c;这插件包含有几个插件。…...

4.5 数据表的外连接

本次课程我们将继续的学习数据表的连接。因为数据表的连接是分为内连接和外连接的。内连接的语法&#xff0c;还有一些练习&#xff0c;我们都是学习到了。那么本次课程咱们就开始学习数据表的外连接语法。首先我来解释一下为什么要使用外连接这种语法。咱们首先看一条记录&…...

请购单一直提示需求部门不能为空无法提交

终于发现了它的逻辑。用户很多次反馈&#xff0c;提交请购单时&#xff0c;提示需求部门不能为空&#xff0c;既使选择了需求部门&#xff0c;保存时&#xff0c;神奇的是会清空掉部门的信息&#xff0c;提交时就会有错误提示出来。 原因&#xff1a;光选择单头上的需求部门是…...

Jenkins基础教程

Jenkins介绍 Jenkins 是一款开源的持续集成&#xff08;CI&#xff09;和持续交付&#xff08;CD&#xff09;工具&#xff0c;在软件开发和自动化部署流程中发挥着关键作用。 1.背景和起源 它最初是由 Sun Microsystems 公司的一名工程师开发的 Hudson 项目&#xff0c;后来…...

如何配置 Java 环境变量:设置 JAVA_HOME 和 PATH

目录 一、什么是 Java 环境变量&#xff1f; 二、配置 Java 环境变量 1. 下载并安装 JDK 2. 配置 JAVA_HOME Windows 系统 Linux / macOS 系统 3. 配置 PATH Windows 系统 Linux / macOS 系统 4. 验证配置 三、常见问题与解决方案 1. 无法识别 java 或 javac 命令 …...

深入解析 Pytest 钩子函数及二次开发过程

关注开源优测不迷路 大数据测试过程、策略及挑战 测试框架原理&#xff0c;构建成功的基石 在自动化测试工作之前&#xff0c;你应该知道的10条建议 在自动化测试中&#xff0c;重要的不是工具 在 Pytest 测试框架中&#xff0c;钩子函数&#xff08;Hooks&#xff09;是一种强…...

http 请求总结get

关于get请求传递body的问题 错误代码 有400 , 415 等情况 <!doctype html><html lang"zh"><head><title>HTTP Status 400 – 错误的请求</title><style type"text/css">body {font-family:Tahoma,Arial,sans-seri…...

漏洞扫描:网络安全的 “体检” 与 “防护指南”

在当今数字化时代&#xff0c;网络安全如同守护城堡的坚固城墙&#xff0c;而漏洞扫描则是检查城墙是否存在缝隙与薄弱环节的重要手段。那么&#xff0c;究竟什么是漏洞扫描&#xff1f;又该如何进行呢&#xff1f; 什么是漏洞扫描&#xff1f; 漏洞扫描是一种安全检测过程&a…...

《Vue进阶教程》第二十七课:实现侦听对象

往期内容&#xff1a; 《Vue进阶教程》第十六课&#xff1a;深入完善响应式系统之单例模式 《Vue进阶教程》第十七课&#xff1a;支持分支切换 《Vue进阶教程》第十八课&#xff1a;避免死循环 《Vue进阶教程》第十九课&#xff1a;computed初步实现 《Vue进阶教程》第二十…...

【Linux 网络 (五)】Tcp/Udp协议

Linux 网络 一前言二、Udp协议1&#xff09;、Udp协议特点2&#xff09;、Udp协议格式3&#xff09;、Udp报文封装和解包过程4&#xff09;、UDP的缓冲区 三、TCP协议1&#xff09;、TCP协议特点2&#xff09;、TCP协议格式1、4位首部长度、源端口、目的端口2、16位窗口大小3、…...

算法工程化工程师

算法工程化工程师是一种结合算法研究与工程开发能力的技术职位&#xff0c;主要职责是将算法从理论研究到实际落地&#xff0c;应用到各种工业或商业场景中。以下是关于这个职位的一些核心内容&#xff1a; 核心职责&#xff1a; 算法实现与优化&#xff1a; 将数学模型或算法…...

信息系统管理师试题-转型升级

1.3.转型升级 战略转型升级是对组织的长期发展方向、运行模式、组织战略、组织方式、资源配置方式、祖师文化等进行全方位升级变革。下列对战略转型升级的描述错误的是&#xff08;&#xff09; A大多数组织的转型主要是战略转型 B组织转型升级首先要解决的是战略选择问题 C组织…...

mysql三种读取模式(普通、流式、游标)

在与MySQL数据库交互时&#xff0c;数据的读取方式有多种选择&#xff0c;包括流式读取、游标读取和普通读取。每种方式都有其独特的原理、优势和劣势。本文将对这三种读取方式进行详细介绍&#xff0c; 1. 普通读取 介绍 普通读取是指通过JDBC的Statement或PreparedStateme…...

月子会所ERP管理云平台 GetData.ashx SQL注入致RCE漏洞复现

0x01 产品简介 月子会所ERP管理云平台是武汉金同方科技有限公司专为为母婴服务行业提供信息化解决方案,是结合行业顶级月子中心相关企业需求开发的一套综合性管理软件。该系统全面管控月子中心经营过程中的各个环节,提高总店及分店月子中心管理水平,规范月子中心从业人员操作…...

Ubuntu22.10/22.04 autoinstall--OK

第一步:建立ubuntu22.04 jammy apt本地源(见本博主对应栏) --------------------------------------------------------------------------------------- ubuntu22.04 grub配置: menuentry Ubuntu22.04-autoinstall(UEFI) --id UBUNTU22.04-autoinstall { echo "…...

操作系统之同步与互斥的基本概念

1. 同步的基本概念 定义&#xff1a;同步是指在多个并发执行的进程或线程之间协调其行为&#xff0c;以使它们能够正确地相互合作。在计算机科学中&#xff0c;同步通常指对共享资源进行访问控制&#xff0c;以避免竞争条件和死锁等问题。 实现方式&#xff1a;为了实现同步&a…...

【ANGULAR网站开发】初始环境搭建

1. 初始化angular项目 1.1 创建angular项目 需要安装npm和nodejs&#xff0c;这边不在重新安装 直接安装最新版本的angular npm install -g angular/cli安装指定大版本的angular npm install -g angular/cli181.2 启动angular 使用idea启动 控制台启动 ng serve启动成功…...

[青少年CTF练习平台]Lihua‘s for

下载附件之后直接IDA启动 查看dword_403040指向的内容&#xff0c;全是数据&#xff0c;提取出来 分析完成写脚本 flag "" temdata [0x00000066, 0x0000006D, 0x00000063, 0x00000064, 0x0000007F, 0x00000064, 0x00000032, 0x00000036, 0x0000006A, 0x000000…...

WebRTC服务质量(12)- Pacer机制(04) 向Pacer中插入数据

WebRTC服务质量&#xff08;01&#xff09;- Qos概述 WebRTC服务质量&#xff08;02&#xff09;- RTP协议 WebRTC服务质量&#xff08;03&#xff09;- RTCP协议 WebRTC服务质量&#xff08;04&#xff09;- 重传机制&#xff08;01) RTX NACK概述 WebRTC服务质量&#xff08;…...

css文字折行以及双端对齐实现方式

使用flex布局后&#xff0c;文字超出容器部分不会自动折行了。实现代码如下&#xff1a; <el-row><el-col :span"24"><span class"label">姓名</span><span class"content">{{name}}</span></el-col>…...

AI智能养站神器-SEO助理原创文章批量生成发布工具

很多站长最头疼的就是网站每天的内容更新&#xff0c;因为不知道写什么&#xff0c;采集被人的文章又会被定义为抄袭&#xff0c;而且现在伪原创已经没有多大的效果了&#xff0c;所以今天给大家分享的就是一款AI智能养战神器-SEO助理原创文章批量生成发布工具。 这款工具支持…...

python数据分析之爬虫基础:selenium详细讲解

目录 1、selenium介绍 2、selenium的作用&#xff1a; 3、配置浏览器驱动环境及selenium安装 4、selenium基本语法 4.1、selenium元素的定位 4.2、selenium元素的信息 4.3、selenium元素的交互 5、Phantomjs介绍 6、chrome handless模式 1、selenium介绍 &#xff08;1…...

使用 ECharts 与 Vue 构建数据可视化组件

在前端开发中&#xff0c;数据可视化是非常重要的一部分。ECharts 作为一个功能强大且易于使用的开源数据可视化库&#xff0c;被广泛应用于各种图表展示需求中。而 Vue.js 是当下流行的前端框架之一&#xff0c;它的数据驱动和组件化开发模式让我们能轻松地将 ECharts 集成到 …...

KAFKA 权威指南笔记(一)究竟应该配置多少个BROKER?

一个KAFKA集群需要多少个BROKER&#xff1f; 一个单独的Kafka服务器被叫做BROKER&#xff0c;BROKER可以处理数千个分区以及每秒百万级别的消息量。由BROKER组成了“集群”&#xff08;其中由集群控制器角色的BROKER是从成员中选举出来的&#xff0c;负责控制管理工作&#xf…...

练习题:20

目录 Python题目 题目 题目分析 1. 类与变量、属性设计分析 2. Value 属性的实现分析 3. 转换函数分析 4. 整体代码结构与编程规范考虑 代码实现 代码解释 1. 类定义部分 2. 对象创建与功能测试部分 运行思路 1. 类定义阶段 2. 对象创建阶段 3. 获取 Value 属性…...

【时时三省】(C语言基础)动态内存函数malloc

山不在高&#xff0c;有仙则名。水不在深&#xff0c;有龙则灵。 ----CSDN 时时三省 malloc 开辟内存块 使用格式 void *malloc&#xff08;size_t sie&#xff09;&#xff1b; 示例 10*sizeof(int&#xff09;就是开辟空间的大小 如果p是void指针的话 p不能解引用 m…...

大数据学习之Redis 缓存数据库二,Scala分布式语言一

一.Redis 缓存数据库二 26.Redis数据安全_AOF持久化机制 27.Redis数据安全_企业中该如何选择持久化机制 28.Redis集群_主从复制概念 29.Redis集群_主从复制搭建 30.Redis集群_主从复制原理剖析 31.Redis集群_哨兵监控概述 32.Redis集群_配置哨兵监控 33.Redis集群_哨兵监控原理…...

第23天:信息收集-APP应用产权渠道服务资产通讯抓包静态提取动态调试测试范围

#知识点 1、信息收集-APP应用-公开信息-知识产权&开发者定位 2、信息收集-APP应用-资产信息-抓包&静态提取&动态调试 一、APP渗透测试的范围->应涵盖APP所有功能和组件&#xff0c;包括但不限于以下几个方面&#xff1a; 1、前端安全&#xff1a;包括界面交互、…...

每日一练 | DHCP 客户端续约过程

01 真题题目 在 DHCP 运行过程中&#xff0c;如果客户端 IP 地址在租约过去 87.5%还没有完成续约的话&#xff0c;客户端将发送什么报文进行再次续约&#xff1f; A. DHCPdiscover 广播报文 B. DHCP release 单播报文 C. DHCPrequest 广播报文 D. DHCPrequest 单播报文 02 真题…...

存储块的获取与释放

目录 获取存储块 释放存储块 设计实现 获取存储块 有空闲存储块&#xff0c;直接取出空闲块&#xff1b; 无空闲存储块&#xff0c;任务进入等待队列。 释放存储块 无任务等待&#xff0c;插入到空闲链表&#xff1b; 有任务等待&#xff0c;释放等待队列头部的任务。 设计实现…...

定位方式:css

使用相对路径 div ul #div下的所有ul&#xff0c;空格表示相对路径&#xff08;这个实际中用的多一些&#xff09; 绝对路径-一般不用绝对路径 html>head>div&#xff0c;“>”表示根路径 使用class名称定位 使用.表示 使用id定位 使用#表示 使用属性定位 [属性名…...

主从复制架构介绍和主从复制配置案例

每一个数据库的业务都对应着一个前端的业务&#xff0c; 主从复制架构的必要性? 第一点是两个服务器如果有一台服务器出现故障&#xff0c;那么另一台服务器可以正常工作&#xff0c;以保障前端业务可以被正常访问&#xff0c;第二点是两个服务器可以共同去处理数据&#xff…...

零跑汽车一路狂飙

新能源汽车市场潮起潮落&#xff0c;只有潮水退去&#xff0c;才能看清谁在裸泳。十年前&#xff0c;一批新能源汽车新势力带着创新的理念和先进的技术&#xff0c;如雨后春笋般涌入中国汽车市场&#xff0c;掀起一场新旧势力的角逐。 经历市场的激烈洗礼与投资泡沫的挤压&…...

固态硬盘SSD

目录 1、固态硬盘结构 2、访问和读取策略 3、固态硬盘 VS SSD固态硬盘 &#xff08;1&#xff09;速度 &#xff08;2&#xff09;性能 &#xff08;3&#xff09;使用寿命 4、磨损均衡技术 &#xff08;1&#xff09;动态磨损均衡 &#xff08;2&#xff09;静态磨损…...