網關登錄校驗

網關登錄校驗

單體架構時我們只需要完成一次用戶登錄、身份校驗,就可以在所有業務中獲取到用戶信息。而微服務拆分后,每個微服務都獨立部署,不再共享數據。也就意味著每個微服務都需要做登錄校驗,這顯然不可取。

鑒權思路分析

登錄授權的功能寫在了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.pswp.cn/web/67629.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/67629.shtml
英文地址,請注明出處:http://en.pswp.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…

我的創作紀念日——成為創作者的 第365天(1年)

機緣 考研的結果讓我感到一陣絕望&#xff0c;就像單片機突然死機一樣&#xff0c;所有的努力像是被一場意外的中斷指令打亂了邏輯流程。曾經本科時因為競賽拿了一堆獎&#xff0c;內心充滿虛榮心和成就感&#xff0c;總覺得自己是一個“天選之子”&#xff0c;但考研的失利卻像…

React 封裝高階組件 做路由權限控制

React 高階組件是什么 官方解釋∶ 高階組件&#xff08;HOC&#xff09;是 React 中用于復用組件邏輯的一種高級技巧。HOC 自身不是 React API 的一部分&#xff0c;它是一種基于 React 的組合特性而形成的設計模式。 高階組件&#xff08;HOC&#xff09;就是一個函數&…

【玩轉全棧】--創建一個自己的vue項目

目錄 vue介紹 創建vue項目 vue頁面介紹 element-plus組件庫 啟動項目 vue介紹 Vue.js 是一款輕量級、易于上手的前端 JavaScript 框架&#xff0c;旨在簡化用戶界面的開發。它采用了響應式數據綁定和組件化的設計理念&#xff0c;使得開發者可以通過聲明式的方式輕松管理數據和…

DS并查集(17)

文章目錄 前言一、何為并查集&#xff1f;二、并查集的實現&#xff1f;并查集的初始化查找元素所在的集合判斷兩個元素是否在同一個集合合并兩個元素所在的集合獲取并查集中集合的個數并查集的路徑壓縮 三、來兩道題練練手&#xff1f;省份的數量等式方程的可滿足性 總結 前言…

Appium介紹

在使用不同版本的Appium包進行自動化測試時&#xff0c;出現警告問題可能是由于版本不兼容、配置不正確等原因導致的。下面將詳細介紹解決這些問題的步驟&#xff0c;確保模擬器能夠正常啟動&#xff0c;并能在Appium查看器中同步顯示。 1. 環境準備 首先&#xff0c;確保你已…

minimind - 從零開始訓練小型語言模型

大語言模型&#xff08;LLM&#xff09;領域&#xff0c;如 GPT、LLaMA、GLM 等&#xff0c;雖然它們效果驚艷&#xff0c; 但動輒10 Bilion龐大的模型參數個人設備顯存遠不夠訓練&#xff0c;甚至推理困難。 幾乎所有人都不會只滿足于用Lora等方案fine-tuing大模型學會一些新的…

【C++動態規劃 離散化】1626. 無矛盾的最佳球隊|2027

本文涉及知識點 C動態規劃 離散化 LeetCode1626. 無矛盾的最佳球隊 假設你是球隊的經理。對于即將到來的錦標賽&#xff0c;你想組合一支總體得分最高的球隊。球隊的得分是球隊中所有球員的分數 總和 。 然而&#xff0c;球隊中的矛盾會限制球員的發揮&#xff0c;所以必須選…

CSS 值和單位詳解:從基礎到實戰

CSS 值和單位詳解&#xff1a;從基礎到實戰 1. 什么是 CSS 的值&#xff1f;示例代碼&#xff1a;使用顏色關鍵字和 RGB 函數 2. 數字、長度和百分比2.1 長度單位絕對長度單位相對長度單位 2.2 百分比 3. 顏色3.1 顏色關鍵字3.2 十六進制 RGB 值3.3 RGB 和 RGBA 值3.4 HSL 和 H…

Privacy Eraser,電腦隱私的終極清除者

Privacy Eraser 是一款專為保護用戶隱私而設計的全能型軟件&#xff0c;它不僅能夠深度清理計算機中的各類隱私數據&#xff0c;還提供了多種系統優化工具&#xff0c;幫助用戶提升設備的整體性能。通過這款軟件&#xff0c;用戶可以輕松清除瀏覽器歷史記錄、緩存文件、Cookie、…

Android 啟動流程

一 Bootloader 階段 在嵌入式系統中&#xff0c;Bootloader的引導過程與傳統的PC環境有所不同&#xff0c;主要是因為嵌入式系統的硬件配置和應用場景更加多樣化。以下是嵌入式系統中Bootloader被引導的一般流程&#xff1a; 1. 硬件復位 當嵌入式設備上電或復位時&#xff…

【數據結構與算法】AVL樹的插入與刪除實現詳解

文章目錄 前言Ⅰ. AVL樹的定義Ⅱ. AVL樹節點的定義Ⅲ. AVL樹的插入Insert一、節點的插入二、插入的旋轉① 新節點插入較高左子樹的左側&#xff08;左左&#xff09;&#xff1a;右單旋② 新節點插入較高右子樹的右側&#xff08;右右&#xff09;&#xff1a;左單旋③ 新節點插…

SCRM開發為企業提供全面客戶管理解決方案與創新實踐分享

內容概要 在當今的商業環境中&#xff0c;客戶關系管理&#xff08;CRM&#xff09;變得越來越重要。而SCRM&#xff08;社交客戶關系管理&#xff09;作為一種新興的解決方案&#xff0c;正在幫助企業徹底改變與客戶的互動方式。快鯨SCRM是一個引人注目的工具&#xff0c;它通…

AI應用部署——streamlit

如何把項目部署到一個具有公網ip地址的服務器上&#xff0c;讓他人看到&#xff1f; 可以利用 streamlit 的社區云免費部署 1、生成requirements.txt文件 終端輸入pip freeze > requirements.txt即可 requirements.txt里既包括自己安裝過的庫&#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…

【Elasticsearch】match_bool_prefix 查詢 vs match_phrase_prefix 查詢

Match Bool Prefix Query vs. Match Phrase Prefix Query 在 Elasticsearch 中&#xff0c;match_bool_prefix 查詢和 match_phrase_prefix 查詢雖然都支持前綴匹配&#xff0c;但它們的行為和用途有所不同。以下是它們之間的主要區別&#xff1a; 1. match_bool_prefix 查詢…

算法基礎——存儲

引入 基礎理論的進步&#xff0c;是推動技術實現重大突破&#xff0c;促使相關領域的技術達成跨越式發展的核心。 在發展日新月異的大數據領域&#xff0c;基礎理論的核心無疑是算法。不管是技術設計&#xff0c;還是工程實踐&#xff0c;都必須仰仗相關算法的支持&#xff0…

正則表達式入門

入門 1、提取文章中所有的英文單詞 //1&#xff0e;先創建一個Pattern對象&#xff0c;模式對象&#xff0c;可以理解成就是一個正則表達式對象 Pattern pattern Pattern.compile("[a-zA-Z]"); //2&#xff0e;創建一個匹配器對象 //理解:就是 matcher匹配器按照p…