网关登录校验

网关登录校验

单体架构时我们只需要完成一次用户登录、身份校验,就可以在所有业务中获取到用户信息。而微服务拆分后,每个微服务都独立部署,不再共享数据。也就意味着每个微服务都需要做登录校验,这显然不可取。

鉴权思路分析

登录授权的功能写在了user-service里,由于采用的是jwt登录方式,不管在哪里登录,只要给用户颁发了jwt的token,那么他就可以带着token访问访问,这时就可以从token里解析出用户的信息,所以登录授权这块代码不需要改变,只需要放在user-service里即可。但是对于jwt的校验就不一样了,很多的微服务都需要知道登录用户的信息。比如说购物车服务,查询和修改购物车都需要登录用户是谁。这样就需要在所有的微服务里面做jwt的校验,代码的重复量就增加了,而且还需要将jwt的密钥发送给他们,密钥泄露的风险也提高了。因此这个校验的操作就需要在网关里做了,因为网关是整个微服务的入口。

在这里插入图片描述

既然网关是所有微服务的入口,一切请求都需要先经过网关。我们完全可以把登录校验的工作放到网关去做,这样之前说的问题就解决了。

  • 只需要在网关和用户服务保存秘钥
  • 只需要在网关开发登录校验功能

此时,登录校验的流程如图:

在这里插入图片描述

不过,这里存在几个问题:

  • 网关路由是配置的,请求转发是Gateway内部代码,我们如何在转发之前做登录校验?
  • 网关校验JWT之后,如何将用户信息传递给微服务?
  • 微服务之间也会相互调用,这种调用不经过网关,又该如何传递用户信息?

这些问题将在接下来几节一一解决。

网关过滤器

登录校验必须在请求转发到微服务之前做,否则就失去了意义。而网关的请求转发是Gateway内部代码实现的,要想在请求转发之前做登录校验,就必须了解Gateway内部工作的基本原理。

在这里插入图片描述

如图所示:

  1. 客户端请求进入网关后由HandlerMapping对请求做判断,它是基于路由断言RouterPredicateHandlerMapping去做路由规则的匹配的,之前对每个路由都配置了路由断言,所有他就可以基于这个断言和前端请求去匹配,找到与当前请求匹配的路由规则(Route)并存入上下文,然后将请求交给WebHandler去处理。
  2. WebHandler默认实现是FilteringWebHandler,它是与过滤器有关的。它会加载网关中配置的多个过滤器。放入集合中排序,形成过滤器链(Filter chain)。然后依次执行这些过滤器。
  3. 图中Filter被虚线分为左右两部分,是因为Filter内部的逻辑分为prepost两部分,分别会在请求路由到微服务之前之后被执行。
  4. 只有所有Filterpre逻辑都依次顺序执行通过后,就会进入Netty过滤器,它的作用是将请求转发到微服务。
  5. 微服务会将结果返回到Netty过滤器Netty过滤器会将结果封装,然后存到上下文里面,接下来再倒序执行Filterpost逻辑依次返回给其他过滤器,最终返回给用户。
  6. 最终把响应结果返回。

如图中所示,最终请求转发是有一个名为NettyRoutingFilter的过滤器来执行的,而且这个过滤器是整个过滤器链中顺序最靠后的一个。如果我们能够定义一个过滤器,在其中实现登录校验逻辑,并且将过滤器执行顺序定义到NettyRoutingFilter之前,这就符合我们的需求了!

网关如何将用户信息传递给微服务?

网关没有业务的,但是微服务要得到用户信息。网关到微服务是一次新的http请求,通过一次http请求传递信息,最佳的传递方案是请求头,因为放到请求头中是不会对业务产生影响的。

如何在为u服务之间传递用户信息?

一些复杂的业务会出现微服务之间的调用,比如下单完成之后需要清理用户的购物车,所以交易服务将来还可能调用购物车服务,完成购物车的清理。在网关到交易服务的时候,会将用户信息通过请求头传递过来。但是在交易服务到购物车服务之间又是一次新的http请求,如果不对接收的请求做处理,那么肯定不会将用户信息向下传递,那么购物城服务就拿不到服务。微服务之间的请求是基于OpenFign发起的,网关的微服务发送请求则是网关内置的一次请求方式,所以在微服务之间发送用户信息,不能通过请求头的方式。

自定义过滤器

网关过滤器链中的过滤器有两种:

  • GatewayFilter:路由过滤器,就是之前33种过滤器,作用范围比较灵活,可以是任意指定的路由Route.
  • GlobalFilter:全局过滤器,作用范围是所有路由,不可配置。

其实GatewayFilterGlobalFilter这两种过滤器的方法签名完全一致:

/*** 处理请求并将其传递给下一个过滤器* @param exchange 当前请求的上下文,其中包含request、response等各种共享数据* @param chain 过滤器链,当前过滤器执行完后,要调用过滤器链种的下一个过滤器。* @return 根据返回值标记当前请求是否被完成或拦截,chain.filter(exchange)就放行了。*/
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);

网关的过滤器内部分为pre和post,在实现filter方法以后,内部实现的所有逻辑都属于pre部分,当pre执行完,可以调用过滤器chain,利用chain调用下一个过滤器。当所有的过滤器执行完之后,会将请求转发给微服务,然后才能执行post逻辑。这样post就需要等待很长时间,如果所有的过滤器都这样等待i,那么耗时就会很久。所以网关采用的是一种非阻塞式的编程,需要利用Mono定义回调函数,这样就不需要等待了,返回结果有了以后再调用它的回调函数Mono,回调函数里面的逻辑就是post部分的逻辑。在大多数的业务种都不需要关系post部分。

无论是GatewayFilter还是GlobalFilter都支持自定义,只不过编码方式、使用方式略有差别。

自定义GlobalFilter

自定义GlobalFilter则简单很多,直接实现GlobalFilter即可,而且也无法设置动态参数:

// 加上Component注解,将其注册为Spring的一个Bean
@Component
// 实现GlobalFilter,并实现其Filter方法。
// Ordered是做排序的,它是Spring核心包下的,要求实现一个getOrder方法,这个值越小代表优先级越高,要保证自定义的过滤器在Netty过滤器之前,Netty过滤器默认的优先级为最低优先级。
public class PrintAnyGlobalFilter implements GlobalFilter, Ordered {@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {// 编写过滤器pre逻辑,完成登录校验。// TODO 模拟登录校验逻辑// 获取请求头,拿到登录凭证。ServerHttpRequest request = exchange.getRequest();HttpHeaders headers = request.getHeaders();System.out.println("headers = " + headers);// 放行return chain.filter(exchange);}@Overridepublic int getOrder() {// 过滤器执行顺序,值越小,优先级越高return 0;}
}

测试:先登录用户,然后打断点,重启网关服务。

在这里插入图片描述

自定义GatewayFilter

自定义GatewayFilter不是直接实现GatewayFilter,而是实现AbstractGatewayFilterFactory。最简单的方式是这样的:

@Component
public class PrintAnyGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {@Overridepublic GatewayFilter apply(Object config) {return new GatewayFilter() {@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {// 获取请求ServerHttpRequest request = exchange.getRequest();// 编写过滤器逻辑System.out.println("过滤器执行了");// 放行return chain.filter(exchange);}};}
}

注意:该类的名称一定要以GatewayFilterFactory为后缀!

然后在yaml配置中这样使用:

spring:cloud:gateway:default-filters:- PrintAny # 此处直接以自定义的GatewayFilterFactory类名称前缀类声明过滤器

另外,这种过滤器还可以支持动态配置参数,不过实现起来比较复杂,示例:

@Component
public class PrintAnyGatewayFilterFactory // 父类泛型是内部类的Config类型extends AbstractGatewayFilterFactory<PrintAnyGatewayFilterFactory.Config> {@Overridepublic GatewayFilter apply(Config config) {// OrderedGatewayFilter是GatewayFilter的子类,包含两个参数:// - GatewayFilter:过滤器// - int order值:值越小,过滤器执行优先级越高return new OrderedGatewayFilter(new GatewayFilter() {@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {// 获取config值String a = config.getA();String b = config.getB();String c = config.getC();// 编写过滤器逻辑System.out.println("a = " + a);System.out.println("b = " + b);System.out.println("c = " + c);// 放行return chain.filter(exchange);}}, 100);}// 自定义配置属性,成员变量名称很重要,下面会用到@Datastatic class Config{private String a;private String b;private String c;}// 将变量名称依次返回,顺序很重要,将来读取参数时需要按顺序获取@Overridepublic List<String> shortcutFieldOrder() {return List.of("a", "b", "c");}// 返回当前配置类的类型,也就是内部的Config@Overridepublic Class<Config> getConfigClass() {return Config.class;}}

然后在yaml文件中使用:

spring:cloud:gateway:default-filters:- PrintAny=1,2,3 # 注意,这里多个参数以","隔开,将来会按照shortcutFieldOrder()方法返回的参数顺序依次复制

上面这种配置方式参数必须严格按照shortcutFieldOrder()方法的返回参数名顺序来赋值。

还有一种用法,无需按照这个顺序,就是手动指定参数名:

spring:cloud:gateway:default-filters:- name: PrintAnyargs: # 手动指定参数名,无需按照参数顺序a: 1b: 2c: 3

登录校验

接下来,我们就利用自定义GlobalFilter来完成登录校验。

JWT工具

登录校验需要用到JWT,而且JWT的加密需要秘钥和加密工具。这些在hm-service中已经有了,我们直接拷贝过来:

在这里插入图片描述

具体作用如下:

  • AuthProperties:配置登录校验需要拦截的路径,因为不是所有的路径都需要登录才能访问
  • JwtProperties:定义与JWT工具有关的属性,比如秘钥文件位置
  • SecurityConfig:读取文件,生成密钥。
  • JwtTool:JWT工具,其中包含了校验和解析token的功能
  • hmall.jks:秘钥文件

其中AuthPropertiesJwtProperties所需的属性要在application.yaml中配置:

hm:jwt:location: classpath:hmall.jks # 秘钥地址alias: hmall # 秘钥别名password: hmall123 # 秘钥文件密码tokenTTL: 30m # 登录有效期auth:excludePaths: # 无需登录校验的路径,可以直接访问。- /search/**- /users/login- /items/**
登录校验过滤器

接下来,我们定义一个登录校验的过滤器:

在这里插入图片描述

代码如下:

package com.hmall.gateway.filter;import com.hmall.common.exception.UnauthorizedException;
import com.hmall.common.utils.CollUtils;
import com.hmall.gateway.config.AuthProperties;
import com.hmall.gateway.util.JwtTool;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;import java.util.List;@Component
@RequiredArgsConstructor
@EnableConfigurationProperties(AuthProperties.class)
public class AuthGlobalFilter implements GlobalFilter, Ordered {private final JwtTool jwtTool;private final AuthProperties authProperties;private final AntPathMatcher antPathMatcher = new AntPathMatcher();@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {// 1.获取RequestServerHttpRequest request = exchange.getRequest();// 2.判断是否不需要拦截if(isExclude(request.getPath().toString())){// 无需拦截,直接放行return chain.filter(exchange);}// 3.获取请求头中的tokenString token = null;List<String> headers = request.getHeaders().get("authorization");if (!CollUtils.isEmpty(headers)) {token = headers.get(0);}// 4.校验并解析tokenLong userId = null;try {userId = jwtTool.parseToken(token);} catch (UnauthorizedException e) {// 如果无效,拦截ServerHttpResponse response = exchange.getResponse();// 401代表未登录或者未授权。response.setRawStatusCode(401);// 终止,后续所有的拦截器都不会执行了。return response.setComplete();}// TODO 5.如果有效,传递用户信息System.out.println("userId = " + userId);// 6.放行return chain.filter(exchange);}private boolean isExclude(String antPath) {// 对于一些特殊的路径带/**或通配符的路径,通过Spring提供的AntPathMatcher匹配器来匹配。for (String pathPattern : authProperties.getExcludePaths()) {if(antPathMatcher.match(pathPattern, antPath)){return true;}}return false;}@Overridepublic int getOrder() {return 0;}
}

重启测试,会发现访问/items开头的路径,未登录状态下不会被拦截:

在这里插入图片描述

访问其他路径则,未登录状态下请求会被拦截,并且返回401状态码:

在这里插入图片描述

登录成功后,查询购物车是成功的。

在这里插入图片描述

在这里插入图片描述

微服务获取用户

现在,网关已经可以完成登录校验并获取登录用户身份信息。但是当网关将请求转发到微服务时,微服务又该如何获取用户身份呢?最佳的方案是将用户信息保存在请求头中,这样微服务就可从请求头中取出用户信息,接下来就能实现自己的业务了。将来微服务的业务很多,可能每个用户都需要得到登录用户的信息。如果将获取请求头中用户信息的业务逻辑在每个微服务中都写一遍,就会很麻烦。

在这里插入图片描述

微服务的接口都是基于SpringMVC实现的,现在不想在每一个业务接口里都去获取登录用户,而是想直接用。所以要在所有业务执行之前获取用户信息,只要SpringMVC的拦截器才会在Controller之前执行。可以在微服务里定义一个SpringMVC的拦截器,这个拦截器里获取请求头中的用户信息,将其保存在ThreadLocal里,这样在后续的业务执行过程中可以随时从ThreadLocal里取出登录信息。

在这里插入图片描述

因此,接下来我们要做的事情有:

  • 改造网关过滤器,在获取用户信息后保存到请求头,转发到下游微服务
  • 编写微服务拦截器,拦截请求获取用户信息,保存到ThreadLocal后放行。
保存用户到请求头

在网关登录校验过滤器中,把获取到的用户写入请求头。修改gateway模块中的登录校验拦截器,在校验成功后保存用户到下游请求的请求头中。要修改转发到微服务的请求,需要用到ServerWebExchange类提供的API。

exchange.mutate() // mutate就是对下游请求做更改.request(builder -> builder.header("user-info",userInfo)).build();

首先,我们修改登录校验拦截器的处理逻辑,保存用户信息到请求头中:

在这里插入图片描述

测试:

查询购物车信息的时候,打印请求头中的用户信息。

在查询购物车的Controller中添加打印用户信息的代码:

在这里插入图片描述

重启服务,查看是否传递用户信息。

在这里插入图片描述

说明已经在token里解析出用户信息,并将用户信息传递给下一个微服务里。

拦截器获取用户

ThreadLocal对象不需要自己创建,在hm-common中已经有一个用于保存登录用户的ThreadLocal工具:

在这里插入图片描述

其中已经提供了保存和获取用户的方法:

在这里插入图片描述

接下来,我们只需要编写拦截器,获取用户信息并保存到UserContext,然后放行即可。这个拦截器是不需要登录拦截的,真正的登录拦截在网关中已经做过了,只要请求到这里就代表登录成功了或者不登录也能访问。所以这个拦截器只需要做用户信息获取即可。

在这里只需要实现handlerInterceptor中的两个方法即可,preHandleafterCompletion。preHeadle在Controller之前执行,需要在里面获取登录用户的信息,最终要将其保存在ThreadLocal里。Controller执行完成之后还需要将ThreadLocal里的信息清理掉,所以使用afterCompletion清理。

由于每个微服务都有获取登录用户的需求,因此拦截器我们直接写在hm-common中,并写好自动装配。这样微服务只需要引入hm-common就可以直接具备拦截器功能,无需重复编写。

我们在hm-common模块下定义一个拦截器:

在这里插入图片描述

具体代码如下:

package com.hmall.common.interceptor;import cn.hutool.core.util.StrUtil;
import com.hmall.common.utils.UserContext;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;public class UserInfoInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1.获取请求头中的用户信息String userInfo = request.getHeader("user-info");// 2.判断是否为空if (StrUtil.isNotBlank(userInfo)) {// 不为空,保存到ThreadLocalUserContext.setUser(Long.valueOf(userInfo));}// 3.放行return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {// 移除用户UserContext.removeUser();}
}

接着在hm-common模块下编写SpringMVC的配置类,配置登录拦截器:

在这里插入图片描述

具体代码如下:

package com.hmall.common.config;import com.hmall.common.interceptors.UserInfoInterceptor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration
// h-common不仅仅被微服务引用了,而且也被网关引用了。但是我们希望MvcCongif在网关里不生效,在SpringMVC里生效。这就需要用到SpringBoot自动装配的原理,给这个配置类加一个条件,让其在网关里面不生效,在微服务里面生效。这就需要对比网关与其他微服务的区别,网关里面没有SpringMVC,而微服务里面有。可以利用这个作为条件,有SpirngMvc就会有相关的api,有SpringMvc一定有其核心api,也就是DispatcherServlet。如果将其作为条件,所有微服务都能生效,网关由于没有SpringMvc不会生效。
@ConditionalOnClass(DispatcherServlet.class)
public class MvcConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new UserInfoInterceptor());}
}

不过,需要注意的是,这个配置类默认是不会生效的,因为它所在的包是com.hmall.common.config,与其它微服务的扫描包不一致,无法被扫描到,因此无法生效。

基于SpringBoot的自动装配原理,我们要将其添加到resources目录下的META-INF/spring.factories文件中:

在这里插入图片描述

内容如下:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\com.hmall.common.config.MyBatisConfig,\com.hmall.common.config.MvcConfig
恢复购物车代码

之前我们无法获取登录用户,所以把购物车服务的登录用户写死了,现在需要恢复到原来的样子。

找到cart-service模块的com.hmall.cart.service.impl.CartServiceImpl

在这里插入图片描述

修改其中的queryMyCarts方法:

在这里插入图片描述

测试:

启动CartService,在根据用户id获取购物车信息中,可以看到用户id,说明用户id成功调用了。

在这里插入图片描述

切换用户rose:

在这里插入图片描述

总结:

首先,需要在网关获取用户信息并且向下传递。网关获取用户信息,通过过滤器拦截以及jwt校验。传递需要用到exchange的api修改请求,将用户信息添加到请求头,这样网关在转发请求到微服务的时候自然就携带微服务的信息。

请求到达微服务的时候需要微服务去解析,如果在每个微服务的controller中手动解析将会很麻烦。所以可以基于SpringMvc拦截器,拦截器可以在Controller之前执行,这个SpringMvc拦截器如果每个微服务都写也会很麻烦。最终可以将拦截器写在common模块里,在这个模块里获取请求头中的用户信息,然后保存在ThreadLocal中,后续所有的业务都可以从ThreadLocal中取用户信息,执行业务。

在定义拦截器的时候,也会碰到一些问题,将拦截器的配置放到common模块下,会导致其他的微服务扫描不到。需要使用SpingBoot的自动装配原理,将定义的配置类放在META-INFspring.factories文件下,这样就能实现自动装配。这样带来的另一个问题是,这个配置类只希望在微服务里面生效,不希望在网关中生效。因此,可以使用条件注解判断当前项目下有没有SpringMvcDispatcherServlet,网关里面没有就不会生效。

OpenFeign传递用户

前端发起的请求都会经过网关再到微服务,由于我们之前编写的过滤器和拦截器功能,微服务可以轻松获取登录用户信息。

但有些业务是比较复杂的,请求到达微服务后还需要调用其它多个微服务。比如下单业务,流程如下:

在这里插入图片描述

下单的过程中,需要调用商品服务扣减库存,调用购物车服务清理用户购物车。而清理购物车时必须知道当前登录的用户身份。但是,订单服务调用购物车时并没有传递用户信息,购物车服务无法知道当前用户是谁!由于微服务获取用户信息是通过拦截器在请求头中读取,因此要想实现微服务之间的用户信息传递,就必须在微服务发起调用时把用户信息存入请求头

微服务之间调用是基于OpenFeign来实现的,并不是我们自己发送的请求。我们如何才能让每一个由OpenFeign发起的请求自动携带登录用户信息呢?这里要借助Feign中提供的一个拦截器接口:feign.RequestInterceptor,所有由OpenFeign发起的请求都会先调用拦截器处理请求。

public interface RequestInterceptor {/*** Called for every request. * Add data using methods on the supplied {@link RequestTemplate}.*/void apply(RequestTemplate template);
}

我们只需要实现这个接口,然后实现apply方法,利用RequestTemplate类来添加请求头,将用户信息保存到请求头中。这样以来,每次OpenFeign发起请求的时候都会调用该方法,传递用户信息。将来所有的微服务都可能调用其他的服务,在调用其他服务的时候都需要传递用户信息,所以在做服务调用的时候都需要去传递,所以需要将拦截器放到一个公共的地方。

由于FeignClient全部都是在hm-api模块,因此我们在hm-api模块的com.hmall.api.config.DefaultFeignConfig中编写这个拦截器:

在这里插入图片描述

com.hmall.api.config.DefaultFeignConfig中添加一个Bean:

@Bean
public RequestInterceptor userInfoRequestInterceptor(){return new RequestInterceptor() {@Overridepublic void apply(RequestTemplate template) {// 获取登录用户Long userId = UserContext.getUser();if(userId == null) {// 如果为空则直接跳过return;}// 如果不为空则放入请求头中,传递给下游微服务template.header("user-info", userId.toString());}};
}

由于要用到UserContext,所以需要在依赖中引入common模块:

        <!--common--><dependency><groupId>com.heima</groupId><artifactId>hm-common</artifactId><version>1.0.0</version></dependency>

DefaultFeignConfig配置类要想生效,需要加在Feign启动类上,交易服务的启动类上应该加上这个配置类。

在这里插入图片描述

测试:

下单成功后

在这里插入图片描述

查看购物车服务的日志:

在这里插入图片描述

好了,现在微服务之间通过OpenFeign调用时也会传递登录用户信息了。

总结:

微服务下实现登录功能的流程:

在这里插入图片描述

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

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

相关文章

wxwidgets直接获取系统图标,效果类似QFileIconProvider

目前只做了windows版本&#xff0c;用法类似QFileIconProvider // 头文件 #ifndef WXFILEICONPROVIDER_H #define WXFILEICONPROVIDER_H#include <wx/wx.h> #include <wx/icon.h> #include <wx/image.h> #include <wx/bmpcbox.h> // Include for wxB…

【玩转全栈】--创建一个自己的vue项目

目录 vue介绍 创建vue项目 vue页面介绍 element-plus组件库 启动项目 vue介绍 Vue.js 是一款轻量级、易于上手的前端 JavaScript 框架&#xff0c;旨在简化用户界面的开发。它采用了响应式数据绑定和组件化的设计理念&#xff0c;使得开发者可以通过声明式的方式轻松管理数据和…

DS并查集(17)

文章目录 前言一、何为并查集&#xff1f;二、并查集的实现&#xff1f;并查集的初始化查找元素所在的集合判断两个元素是否在同一个集合合并两个元素所在的集合获取并查集中集合的个数并查集的路径压缩 三、来两道题练练手&#xff1f;省份的数量等式方程的可满足性 总结 前言…

minimind - 从零开始训练小型语言模型

大语言模型&#xff08;LLM&#xff09;领域&#xff0c;如 GPT、LLaMA、GLM 等&#xff0c;虽然它们效果惊艳&#xff0c; 但动辄10 Bilion庞大的模型参数个人设备显存远不够训练&#xff0c;甚至推理困难。 几乎所有人都不会只满足于用Lora等方案fine-tuing大模型学会一些新的…

【C++动态规划 离散化】1626. 无矛盾的最佳球队|2027

本文涉及知识点 C动态规划 离散化 LeetCode1626. 无矛盾的最佳球队 假设你是球队的经理。对于即将到来的锦标赛&#xff0c;你想组合一支总体得分最高的球队。球队的得分是球队中所有球员的分数 总和 。 然而&#xff0c;球队中的矛盾会限制球员的发挥&#xff0c;所以必须选…

Privacy Eraser,电脑隐私的终极清除者

Privacy Eraser 是一款专为保护用户隐私而设计的全能型软件&#xff0c;它不仅能够深度清理计算机中的各类隐私数据&#xff0c;还提供了多种系统优化工具&#xff0c;帮助用户提升设备的整体性能。通过这款软件&#xff0c;用户可以轻松清除浏览器历史记录、缓存文件、Cookie、…

【数据结构与算法】AVL树的插入与删除实现详解

文章目录 前言Ⅰ. AVL树的定义Ⅱ. AVL树节点的定义Ⅲ. AVL树的插入Insert一、节点的插入二、插入的旋转① 新节点插入较高左子树的左侧&#xff08;左左&#xff09;&#xff1a;右单旋② 新节点插入较高右子树的右侧&#xff08;右右&#xff09;&#xff1a;左单旋③ 新节点插…

SCRM开发为企业提供全面客户管理解决方案与创新实践分享

内容概要 在当今的商业环境中&#xff0c;客户关系管理&#xff08;CRM&#xff09;变得越来越重要。而SCRM&#xff08;社交客户关系管理&#xff09;作为一种新兴的解决方案&#xff0c;正在帮助企业彻底改变与客户的互动方式。快鲸SCRM是一个引人注目的工具&#xff0c;它通…

【C/C++】区分0、NULL和nullptr

&#x1f984;个人主页:小米里的大麦-CSDN博客 &#x1f38f;所属专栏:C_小米里的大麦的博客-CSDN博客 &#x1f381;代码托管:C: 探索C编程精髓&#xff0c;打造高效代码仓库 (gitee.com) ⚙️操作环境:Visual Studio 2022 目录 1. 0 和空指针 2. NULL 3. nullptr 总结 …

【Numpy核心编程攻略:Python数据处理、分析详解与科学计算】2.1 NumPy高级索引:布尔型与花式索引的底层原理

2.1 NumPy高级索引&#xff1a;布尔型与花式索引的底层原理 目录 #mermaid-svg-NpcC75NxxU2mkB3V {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-NpcC75NxxU2mkB3V .error-icon{fill:#552222;}#mermaid-svg-NpcC75…

云原生(五十二) | DataGrip软件使用

文章目录 DataGrip软件使用 一、DataGrip基本使用 二、软件界面介绍 三、附件文件夹到项目中 四、DataGrip设置 五、SQL执行快捷键 DataGrip软件使用 一、DataGrip基本使用 1. 软件界面介绍 2. 附加文件夹到项目中【重要】 3. DataGrip配置 快捷键使用&#xff1a;C…

《TCP 网络编程实战:开发流程、缓冲区原理、三次握手与四次挥手》

一、 TCP 网络应用程序开发流程 学习目标 能够知道TCP客户端程序的开发流程1. TCP 网络应用程序开发流程的介绍 TCP 网络应用程序开发分为: TCP 客户端程序开发TCP 服务端程序开发说明: 客户端程序是指运行在用户设备上的程序 服务端程序是指运行在服务器设备上的程序,专门…

新年新挑战:如何用LabVIEW开发跨平台应用

新的一年往往伴随着各种新的项目需求&#xff0c;而跨平台应用开发无疑是当前备受瞩目的发展趋势。在众多开发工具中&#xff0c;LabVIEW 以其独特的图形化编程方式和强大的功能&#xff0c;为开发跨平台应用提供了有效的途径。本文将深入探讨如何运用 LabVIEW 开发能够在不同操…

UE5.3 C++ CDO的初步理解

一.UObject UObject是所有对象的基类&#xff0c;往上还有UObjectBaseUtility。 注释&#xff1a;所有虚幻引擎对象的基类。对象的类型由基于 UClass 类来定义。 这为创建和使用UObject的对象提供了 函数&#xff0c;并且提供了应在子类中重写的虚函数。 /** * The base cla…

【PyTorch】4.张量拼接操作

个人主页&#xff1a;Icomi 在深度学习蓬勃发展的当下&#xff0c;PyTorch 是不可或缺的工具。它作为强大的深度学习框架&#xff0c;为构建和训练神经网络提供了高效且灵活的平台。神经网络作为人工智能的核心技术&#xff0c;能够处理复杂的数据模式。通过 PyTorch&#xff0…

jstat命令详解

jstat 用于监视虚拟机运行时状态信息的命令&#xff0c;它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT 编译等运行数据。 命令的使用格式如下。 jstat [option] LVMID [interval] [count]各个参数详解&#xff1a; option&#xff1a;操作参数LVMID&#xff1a;本…

(动态规划路径基础 最小路径和)leetcode 64

视频教程 1.初始化dp数组&#xff0c;初始化边界 2、从[1行到n-1行][1列到m-1列]依次赋值 #include<vector> #include<algorithm> #include <iostream>using namespace std; int main() {vector<vector<int>> grid { {1,3,1},{1,5,1},{4,2,1}…

松灵机器人 scout ros2 驱动 安装

必须使用 ubuntu22 必须使用 链接的humble版本 #打开can 口 sudo modprobe gs_usbsudo ip link set can0 up type can bitrate 500000sudo ip link set can0 up type can bitrate 500000sudo apt install can-utilscandump can0mkdir -p ~/ros2_ws/srccd ~/ros2_ws/src git cl…

MATLAB-Simulink并行仿真示例

一、概述 在进行simulink仿真的过程中常常遇到CPU利用率较低&#xff0c;仿真缓慢的情况&#xff0c;可以借助并行仿真改善这些问题&#xff0c;其核心思想是将参数扫描、蒙特卡洛分析或多工况验证等任务拆分成多个子任务&#xff0c;利用多核CPU或计算集群的并行计算能力&…

Workbench 中的热源仿真

探索使用自定义工具对移动热源进行建模及其在不同行业中的应用。 了解热源动力学 对移动热源进行建模为各种工业过程和应用提供了有价值的见解。激光加热和材料加工使用许多激光束来加热、焊接或切割材料。尽管在某些情况下&#xff0c;热源 &#xff08;q&#xff09; 不是通…