网关登录校验

news2025/1/30 19:32:36

网关登录校验

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

鉴权思路分析

登录授权的功能写在了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 {
    @Override
    public 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);
    }

    @Override
    public int getOrder() {
        // 过滤器执行顺序,值越小,优先级越高
        return 0;
    }
}

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

在这里插入图片描述

自定义GatewayFilter

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

@Component
public class PrintAnyGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
    @Override
    public GatewayFilter apply(Object config) {
        return new GatewayFilter() {
            @Override
            public 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> {

    @Override
    public GatewayFilter apply(Config config) {
        // OrderedGatewayFilter是GatewayFilter的子类,包含两个参数:
        // - GatewayFilter:过滤器
        // - int order值:值越小,过滤器执行优先级越高
        return new OrderedGatewayFilter(new GatewayFilter() {
            @Override
            public 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);
    }

    // 自定义配置属性,成员变量名称很重要,下面会用到
    @Data
    static class Config{
        private String a;
        private String b;
        private String c;
    }
    // 将变量名称依次返回,顺序很重要,将来读取参数时需要按顺序获取
    @Override
    public List<String> shortcutFieldOrder() {
        return List.of("a", "b", "c");
    }
        // 返回当前配置类的类型,也就是内部的Config
    @Override
    public Class<Config> getConfigClass() {
        return Config.class;
    }

}

然后在yaml文件中使用:

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

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

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

spring:
  cloud:
    gateway:
      default-filters:
            - name: PrintAny
              args: # 手动指定参数名,无需按照参数顺序
                a: 1
                b: 2
                c: 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();

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 1.获取Request
        ServerHttpRequest request = exchange.getRequest();
        // 2.判断是否不需要拦截
        if(isExclude(request.getPath().toString())){
            // 无需拦截,直接放行
            return chain.filter(exchange);
        }
        // 3.获取请求头中的token
        String token = null;
        List<String> headers = request.getHeaders().get("authorization");
        if (!CollUtils.isEmpty(headers)) {
            token = headers.get(0);
        }
        // 4.校验并解析token
        Long 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;
    }

    @Override
    public 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 {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.获取请求头中的用户信息
        String userInfo = request.getHeader("user-info");
        // 2.判断是否为空
        if (StrUtil.isNotBlank(userInfo)) {
            // 不为空,保存到ThreadLocal
                UserContext.setUser(Long.valueOf(userInfo));
        }
        // 3.放行
        return true;
    }

    @Override
    public 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 {
    @Override
    public 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() {
        @Override
        public 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.coloradmin.cn/o/2286567.html

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

相关文章

【C语言】在Windows上为可执行文件.exe添加自定义图标

本文详细介绍了在 Windows 环境下,如何为使用 GCC 编译器编译的 C程序 添加自定义图标,从而生成带有图标的 .exe 可执行文件。通过本文的指导,读者可以了解到所需的条件以及具体的操作步骤,使生成的程序更具专业性和个性化。 目录 1. 准备条件2. 具体步骤步骤 1: 准备资源文…

计算机毕业设计Python+知识图谱大模型AI医疗问答系统 健康膳食推荐系统 食谱推荐系统 医疗大数据 机器学习 深度学习 人工智能 爬虫 大数据毕业设计

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 作者简介&#xff1a;Java领…

商品信息管理自动化测试

目录 前言 一、思维导图 二、代码编写 1.在pom.xml文件中添加相关依赖 2.自动化代码编写 三、代码测试 小结 前言 1. 针对商品信息管理项目进行测试&#xff0c;商品信息管理项目主要有商品列表页、部门列表页、员工列表页&#xff0c;主要功能&#xff1a;对商品信息的…

【实践】基于SakuraLLM的离线日文漫画及视频汉化

介绍 LLM 大型语言模型&#xff08;英语&#xff1a;large language model&#xff0c;LLM&#xff09;&#xff0c;也称大语言模型&#xff0c;是由具有大量参数&#xff08;通常数十亿个权重或更多&#xff09;的人工神经网络组成的一类语言模型。在进行语言理解与分析&…

常见的同态加密算法收集

随着对crypten与密码学的了解&#xff0c;我们将逐渐深入学习相关知识。今天&#xff0c;我们将跟随同态加密的发展历程对相关算法进行简单的收集整理 。 目录 同态加密概念 RSA算法 ElGamal算法 ELGamal签名算法 Paillier算法 BGN方案 Gentry 方案 BGV 方案 BFV 方案…

SSM-MyBatis-总结

文章目录 一、Hello MyBatis1.1 流程1.2 总结 二、Crud 的一些注意点三、参数传递3.1 #{ } VS ${ }3.2 单、复参数传递&#xff08;1&#xff09;单参数&#xff08;2&#xff09;多参数 -- Param&#xff08;3&#xff09;总结 四、查询结果返回--结果封装4.1 ResultType 一般…

万字长文总结前端开发知识---JavaScriptVue3Axios

JavaScript学习目录 一、JavaScript1. 引入方式1.1 内部脚本 (Inline Script)1.2 外部脚本 (External Script) 2. 基础语法2.1 声明变量2.2 声明常量2.3 输出信息 3. 数据类型3.1 基本数据类型3.2 模板字符串 4. 函数4.1 具名函数 (Named Function)4.2 匿名函数 (Anonymous Fun…

Flutter android debug 编译报错问题。插件编译报错

下面相关内容 都以 Mac 电脑为例子。 一、问题 起因&#xff1a;&#xff08;更新 Android studio 2024.2.2.13、 Flutter SDK 3.27.2&#xff09; 最近 2025年 1 月 左右&#xff0c;我更新了 Android studio 和 Flutter SDK 再运行就会出现下面的问题。当然 下面的提示只是其…

【Proteus仿真】【51单片机】简易计算器系统设计

目录 一、主要功能 二、使用步骤 三、硬件资源 四、软件设计 五、实验现象 联系作者 一、主要功能 1、LCD1602液晶显示 2、矩阵按键​ 3、可以进行简单的加减乘除运算 4、最大 9999*9999 二、使用步骤 系统运行后&#xff0c;LCD1602显示数据&#xff0c;通过矩阵按键…

JavaScript函数中this的指向

总结&#xff1a;谁调用我&#xff0c;我就指向谁&#xff08;es6箭头函数不算&#xff09; 一、ES6之前 每一个函数内部都有一个关键字是 this &#xff0c;可以直接使用 重点&#xff1a; 函数内部的 this 只和函数的调用方式有关系&#xff0c;和函数的定义方式没有关系 …

51单片机入门_01_单片机(MCU)概述(使用STC89C52芯片;使用到的硬件及课程安排)

文章目录 1. 什么是单片机1.1 微型计算机的组成1.2 微型计算机的应用形态1.3 单板微型计算机1.4 单片机(MCU)1.4.1 单片机内部结构1.4.2 单片机应用系统的组成 1.5 80C51单片机系列1.5.1 STC公司的51单片机1.5.1 STC公司单片机的命名规则 2. 单片机的特点及应用领域2.1 单片机的…

51单片机入门_02_C语言基础0102

C语言基础部分可以参考我之前写的专栏C语言基础入门48篇 以及《从入门到就业C全栈班》中的C语言部分&#xff0c;本篇将会结合51单片机讲差异部分。 课程主要按照以下目录进行介绍。 文章目录 1. 进制转换2. C语言简介3. C语言中基本数据类型4. 标识符与关键字5. 变量与常量6.…

时间轮:XXL-JOB 高效、精准定时任务调度实现思路分析

大家好&#xff0c;我是此林。 定时任务是我们项目中经常会遇到的一个场景。那么如果让我们手动来实现一个定时任务框架&#xff0c;我们会怎么做呢&#xff1f; 1. 基础实现&#xff1a;简单的线程池时间轮询 最直接的方式是创建一个定时任务线程池&#xff0c;用户每提交一…

人工智能如何驱动SEO关键词优化策略的转型与效果提升

内容概要 随着数字化时代的到来&#xff0c;人工智能&#xff08;AI&#xff09;技术对各行各业的影响日益显著&#xff0c;在搜索引擎优化&#xff08;SEO&#xff09;领域尤为如此。AI的应用不仅改变了关键词研究的方法&#xff0c;而且提升了内容生成和搜索优化的效率&…

【NLP251】NLP RNN 系列网络

NLP251 系列主要记录从NLP基础网络结构到知识图谱的学习 &#xff11;.原理及网络结构 &#xff11;.&#xff11;&#xff32;&#xff2e;&#xff2e; 在Yoshua Bengio论文中( http://proceedings.mlr.press/v28/pascanu13.pdf )证明了梯度求导的一部分环节是一个指数模型…

【越学学糊涂的Linux系统】Linux指令篇(二)

一、pwd指令&#xff1a; 00x0:打印该用户当前目录下所属的文件路径 看指令框可以看出我用的是一个叫sw的用户&#xff0c;我们的路径就是在一个home目录下的sw目录下的class113文件路径。 也可以说是指出当前所处的工作目录 补充&#xff1a;&#x1f386;​​​​​​​Wi…

【AI论文】Omni-RGPT:通过标记令牌统一图像和视频的区域级理解

摘要&#xff1a;我们提出了Omni-RGPT&#xff0c;这是一个多模态大型语言模型&#xff0c;旨在促进图像和视频的区域级理解。为了在时空维度上实现一致的区域表示&#xff0c;我们引入了Token Mark&#xff0c;这是一组在视觉特征空间中突出目标区域的标记。这些标记通过使用区…

Java面试题2025-并发编程基础(多线程、锁、阻塞队列)

并发编程 一、线程的基础概念 一、基础概念 1.1 进程与线程A 什么是进程&#xff1f; 进程是指运行中的程序。 比如我们使用钉钉&#xff0c;浏览器&#xff0c;需要启动这个程序&#xff0c;操作系统会给这个程序分配一定的资源&#xff08;占用内存资源&#xff09;。 …

Three城市引擎地图插件Geo-3d

一、简介 基于Three开发&#xff0c;为Three 3D场景提供GIS能力和城市底座渲染能力。支持Web墨卡托、WGS84、GCJ02等坐标系&#xff0c;支持坐标转换&#xff0c;支持影像、地形、geojson建筑、道路&#xff0c;植被等渲染。支持自定义主题。 二、效果 三、代码 //插件初始化…

MySQL的复制

一、概述 1.复制解决的问题是让一台服务器的数据与其他服务器保持同步&#xff0c;即主库的数据可以同步到多台备库上&#xff0c;备库也可以配置成另外一台服务器的主库。这种操作一般不会增加主库的开销&#xff0c;主要是启用二进制日志带来的开销。 2.两种复制方式&#xf…