微服务OAuth 2.1认证授权可行性方案(Spring Security 6)

文章目录

  • 一、背景
  • 二、微服务架构介绍
  • 三、认证服务器
      • 1. 数据库创建
      • 2. 新建模块
      • 3. 导入依赖和配置
      • 4. 安全认证配置类
  • 四、认证服务器测试
      • 1. AUTHORIZATION_CODE(授权码模式)
          • 1. 获取授权码
          • 2. 获取JWT
      • 2. CLIENT_CREDENTIALS(客户端凭证模式)
  • 五、Gateway
      • 1. 引入依赖
      • 2. 添加白名单文件
      • 3. 全局过滤器
      • 4. 获取远程JWKS
      • 5. 校验JWT
      • 6. 测试(如何携带JWT)
  • 六、后记

一、背景

Oauth2停止维护,基于OAuth 2.1OpenID Connect 1.0Spring Authorization Server模块独立于SpringCloud

本文开发环境如下:

Version
Java17
SpringCloud2023.0.0
SpringBoot3.2.1
Spring Authorization Server1.2.1
Spring Security6.2.1
mysql8.2.0

https://spring.io/projects/spring-security#learn
https://spring.io/projects/spring-authorization-server#learn

二、微服务架构介绍

一个认证服务器(也是一个微服务),专门用于颁发JWT。
一个网关(也是一个微服务),用于白名单判断和JWT校验。
若干微服务。

本文的关键在于以下几点:

  • 搭建认证服务器
  • 网关白名单判断
  • 网关验证JWT
  • 认证服务器如何共享公钥,让其余微服务有JWT自校验的能力。
    在这里插入图片描述

三、认证服务器

这里是官方文档https://spring.io/projects/spring-authorization-server#learn
基本上跟着Getting Started写完就可以。

1. 数据库创建

新建一个数据库xc_users
然后执行jar里自带的三个sql
在这里插入图片描述
这一步官方并没有给出,大概因为可以使用内存存储,在简单demo省去了持久化。不建立数据库可能也是可行的,我没试过。

2. 新建模块

新建一个auth模块,作为认证服务器。

3. 导入依赖和配置

<dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-oauth2-authorization-server</artifactId></dependency>
server:servlet:context-path: /authport: 63070
spring:application:name: auth-serviceprofiles:active: devdatasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://192.168.101.65:3306/xc_users?serverTimezone=UTC&userUnicode=true&useSSL=false&username: rootpassword: 1009

4. 安全认证配置类

@Configuration
@EnableWebSecurity
public class AuthServerSecurityConfig {}

里面包含诸多内容,有来自Spring Security的,也有来自的Spring Authorization Server的。

  1. UserDetailsService 的实例,用于检索用户进行身份验证。
    @Beanpublic UserDetailsService userDetailsService() {UserDetails userDetails = User.withUsername("lisi").password("456").roles("read").build();return new InMemoryUserDetailsManager(userDetails);}
  1. 密码编码器(可选,本文不用)
    @Beanpublic PasswordEncoder passwordEncoder() {// 密码为明文方式return NoOpPasswordEncoder.getInstance();// 或使用 BCryptPasswordEncoder
//         return new BCryptPasswordEncoder();}
  1. 协议端点的 Spring Security 过滤器链
@Bean@Order(1)public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)throws Exception {OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);http.getConfigurer(OAuth2AuthorizationServerConfigurer.class).oidc(Customizer.withDefaults());	// Enable OpenID Connect 1.0http// Redirect to the login page when not authenticated from the// authorization endpoint.exceptionHandling((exceptions) -> exceptions.defaultAuthenticationEntryPointFor(new LoginUrlAuthenticationEntryPoint("/login"),new MediaTypeRequestMatcher(MediaType.TEXT_HTML)))// Accept access tokens for User Info and/or Client Registration.oauth2ResourceServer((resourceServer) -> resourceServer.jwt(Customizer.withDefaults()));return http.build();}
  1. 用于身份验证的 Spring Security 过滤器链。
    至于哪些要校验身份,哪些不用,根据自己需求写。
@Bean@Order(2)public SecurityFilterChain defaultFilterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests((authorize) ->authorize.requestMatchers(new AntPathRequestMatcher("/actuator/**")).permitAll().requestMatchers(new AntPathRequestMatcher("/login")).permitAll().requestMatchers(new AntPathRequestMatcher("/oauth2/**")).permitAll().requestMatchers(new AntPathRequestMatcher("/**/*.html")).permitAll().requestMatchers(new AntPathRequestMatcher("/**/*.json")).permitAll().requestMatchers(new AntPathRequestMatcher("/auth/**")).permitAll().anyRequest().authenticated()).formLogin(Customizer.withDefaults()).oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())));return http.build();}
  1. 自定义验证转化器(可选)
    private JwtAuthenticationConverter jwtAuthenticationConverter() {JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter();// 此处可以添加自定义逻辑来提取JWT中的权限等信息// jwtConverter.setJwtGrantedAuthoritiesConverter(...);return jwtConverter;}
  1. 用于管理客户端的 RegisteredClientRepository 实例
	@Beanpublic RegisteredClientRepository registeredClientRepository() {RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString()).clientId("XcWebApp")
//                .clientSecret("{noop}XcWebApp").clientSecret("XcWebApp").clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC).authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE).authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN).authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS).redirectUri("http://www.51xuecheng.cn")
//                .postLogoutRedirectUri("http://localhost:63070/login?logout").scope("all").scope(OidcScopes.OPENID).scope(OidcScopes.PROFILE).scope("message.read").scope("message.write").scope("read").scope("write").clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build()).tokenSettings(TokenSettings.builder().accessTokenTimeToLive(Duration.ofHours(2))  // 设置访问令牌的有效期.refreshTokenTimeToLive(Duration.ofDays(3))  // 设置刷新令牌的有效期.reuseRefreshTokens(true)                   // 是否重用刷新令牌.build()).build();return new InMemoryRegisteredClientRepository(registeredClient);}
  1. 用于对访问令牌进行签名的实例
    @Beanpublic JWKSource<SecurityContext> jwkSource() {KeyPair keyPair = generateRsaKey();RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();RSAKey rsaKey = new RSAKey.Builder(publicKey).privateKey(privateKey).keyID(UUID.randomUUID().toString()).build();JWKSet jwkSet = new JWKSet(rsaKey);return new ImmutableJWKSet<>(jwkSet);}private static KeyPair generateRsaKey() {KeyPair keyPair;try {KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");keyPairGenerator.initialize(2048);keyPair = keyPairGenerator.generateKeyPair();}catch (Exception ex) {throw new IllegalStateException(ex);}return keyPair;}
  1. 用于解码签名访问令牌的JwtDecoder 实例
    @Beanpublic JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);}
  1. 用于配置Spring Authorization ServerAuthorizationServerSettings 实例
    @Beanpublic AuthorizationServerSettings authorizationServerSettings() {return AuthorizationServerSettings.builder().build();}

这里可以设置各种端点的路径,默认路径点开builder()即可看到,如下

public static Builder builder() {return new Builder().authorizationEndpoint("/oauth2/authorize").deviceAuthorizationEndpoint("/oauth2/device_authorization").deviceVerificationEndpoint("/oauth2/device_verification").tokenEndpoint("/oauth2/token").jwkSetEndpoint("/oauth2/jwks").tokenRevocationEndpoint("/oauth2/revoke").tokenIntrospectionEndpoint("/oauth2/introspect").oidcClientRegistrationEndpoint("/connect/register").oidcUserInfoEndpoint("/userinfo").oidcLogoutEndpoint("/connect/logout");}

这里我必须吐槽一下,qnmd /.well-known/jwks.json,浪费我一下午。获取公钥信息的端点现在已经替换成了/oauth2/jwks。

四、认证服务器测试

基本上跟着Getting Started走就行。只不过端点的变动相较于Oauth2很大,还有使用方法上不同。

在配置RegisteredClient的时候,我们设置了三种GrantType,这里只演示两种AUTHORIZATION_CODECLIENT_CREDENTIALS

1. AUTHORIZATION_CODE(授权码模式)

1. 获取授权码

用浏览器打开以下网址,

http://localhost:63070/auth/oauth2/authorize?client_id=XcWebApp&response_type=code&scope=all&redirect_uri=http://www.51xuecheng.cn

对应oauth2/authorize端点,后面的参数和当时设置RegisteredClient 保持对应就行。response_type一定是code
进入到登陆表单,输入lisi - 456登陆。
在这里插入图片描述
选择all,同意请求。
在这里插入图片描述
url被重定向到http://www.51xuecheng.cn,并携带一个code,这就是授权码。

http://www.51xuecheng.cn/?code=9AexK_KFH1m3GiNBKsc0FU2KkedM2h_6yR-aKF-wPnpQT5USKLTqoZiSkHC3GUvt-56_ky-E3Mv5LbMeH9uyd-S1UV6kfJO6znqAcCAF43Yo4ifxTAQ8opoPJTjLIRUC
2. 获取JWT

使用apifox演示,postmanidea-http都可以。
localhost:63070/auth服务的/oauth2/token端点发送Post请求,同时需要携带认证信息。
认证信息可以如图所填的方法,也可以放到Header中,具体做法是将客户端ID和客户端密码用冒号(:)连接成一个字符串,进行Base64编码放入HTTP请求的Authorization头部中,前缀为Basic 。比如
Authorization: Basic bXlDbGllbnRJZDpteUNsaWVudFNlY3JldA==

在这里插入图片描述
在这里插入图片描述
得到JWT
在这里插入图片描述

2. CLIENT_CREDENTIALS(客户端凭证模式)

不需要授权码,直接向localhost:63070/auth服务的/oauth2/token端点发送Post请求,同时需要携带认证信息。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

五、Gateway

至于gateway基础搭建步骤和gateway管理的若干微服务本文不做指导。

相较于auth模块(也就是Authorization Server),gateway的角色是Resource Server
在这里插入图片描述

1. 引入依赖

		<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-oauth2-resource-server</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency>

2. 添加白名单文件

resource下添加security-whitelist.properties文件。
写入以下内容

/auth/**=????
/content/open/**=??????????
/media/open/**=??????????

3. 全局过滤器

在全局过滤器中,加载白名单,然后对请求进行判断。

@Component
@Slf4j
public class GatewayAuthFilter implements GlobalFilter, Ordered {//白名单private static List<String> whitelist = null;static {//加载白名单try (InputStream resourceAsStream = GatewayAuthFilter.class.getResourceAsStream("/security-whitelist.properties");) {Properties properties = new Properties();properties.load(resourceAsStream);Set<String> strings = properties.stringPropertyNames();whitelist= new ArrayList<>(strings);} catch (Exception e) {log.error("加载/security-whitelist.properties出错:{}",e.getMessage());e.printStackTrace();}}@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {String requestUrl = exchange.getRequest().getPath().value();log.info("请求={}",requestUrl);AntPathMatcher pathMatcher = new AntPathMatcher();//白名单放行for (String url : whitelist) {if (pathMatcher.match(url, requestUrl)) {return chain.filter(exchange);}}}private Mono<Void> buildReturnMono(String error, ServerWebExchange exchange) {ServerHttpResponse response = exchange.getResponse();String jsonString = JSON.toJSONString(new RestErrorResponse(error));byte[] bits = jsonString.getBytes(StandardCharsets.UTF_8);DataBuffer buffer = response.bufferFactory().wrap(bits);response.setStatusCode(HttpStatus.UNAUTHORIZED);response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");return response.writeWith(Mono.just(buffer));}@Overridepublic int getOrder() {return 0;}
}

4. 获取远程JWKS

yml配置中添加jwk-set-uri属性。

spring:security:oauth2:resourceserver:jwt:jwk-set-uri: http://localhost:63070/auth/oauth2/jwks

新建配置类,自动注入JwtDecoder

@Configuration
public class JwtDecoderConfig {@Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}")String jwkSetUri;@Beanpublic JwtDecoder jwtDecoderLocal() {return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();}
}

5. 校验JWT

在全局过滤器中补全逻辑。

@Component
@Slf4j
public class GatewayAuthFilter implements GlobalFilter, Ordered {@Lazy@Autowiredprivate JwtDecoder jwtDecoderLocal;@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {String requestUrl = exchange.getRequest().getPath().value();log.info("请求={}",requestUrl);AntPathMatcher pathMatcher = new AntPathMatcher();//白名单放行for (String url : whitelist) {if (pathMatcher.match(url, requestUrl)) {return chain.filter(exchange);}}//检查token是否存在String token = getToken(exchange);log.info("token={}",token);if (StringUtils.isBlank(token)) {return buildReturnMono("没有携带Token,没有认证",exchange);}
//        return chain.filter(exchange);try {Jwt jwt = jwtDecoderLocal.decode(token);// 如果没有抛出异常,则表示JWT有效// 此时,您可以根据需要进一步检查JWT的声明log.info("token有效期至:{}", formatInstantTime(jwt.getExpiresAt()));return chain.filter(exchange);} catch (JwtValidationException e) {log.info("token验证失败:{}",e.getMessage());return buildReturnMono("认证token无效",exchange);}}/*** 从请求头Authorization中获取token*/private String getToken(ServerWebExchange exchange) {String tokenStr = exchange.getRequest().getHeaders().getFirst("Authorization");if (StringUtils.isBlank(tokenStr)) {return null;}String token = tokenStr.split(" ")[1];if (StringUtils.isBlank(token)) {return null;}return token;}/*** 格式化Instant时间** @param expiresAt 在到期* @return {@link String}*/public String formatInstantTime(Instant expiresAt) {// 将Instant转换为系统默认时区的LocalDateTimeLocalDateTime dateTime = LocalDateTime.ofInstant(expiresAt, ZoneId.systemDefault());// 定义日期时间的格式DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");// 格式化日期时间并打印return dateTime.format(formatter);}}

6. 测试(如何携带JWT)

携带一个正确的JWTgateway发送请求。
JWT写到HeaderAuthorization字段中,添加前缀Bearer(用空格隔开),向gateway微服务所在地址发送请求。
在这里插入图片描述
gateway日志输出。
在这里插入图片描述

六、后记

颁发JWT都归一个认证服务器管理,校验JWT都归Gateway管理,至于授权,则由各个微服务自己定义。耦合性低、性能较好。

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

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

相关文章

The Back-And-Forth Method (BFM) for Wasserstein Gradient Flows windows安装

本文记录了BFM算法代码在windows上的安装过程。 算法原网站&#xff1a;https://wasserstein-gradient-flows.netlify.app/ github&#xff1a;https://github.com/wonjunee/wgfBFMcodes 文章目录 FFTWwgfBFMcodesMATLABpython注 FFTW 官网/下载路径&#xff1a;https://ww…

Flink从入门到实践(一):Flink入门、Flink部署

文章目录 系列文章索引一、快速上手1、导包2、求词频demo&#xff08;1&#xff09;要读取的数据&#xff08;2&#xff09;demo1&#xff1a;批处理&#xff08;离线处理&#xff09;&#xff08;3&#xff09;demo2 - lambda优化&#xff1a;批处理&#xff08;离线处理&…

python视频播放列表信息库之m3u8使用详解

m3u8库是什么&#xff1f; m3u8是一个用于解析和操作M3U8文件的Python库。M3U8文件&#xff0c;是指使用UTF-8编码格式的M3U文件&#xff0c;它们通常用于播放列表文件&#xff0c;尤其是在HTTP Live Streaming&#xff08;HLS&#xff09;中。简单来说&#xff0c;m3u8库能帮…

HiveSQL——条件判断语句嵌套windows子句的应用

注&#xff1a;参考文章&#xff1a; SQL条件判断语句嵌套window子句的应用【易错点】--HiveSql面试题25_sql剁成嵌套判断-CSDN博客文章浏览阅读920次&#xff0c;点赞4次&#xff0c;收藏4次。0 需求分析需求&#xff1a;表如下user_idgood_namegoods_typerk1hadoop1011hive1…

假期刷题打卡--Day27

1、MT1217矩阵乘法 输入3X4整型矩阵A和4X3的整型矩阵B&#xff0c;计算A*B&#xff0c;放到矩阵C里面&#xff0c;输出矩阵C。 格式 输入格式&#xff1a; 分两行输入两个矩阵&#xff0c;空格分隔。 输出格式&#xff1a; 按矩阵形式输出&#xff0c;整型&#xff0c;每…

[算法前沿]--059-大语言模型Fine-tuning踩坑经验之谈

前言 由于 ChatGPT 和 GPT4 兴起,如何让人人都用上这种大模型,是目前 AI 领域最活跃的事情。当下开源的 LLM(Large language model)非常多,可谓是百模大战。面对诸多开源本地模型,根据自己的需求,选择适合自己的基座模型和参数量很重要。选择完后需要对训练数据进行预处…

基于微信小程序的校园二手交易平台

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

双指针算法 判断子序列

判断子序列 1.C代码实现: #include<iostream> using namespace std; const int N100002; int a[N],b[N];int main(){int n,m;cin>>n>>m; // 输入n和m的值for(int i0;i<n;i){cin>>a[i]; // 输入数组a的元素}for(int j0;j<m;j){cin>>b[j]; …

MYSQL存储过程(含入参、出参)

1、创建库存表语句 -- eladmin.t_stock definitionCREATE TABLE t_stock (id bigint(20) NOT NULL AUTO_INCREMENT,quantity bigint(20) NOT NULL,PRIMARY KEY (id) ) ENGINEInnoDB AUTO_INCREMENT4101 DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_bin; id为主键&#xff0c;便于…

CTFshow web(php命令执行 45-49)

基础知识&#xff1a; 1.绕过cat使用&#xff1a; tac more less head tac tail nl od(二进制查看) vi vim sort uniq rev 2.绕过空格用&#xff1a; %09 <> ${IFS} $IFS$ {cat,fl*} %20 注&#xff1a; %09 ##&#xff08;Tab&#xff09; %20 ##&#xff08;spa…

项目02《游戏-12-开发》Unity3D

基于 项目02《游戏-11-开发》Unity3D &#xff0c; 任务&#xff1a;实现场景怪物自动巡航 &#xff0c; 首先在场景中创建小球命名为路径点WayPoint0&#xff0c; 取消小球的碰撞器Collider&#xff0c; 再复制两个改名为WayPoint1 和 WayPoint2 &#xff0c; 在…

Spring Boot 笔记 003 Bean注册

使用Idea导入第三方jar包 在porn.xml种添加的第三方jar包依赖&#xff0c;并刷新 可以在启动类中尝试调用 以上放到启动类中&#xff0c;不推荐&#xff0c;建议创建一个专门定义的类 package com.geji.config;import cn.itcast.pojo.Country; import cn.itcast.pojo.Province;…

树与二叉树---数据结构

树作为一种逻辑结构&#xff0c;同时也是一种分层结构&#xff0c;具有以下两个特点&#xff1a; 1&#xff09;树的根结点没有前驱&#xff0c;除根结点外的所有结点有 且只有一个前驱。 2&#xff09;树中所有结点可以有零个或多个后继。 树结点数据结构 满二叉树和完全二…

Spinnaker多云持续交付平台: 部署Minio存储服务

目录 一、实验 1.环境 2.K8S storage节点部署NFS 3.K8S 动态创建PV 4.K8S master节点部署HELM3 4.K8S master节点部署Minio存储服务&#xff08;第一种方式安装&#xff09; 5.Minio客户端安装MC命令 6.K8S master节点使用Docker 部署Minio存储服务&#xff08;第二种方…

云游戏发行需要哪些条件

云游戏是一种创新性的游戏服务模式&#xff0c;将游戏运算和渲染等处理任务移至云端服务器&#xff0c;通过互联网实时传输画面和操作指令&#xff0c;使玩家能够在低端终端设备上也能流畅玩游戏。要做云游戏发行&#xff0c;需要考虑一系列条件&#xff0c;包括技术、基础设施…

幻兽帕鲁服务器创建私服教程(新版教程更简单)

幻兽帕鲁官方服务器不稳定&#xff1f;自己搭建幻兽帕鲁服务器&#xff0c;低延迟、稳定不卡&#xff0c;目前阿里云和腾讯云均推出幻兽帕鲁专用服务器&#xff0c;腾讯云直接提供幻兽帕鲁镜像系统&#xff0c;阿里云通过计算巢服务&#xff0c;均可以一键部署&#xff0c;鼠标…

表单标记(html)

前言 发现input的type属性还是有挺多的&#xff0c;这里把一些常用的总结一下。 HTML 输入类型 (w3school.com.cn)https://www.w3school.com.cn/html/html_form_input_types.asp text-文本 文本输入,如果文字太长&#xff0c;超出的部分就不会显示。 定义供文本输入的单行…

项目学习记录

项目开发 创建项目环境配置关联git新增模块项目启动打印地址日志使用httpclient进行idea内部控制台测试使用AOP拦截器打印日志 创建项目 创建一个空项目&#xff0c;并勾选下面选项 然后进入pom.xml中修改项目配置 根据这个链接选则&#xff0c;修改项目的支持版本 链接&#…

《MySQL 简易速速上手小册》第5章:高可用性和灾难恢复(2024 最新版)

文章目录 5.1 构建高可用性 MySQL 解决方案5.1.1 基础知识5.1.2 重点案例&#xff1a;使用 Python 构建高可用性的电子商务平台数据库5.1.3 拓展案例 5.2 数据备份策略和工具5.2.1 基础知识5.2.2 重点案例&#xff1a;使用 Python 实现 MySQL 定期备份5.2.3 拓展案例 5.3 灾难恢…

Mybatis是否支持延迟加载?

前言 随着互联网应用的不断发展&#xff0c;数据库访问成为了应用开发中的一个重要环节。在这个背景下&#xff0c;MyBatis作为一种优秀的持久层框架&#xff0c;提供了灵活的SQL映射配置和强大的功能&#xff0c;为开发者提供了便捷的数据库访问解决方案。本文将深入探讨MyBat…