基于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.mzph.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…