基于Spring Security 6的OAuth2 系列之二十六 - 終章

之所以想寫這一系列,是因為之前工作過程中使用Spring Security OAuth2搭建了網關和授權服務器,但當時基于spring-boot 2.3.x,其默認的Spring Security是5.3.x。之后新項目升級到了spring-boot 3.3.0,結果一看Spring Security也升級為6.3.0。無論是Spring Security的風格和以及OAuth2都做了較大改動,里面甚至將授權服務器模塊都移除了,導致在配置同樣功能時,花費了些時間研究新版本的底層原理,這里將一些學習經驗分享給大家。

注意由于框架不同版本改造會有些使用的不同,因此本次系列中使用基本框架是 spring-boo-3.3.0(默認引入的Spring Security是6.3.0),JDK版本使用的是19,本系列OAuth2的代碼采用Spring Security6.3.0框架,所有代碼都在oauth2-study項目上:https://github.com/forever1986/oauth2-study.git

目錄

  • 1 整體架構
  • 2 前提條件準備
  • 3 授權服務器
  • 4 網關
    • 4.1 初始化項目
    • 4.2 配置nacos自動加載
    • 4.3 配置redis
    • 4.4 配置gateway以及資源服務器
  • 5 測試服務
  • 6 測試結果

前面我們通過二十幾章的內容,將新版本Spring Security 6實現OAuth2的大部分內容都講了一遍。相信沒有講到的內容,你自己現在也能通過看源代碼上手了。這一章作為最后,將以一個真實生成案例,做一個授權服務器+網關的組合,作為內部微服務訪問鑒權。(由于gateway默認采用的是netty作為服務器,并且支持Webflux響應式編程,因此本章也是基于上2章學習的響應式編程,搭建gateway)

1 整體架構

在微服務架構中,隨著微服務越來越多,一般會采用一個網關作為統一管理,但是網關也會涉及鑒權問題,而OAuth2經常為微服務架構提供統一的認證和授權服務。這里的鑒權需要說明一下,一種是訪問權限,一種是詳細的業務權限,假如一個前端服務需要訪問訂單服務,那么訂單服務就是訪問權限,能訪問多少訂單數據則屬于業務權限。而OAuth2只做訪問權限管理,也就是前端服務能訪問哪些后端服務,至于詳細業務權限還是需要每個后端服務自己實現。因此網關結合OAuth2其實是一個url訪問權限管理,底層是網關作為一個資源服務器,驗證access_token以及其scope去判斷url。

下面我們就通過授權服務器+網關的組合,展現真實環境下搭建一個以OAuth2為基礎的網關權限認證。

在這里插入圖片描述

1)前端應用通過登錄并獲得應用授權
2)授權服務器返回token
3)前端應用攜帶token去訪問API網關
4)API網關通過驗證token有效性以及鑒權情況
5)驗證通過后,則轉發請求

2 前提條件準備

1)mysql數據庫,建立一個oauth_study數據庫,里面建立4張表,分別是t_user(用戶信息),oauth2_registered_client(客戶端信息)、oauth2_authorization_consent(授權信息)和oauth2_authorization(授權確認信息)。SQL語句參考lesson20的create.sql附件。注意:由于數據庫和表都是沿用之前項目的,如果之前項目已經新建過表,則無需在新建,只需要執行insert.sql

-- 插入一個用戶lesson20
INSERT INTO oauth_study.t_user (username, password, email, phone) VALUES('lesson20', '{noop}1234', 'lesson20@demo.com', '13788888888');
-- 插入一個客戶端lesson20-client,其使用client_credentials客戶端模式,并且scopes包括一個api-a的值(角色)
INSERT INTO oauth_study.oauth2_registered_client (id, client_id, client_id_issued_at, client_secret, client_secret_expires_at, client_name, client_authentication_methods, authorization_grant_types, redirect_uris, post_logout_redirect_uris, scopes, client_settings, token_settings) VALUES('7f5b4fc6-9b32-42e0-89a3-012ad594a6e8', 'lesson20-client', '2025-02-11 16:04:01', '{bcrypt}$2a$10$ytMBTJDyysYDsI.aqSJche1/1de00KdPw4IeHZAMCT5T8SdxfciTq', NULL, 'lesson20-client', 'client_secret_basic', 'refresh_token,client_credentials', 'http://localhost:8080/login/oauth2/code/lesson20-client', 'http://localhost:8080/', 'api-a,openid,profile', '{"@class":"java.util.Collections$UnmodifiableMap","settings.client.require-proof-key":false,"settings.client.require-authorization-consent":true}', '{"@class":"java.util.Collections$UnmodifiableMap","settings.token.reuse-refresh-tokens":true,"settings.token.x509-certificate-bound-access-tokens":false,"settings.token.id-token-signature-algorithm":["org.springframework.security.oauth2.jose.jws.SignatureAlgorithm","RS256"],"settings.token.access-token-time-to-live":["java.time.Duration",300.000000000],"settings.token.access-token-format":{"@class":"org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat","value":"self-contained"},"settings.token.refresh-token-time-to-live":["java.time.Duration",3600.000000000],"settings.token.authorization-code-time-to-live":["java.time.Duration",300.000000000],"settings.token.device-code-time-to-live":["java.time.Duration",300.000000000]}');

2)redis緩存數據庫,用于存儲登錄的token以及權限數據
3)nacos注冊和配置中心,用于微服務注冊以及gateway網關的配置

3 授權服務器

1)授權服務器使用mysql存儲用戶信息以及客戶端信息
2)授權服務器使用前后端分離的方式(自定義登錄接口,屏蔽登錄界面)
3)登錄使用jwt生成token(非對稱加密),屏蔽原先session
4)登錄的token使用redis存儲

在這里插入圖片描述

注意:由于其功能與lesson06子模塊一樣,因此我們直接使用lesson06子模塊,需要了解詳情,請參考《系列之六 - 授權服務器–自定義授權頁面》

4 網關

1)通過讀取naocs實現路由配置,實現路由動態加載
2)通過讀取本地的公鑰,進行token驗證
3)通過讀取redis存儲的URL與授權的scope(角色)的關系,實現鑒權判斷

在這里插入圖片描述

代碼參考lesson20子模塊下面的gateway子模塊

4.1 初始化項目

1)在lesson20子模塊下面新建gateway子模塊,其pom引入

<dependencies><!-- 注意:引入spring-cloud-starter-gateway之后,并不需要在引入spring-boot-starter-web --><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId></dependency><!-- 引入spring-cloud-loadbalancer --><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-loadbalancer</artifactId></dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-oauth2-resource-server</artifactId></dependency><!--Spring Cloud2020之后移除了bootstrap加載,需要手動加入依賴--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-bootstrap</artifactId></dependency><!-- 引用PathMatcher匹配方法 --><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-oauth2-jose</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency></dependencies>

2)在resources下配置文件bootstrap.yml(注意:是bootstrap.yml,而非application.yml

server:port: 9001
spring:application:name: gateway-lesson20cloud:nacos:discovery:server-addr: 127.0.0.1:8848username: nacospassword: nacosconfig:server-addr: 127.0.0.1:8848username: nacospassword: nacosloadbalancer:ribbon:enabled: false#redis配置data:redis:host: 127.0.0.1

3)拷貝lesson06的resources目錄下的demo.jks文件到gateway子模塊的resources下面

4.2 配置nacos自動加載

1)nacos上面配置gateway-lesson20的json格式配置文件,在public命名空間下,DEFAULT_GROUP分組下面

在這里插入圖片描述

2)gateway-lesson20的內容如下

[{"id": "auth","predicates": [{"args": {"pattern": "/auth/**"},"name": "Path"}],"uri": "http://localhost:9000","filters": [{"args": {"_genkey_0": "1"},"name": "StripPrefix"}]},{"id": "api-a","predicates": [{"args": {"pattern": "/api-a/**"},"name": "Path"}],"uri": "http://localhost:9002","filters": [{"args": {"_genkey_0": "1"},"name": "StripPrefix"}]}
]

3)在config包下面配置nacos自動加載代碼NacosRouteDefinitionRepository

/*** nacos路由數據源*/
@Component
public class NacosRouteDefinitionRepository implements RouteDefinitionRepository {private static final Logger logger = LoggerFactory.getLogger(NacosRouteDefinitionRepository.class);private static final String SCG_DATA_ID = "gateway-lesson20";private static final String SCG_GROUP_ID = "DEFAULT_GROUP";private final ApplicationEventPublisher publisher;private final NacosConfigManager nacosConfigManager;public NacosRouteDefinitionRepository(ApplicationEventPublisher publisher, NacosConfigManager nacosConfigManager) {this.publisher = publisher;this.nacosConfigManager = nacosConfigManager;addListener();}@Overridepublic Flux<RouteDefinition> getRouteDefinitions() {List<RouteDefinition> routeDefinitionList = new ArrayList<>(0);try {String configContent = nacosConfigManager.getConfigService().getConfig(SCG_DATA_ID, SCG_GROUP_ID, 5000);if (!Strings.isNullOrEmpty(configContent)) {routeDefinitionList = JSON.parseArray(configContent, RouteDefinition.class);}} catch (NacosException e) {logger.error("從Nacos加載配置的動態路由信息異常", e);}return Flux.fromIterable(routeDefinitionList);}/*** 添加Nacos監聽*/private void addListener() {try {nacosConfigManager.getConfigService().addListener(SCG_DATA_ID, SCG_GROUP_ID, new Listener() {@Overridepublic Executor getExecutor() {return null;}@Overridepublic void receiveConfigInfo(String configInfo) {publisher.publishEvent(new RefreshRoutesEvent(this));}});} catch (NacosException e) {logger.error("添加Nacos監聽異常", e);}}@Overridepublic Mono<Void> save(Mono<RouteDefinition> route) {return null;}@Overridepublic Mono<Void> delete(Mono<String> routeId) {return null;}
}

4.3 配置redis

1)在config包下面配置redis配置類RedisConfiguration

@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)在controller下面新建RedisController,用于初始化redis數據

@RestController
public class RedisController {@Autowiredprivate RedisTemplate redisTemplate;@GetMapping("/init")public void init() {Map<String, List<String>> urlPermRolesRules = new ConcurrentHashMap<>();List<String> roles = new ArrayList<>();// /api-a/*的所有接口都需要api-a的角色才能訪問roles.add("api-a");urlPermRolesRules.put("/api-a/*", roles);redisTemplate.opsForHash().putAll(ResourceServerManager.AUTHORITIES_URL_ROLE,urlPermRolesRules);}
}

4.4 配置gateway以及資源服務器

1)在包security下,新建以反應式方式設置鑒權類ResourceServerManager

@Component
@Slf4j
public class ResourceServerManager implements ReactiveAuthorizationManager<AuthorizationContext> {private static final String AUTHORIZATION_KEY ="Authorization";private static final String AUTHORITIES_SCOPE_PREFIX = "SCOPE_";public static final String AUTHORITIES_URL_ROLE = "AUTHORITIES_URL_ROLE";@Autowiredprivate RedisTemplate redisTemplate;@Overridepublic Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {ServerHttpRequest request = authorizationContext.getExchange().getRequest();// 預檢請求放行if (request.getMethod() == HttpMethod.OPTIONS) {return Mono.just(new AuthorizationDecision(true));}// 訪問登錄和授權的請求,放行PathMatcher pathMatcher = new AntPathMatcher();String path = request.getURI().getPath();if (pathMatcher.match("/auth/**", path)|| pathMatcher.match("/init", path)) {return Mono.just(new AuthorizationDecision(true));}// 判斷token(必須在Authorization中有)String token = "";boolean tokenCheckFlag = false;if(Objects.nonNull(request.getHeaders().getFirst(AUTHORIZATION_KEY)) && !Strings.isNullOrEmpty(request.getHeaders().getFirst(AUTHORIZATION_KEY))){token = request.getHeaders().getFirst(AUTHORIZATION_KEY);tokenCheckFlag = true;}if (!tokenCheckFlag) {return Mono.just(new AuthorizationDecision(false));}log.info("判斷權限");// 緩存中獲取url與角色的對應關系Map<String, List<String>> urlPermRolesRules = redisTemplate.opsForHash().entries(AUTHORITIES_URL_ROLE);// 獲取當前資源 所需要的角色List<String> authorizedRoles = new ArrayList<>(); // 擁有訪問權限的角色for (Map.Entry<String, List<String>> permRoles : urlPermRolesRules.entrySet()) {String perm = permRoles.getKey();if (pathMatcher.match(perm, path)) {List<String> values = permRoles.getValue();authorizedRoles.addAll(values);}}// 判斷JWT中攜帶的scope,對應在緩存中有/api-a/*權限的角色Mono<AuthorizationDecision> authorizationDecisionMono = mono.filter(Authentication::isAuthenticated).flatMapIterable(Authentication::getAuthorities).map(GrantedAuthority::getAuthority).any(authority -> {String roleCode = authority.substring(AUTHORITIES_SCOPE_PREFIX.length()); // 用戶的角色boolean hasAuthorized = !CollectionUtils.isEmpty(authorizedRoles) && authorizedRoles.contains(roleCode);return hasAuthorized;}).map(AuthorizationDecision::new).defaultIfEmpty(new AuthorizationDecision(false));return authorizationDecisionMono;}
}

2)在config包下,新建ResourceServerConfig,配置網關為資源服務器

@RequiredArgsConstructor
@Configuration
@EnableWebFluxSecurity //注意該注解,是使用Flux方式
public class ResourceServerConfig {private final ResourceServerManager resourceServerManager;@Beanpublic SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {// 本地獲取公鑰http.oauth2ResourceServer(server ->server.jwt(jwt->jwt.jwtDecoder(jwtDecoder())));// 配置路由攔截http.authorizeExchange(exchange->// 設置resourceServerManagerexchange.anyExchange().access(resourceServerManager))// 處理未授權.exceptionHandling(ex->ex.accessDeniedHandler(accessDeniedHandler()))// 關閉csrf.csrf(ServerHttpSecurity.CsrfSpec::disable);return http.build();}/*** 自定義未授權響應*/@BeanServerAccessDeniedHandler accessDeniedHandler() {return (exchange, denied) -> {Mono<Void> mono = Mono.defer(() -> Mono.just(exchange.getResponse())).flatMap(response -> ResponseUtils.writeErrorInfo(response));return mono;};}/*** 自定義JwtDecoder*/public ReactiveJwtDecoder jwtDecoder() {NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withPublicKey(generateRsaKey()).build();return jwtDecoder;}/*** 其 key 在啟動時生成,用于創建上述 JWKSource*/private static RSAPublicKey generateRsaKey() {KeyStoreKeyFactory factory = new KeyStoreKeyFactory(new ClassPathResource("demo.jks"), "linmoo".toCharArray());KeyPair keyPair = factory.getKeyPair("demo", "linmoo".toCharArray());RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();return publicKey;}
}

3)在utils包下面,新建ResponseUtils工具類

public class ResponseUtils {public static Mono<Void> writeErrorInfo(ServerHttpResponse response) {response.setStatusCode(HttpStatus.UNAUTHORIZED);response.getHeaders().set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);response.getHeaders().set("Access-Control-Allow-Origin", "*");response.getHeaders().set("Cache-Control", "no-cache");String body = "{\\\"code\\\":\\\"\"" + "0001" + '\"' + "\", \\\"msg\\\":\\\"\"" + "無權限" + '\"' + "'}";DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));return response.writeWith(Mono.just(buffer)).doOnError(error -> DataBufferUtils.release(buffer));}}

4)設置啟動類

@SpringBootApplication
@EnableDiscoveryClient
public class Oauth2Lesson20GatewayApplication {public static void main(String[] args) {SpringApplication.run(Oauth2Lesson20GatewayApplication .class, args);}}

5 測試服務

使用一個訪問api-a的服務,作為測試后端服務

代碼參考lesson20子模塊下面的api-a子模塊

1)在lesson20子模塊下面新建api-a子模塊,其pom引入如下:

<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webflux</artifactId></dependency>
</dependencies>

2)其resources下的yaml如下:

server:port: 9002

3)在controller目錄新建DemoController測試接口,我們這里也采用響應式編程方式。

@RestController
public class DemoController {@GetMapping("/demo")public Mono<String> demo() {return Mono.just("demo");}
}

4)新建啟動類Oauth2Lesson20ApiApplication

@SpringBootApplication
public class Oauth2Lesson20ApiApplication {public static void main(String[] args) {SpringApplication.run(Oauth2Lesson20ApiApplication .class, args);}}

6 測試結果

分別啟動:lesson06子模塊,gateway子模塊和api-a子模塊

1)先訪問:http://localhost:9001/init 將權限和角色綁定設置到redis中

在這里插入圖片描述

2)訪問:http://localhost:9001/auth/login 進行登錄,得到登錄的token(注意:這里訪問的是gateway,通過gateway轉發到授權服務器上)

在這里插入圖片描述

3)訪問:http://localhost:9001/auth/oauth2/token 獲取access_token。在步驟1中獲得到的登錄token,放入headers中的access_token,這里使用的是客戶端模式。(注意:這里訪問的是gateway,通過gateway轉發到授權服務器上)

在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

4)訪問:http://localhost:9001/api-a/demo 在步驟2中獲取到的的access_token,訪問可以得到demo結果(注意這里也是訪問gateway,由gateway判斷token有效性以及鑒權,然后轉發到api-a服務上)

在這里插入圖片描述

4)假如在步驟2中的scope參數沒有api-a,通過這樣獲取的access_token是無法訪問demo的,大家可以測試一下

在這里插入圖片描述

結語:至此,我們對Spring Security 6實現OAuth2的內容就介紹完了,當然還有很多內容沒有介紹,不過相信通過這一系列,你應該已經掌握了底層原理了,自己就可以去看其它的功能。感謝陪伴!

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/76825.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/76825.shtml
英文地址,請注明出處:http://en.pswp.cn/web/76825.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

一鍵配置多用戶VNC遠程桌面:自動化腳本詳解

在當今遠程工作盛行的時代,高效且安全地管理多用戶遠程桌面訪問變得至關重要。本文將介紹一個強大的自動化腳本,該腳本能夠快速創建用戶并配置VNC遠程桌面環境,大大簡化了系統管理員的工作。 一、背景介紹 在Linux系統中,手動配置VNC服務器通常需要執行多個步驟,包括創建…

IOT項目——雙軸追光系統

雙軸太陽能追光系統 - ESP32實現 系統概述 這個系統使用&#xff1a; ESP32開發板2個舵機&#xff08;水平方向和垂直方向&#xff09;4個光敏電阻&#xff08;用于檢測光照方向&#xff09;適當的電阻&#xff08;用于光敏電阻分壓&#xff09; 接線示意圖 --------------…

Maven集成模塊打包使用

文章目錄 1.問題思考&#xff08;如何對集成模塊進行打包&#xff09;2.問題解決 &#xff08;如何對集成模塊進行打包&#xff09;3.使用者使用該jar包(jar包安裝本地倉庫和使用) 1.問題思考&#xff08;如何對集成模塊進行打包&#xff09; 思考&#xff1a;假設有這么一個場…

OpenVINO教程(二):圖片目標檢測推理應用

YOLO模型物體檢測 下面是一個簡單的python程序,他的功能是使用yolo11n模型對coco_bike.jpg照片進行檢測,并顯示檢測結果 代碼步驟如下: coco_bike.jpg照片加載yolo模型使用模型進行detect推理顯示推理結果 下面是完整的代碼 from pathlib import Pathimport urllib.request…

聚類算法(K-means、DBSCAN)

聚類算法 K-means 算法 算法原理 K-means 是一種基于類內距離最小化的劃分式聚類算法&#xff0c;其核心思想是通過迭代優化將數據劃分為 K 個簇。目標函數為最小化平方誤差&#xff08;SSE&#xff09;&#xff1a; S S E ∑ i 1 K ∑ x ∈ C i ∣ ∣ x ? μ i ∣ ∣ 2…

Oracle在ERP市場擊敗SAP

2024年&#xff0c;甲骨文&#xff08;Oracle&#xff09;以87億美元的ERP收入和6.63%的市場份額&#xff0c;首次超越SAP&#xff0c;成為全球最大的ERP應用軟件供應商&#xff0c;結束了SAP自上世紀80年代以來在該領域的長期霸主地位。據APPS RUN THE WORLD的市場調研&#x…

嵌入式面試高頻筆試題目解析

一、基礎概念與 C 語言核心題 1. 指針與內存操作 典型題目: char str[] = "hello"; char *ptr = "world"; str[0] = H; // 合法嗎? ptr[0] = W; // 合法嗎?為什么?解析: str 是棧上數組,可修改內容,str[0]=H 合法。ptr 指向常量字符串區,修改會…

【Python】Selenium切換網頁的標簽頁的寫法(全!!!)

在使用selenium做網站爬取測試的時候&#xff0c;我們經常會遇到一些需要點擊的元素&#xff0c;才能點擊到我們想要進入的頁面&#xff0c; 于是我們就要模擬 不斷地 點點點擊 鼠標的樣子。 這個時候網頁上就會有很多的標簽頁&#xff0c;你的瀏覽器網頁標簽欄 be like: 那…

MySQL GTID模式主從同步配置全指南:從配置到故障轉移

前言 MySQL主從復制是企業級數據庫架構的基礎&#xff0c;而GTID(Global Transaction Identifier)模式則是MySQL 5.6版本后推出的革命性復制技術。本文將詳細介紹如何配置基于GTID的主從同步&#xff0c;并包含實用的故障轉移操作指南。 一、GTID模式核心優勢 相比傳統基于…

MAC系統下完全卸載Android Studio

刪除以下文件 /Applications/Android Studio.app /Users/用戶名/Library/Application Support/Google/AndroidStudio2024.2 /Users/用戶名/Library/Google/AndroidStudio /Users/用戶名/Library/Preferences/com.google.android.studio.plist /Users/用戶名/Library/Cache…

<C#>.NET WebAPI 的 FromBody ,FromForm ,FromServices等詳細解釋

在 .NET 8 Web API 中&#xff0c;[FromBody]、[FromForm]、[FromHeader]、[FromKeyedServices]、[FromQuery]、[FromRoute] 和 [FromServices] 這些都是用于綁定控制器動作方法參數的特性&#xff0c;下面為你詳細解釋這些特性。 1. [FromBody] 作用&#xff1a;從 HTTP 請求…

# 透視 Linux 內核:Socket 機制的底層架構與運行邏輯深度解析

在由 Linux 操作系統構建的龐大網絡生態中&#xff0c;Socket 作為網絡通信的核心樞紐&#xff0c;承載著不同主機間應用進程的數據交互重任。無論是日常的網頁瀏覽、在線游戲&#xff0c;還是復雜的分布式系統通信&#xff0c;Socket 都在幕后扮演著關鍵角色。盡管多數開發者對…

# 利用遷移學習優化食物分類模型:基于ResNet18的實踐

利用遷移學習優化食物分類模型&#xff1a;基于ResNet18的實踐 在深度學習的眾多應用中&#xff0c;圖像分類一直是一個熱門且具有挑戰性的領域。隨著研究的深入&#xff0c;我們發現利用預訓練模型進行遷移學習是一種非常有效的策略&#xff0c;可以顯著提高模型的性能&#…

Excel提取圖片并自動上傳到文件服務器(OOS),獲取文件鏈接

Excel提取圖片并自動上傳到接口 在實際項目中&#xff0c;我們可能經常會遇到需要批量從Excel文件&#xff08;.xlsx&#xff09;中提取圖片并上傳到特定接口的場景。今天&#xff0c;我就詳細介紹一下如何使用Python實現這一功能&#xff0c;本文會手把手教你搭建一個完整的解…

jmeter利用csv進行參數化和自動斷言

1.測試數據 csv測試數據如下&#xff08;以注冊接口為例&#xff09; 2.jemer參數化csv設置 打開 jmeter&#xff0c;添加好線程組、HTTP信息頭管理器、CSV 數據文件設置、注冊請求、響應斷言、查看結果樹 1&#xff09; CSV 數據文件設置 若 CSV 中數據包含中文&#xff0c;…

騰訊云對象存儲m3u8文件使用騰訊播放器播放

參考騰訊云官方文檔&#xff1a; 播放器 SDK Demo 體驗_騰訊云 重要的一步來了&#xff1a; 登錄騰訊云控制臺&#xff0c;找到對象存儲的存儲桶。 此時&#xff0c;再去刷新剛才創建的播放器html文件&#xff0c;即可看到播放畫面了。

CSS 美化頁面(五)

一、position屬性 屬性值??描述??應用場景?static默認定位方式&#xff0c;元素遵循文檔流正常排列&#xff0c;top/right/bottom/left 屬性無效?。普通文檔流布局&#xff0c;默認布局&#xff0c;無需特殊定位。relative相對定位&#xff0c;相對于元素原本位置進行偏…

Spring MVC 核心注解與文件上傳教程

一、RequestBody 注解詳解 1. 基本使用 作用&#xff1a;從 HTTP 請求體中獲取數據&#xff0c;適用于 POST/PUT 請求。 限制&#xff1a;GET 請求無請求體&#xff0c;不可使用該注解。 示例代碼 Controller RequestMapping("/demo01") public class Demo01Cont…

js原型鏈prototype解釋

function Person(){} var personnew Person() console.log(啊啊,Person instanceof Function);//true console.log(,Person.__proto__Function.prototype);//true console.log(,Person.prototype.__proto__ Object.prototype);//true console.log(,Function.prototype.__prot…

為您的照片提供本地 AI 視覺:使用 Llama Vision 和 ChromaDB 構建 AI 圖像標記器

有沒有花 20 分鐘瀏覽您的文件夾以找到心中的特定圖像或屏幕截圖&#xff1f;您并不孤單。 作為工作中的產品經理&#xff0c;我總是淹沒在競爭對手產品的屏幕截圖、UI 靈感以及白板會議或草圖的照片的海洋中。在我的個人生活中&#xff0c;我總是捕捉我在生活中遇到的事物&am…