Spring Security 6.x 迁移到 7.0 的完整步骤

Spring Security 6.x 迁移到 7.0 的完整步骤

重要提示: Spring Security 7.0 包含重大更改。在升级到 7.0 之前,请使用 Spring Security 6.5 作为准备版本。Spring Security 6.5 允许你逐步迁移单个功能,使升级风险降至最低。

概述

本指南涵盖了从 Spring Security 6.x 迁移到 7.0 的完整步骤。Spring Security 7.0 代表了框架的重大现代化,移除了长期弃用的 API 并提高了整体一致性。

主要迁移主题

  • 授权API: 从 AccessDecisionManager 完全迁移到 AuthorizationManager
  • 配置 : 移除 and() 方法,XML schema 版本要求
  • 请求匹配: 基于 PathPatternRequestMatcher 的标准化
  • Jackson: 迁移到 Jackson 3
  • OAuth 2.0: 默认启用 PKCE,移除密码授权
  • SAML 2.0: 改进的规范合规性,要求 OpenSAML 5
  • 模块架构: 集成以前独立的项目

版本要求

在迁移到 Spring Security 7.0 之前,请确保你的项目满足以下版本要求:

  • Spring Framework: 6.x (最低要求)
  • Spring Boot: 4.0.x (如果使用 Spring Boot)
  • Java: 17 或更高版本
  • Jakarta EE: Jakarta EE 9+ 命名空间 (jakarta.* 而不是 javax.*)

版本兼容性矩阵

Spring Security Spring Boot Java
7.0.x 4.0.x 17+
6.5.x 3.2.x 17+
6.4.x 3.1.x 17+

版本兼容性在运行时由 SpringSecurityCoreVersion 强制执行,它会验证是否使用了正确的 Spring Framework 版本。

依赖更新

Maven 配置

pom.xml 中更新以下依赖:

xml 复制代码
<properties>
    <spring-security.version>7.0.0</spring-security.version>
</properties>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-bom</artifactId>
            <version>${spring-security.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

Gradle 配置

build.gradle 中更新依赖:

gradle 复制代码
ext {
    springSecurityVersion = '7.0.0'
}

dependencies {
    implementation platform("org.springframework.security:spring-security-bom:${springSecurityVersion}")
    implementation 'org.springframework.security:spring-security-core'
    implementation 'org.springframework.security:spring-security-web'
    implementation 'org.springframework.security:spring-security-config'
}

核心API变更

AuthorizationManager#check 移除

AuthorizationManager 接口中的 check 方法已被移除。使用 authorize 方法替代。

迁移前 (6.x):

java 复制代码
AuthorizationDecision decision = authorizationManager.check(
    authentication, object);

迁移后 (7.0):

java 复制代码
AuthorizationDecision decision = authorizationManager.authorize(
    authentication, object);

authorize 方法在 Spring Security 5.5 中引入,现在是唯一支持的方法。

Access Decision API 迁移

如果你的应用程序使用传统的访问决策 API (AccessDecisionManager, AccessDecisionVoter),必须添加 spring-security-access 模块:

Maven:

xml 复制代码
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-access</artifactId>
</dependency>

Gradle:

gradle 复制代码
implementation 'org.springframework.security:spring-security-access'

这些 API 自 Spring Security 5.5 以来已被弃用,转而使用 AuthorizationManager。

Authentication.Builder

Spring Security 7.0 添加了 Authentication.Builder 用于创建和修改 Authentication 实例。虽然这是附加功能,不需要迁移,但它为自定义认证逻辑提供了更简洁的 API。

配置变更

XML 命名空间配置

XML 命名空间必须引用 Spring Security 7.0 schema 或更高版本。不支持早期的 schema。

更新你的 schema 位置:

迁移前 (6.x 或更早):

xml 复制代码
<beans xmlns:security="http://www.springframework.org/schema/security"
       xsi:schemaLocation="
           http://www.springframework.org/schema/security
           https://www.springframework.org/schema/security/spring-security-6.5.xsd">

迁移后 (7.0) - 选项1:使用特定版本

xml 复制代码
<beans xmlns:security="http://www.springframework.org/schema/security"
       xsi:schemaLocation="
           http://www.springframework.org/schema/security
           https://www.springframework.org/schema/security/spring-security-7.0.xsd">

迁移后 (7.0) - 选项2:使用无版本(推荐)

xml 复制代码
<beans xmlns:security="http://www.springframework.org/schema/security"
       xsi:schemaLocation="
           http://www.springframework.org/schema/security
           https://www.springframework.org/schema/security/spring-security.xsd">

SecurityNamespaceHandler 在解析时强制执行此要求。

Java 配置:移除 and() 方法

HttpSecurity DSL 中的 and() 方法已被移除。使用基于 lambda 的配置替代。

迁移前 (6.x):

java 复制代码
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(authz -> authz
            .requestMatchers("/public/**").permitAll()
            .anyRequest().authenticated()
        )
        .and()
        .formLogin(form -> form
            .loginPage("/login")
            .permitAll()
        )
        .and()
        .logout(logout -> logout.permitAll());
    return http.build();
}

迁移后 (7.0):

java 复制代码
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(authz -> authz
            .requestMatchers("/public/**").permitAll()
            .anyRequest().authenticated()
        )
        .formLogin(form -> form
            .loginPage("/login")
            .permitAll()
        )
        .logout(logout -> logout.permitAll());
    return http.build();
}

授权配置

authorizeRequests() 方法已被移除。使用 authorizeHttpRequests() 替代。

迁移:

java 复制代码
// 迁移前 (6.x)
http.authorizeRequests(authz -> authz
    .requestMatchers("/admin/**").hasRole("ADMIN")
    .anyRequest().authenticated()
);

// 迁移后 (7.0)
http.authorizeHttpRequests(authz -> authz
    .requestMatchers("/admin/**").hasRole("ADMIN")
    .anyRequest().authenticated()
);

authorizeHttpRequests() 使用 AuthorizationManager API 而不是传统的 AccessDecisionManager

请求匹配器

MvcRequestMatcherAntPathRequestMatcher 已被移除。使用 PathPatternRequestMatcher 替代。

迁移:

java 复制代码
// 迁移前 (6.x)
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

http.authorizeHttpRequests(authz -> authz
    .requestMatchers(new AntPathRequestMatcher("/api/**")).hasRole("API")
);

// 迁移后 (7.0)
http.authorizeHttpRequests(authz -> authz
    .requestMatchers("/api/**").hasRole("API")  // 默认使用 PathPattern
);

默认的请求匹配策略现在基于 Spring 的 PathPattern,它比 Ant 样式的模式提供更好的性能和更多功能。

XML 配置:intercept-methods

intercept-methods 元素现在默认使用 AuthorizationManager 而不是 AccessDecisionManager

自定义配置的迁移:

迁移前 (6.x 行为) - 选择使用传统方式

xml 复制代码
<bean id="target" class="com.example.BusinessBean">
    <intercept-methods use-authorization-manager="false">
        <protect method="get*" access="ROLE_USER"/>
    </intercept-methods>
</bean>

迁移后 (7.0 默认) - 使用 AuthorizationManager

xml 复制代码
<bean id="target" class="com.example.BusinessBean">
    <intercept-methods>
        <protect method="get*" access="hasRole('ROLE_USER')"/>
    </intercept-methods>
</bean>

如果需要继续使用传统 API,确保包含 spring-security-access 并设置 use-authorization-manager="false"

Jackson 迁移 (Jackson 2 → Jackson 3)

Spring Security 7.0 默认使用 Jackson 3。Jackson 2 支持已弃用。

迁移步骤:

更新 ObjectMapper 配置:

迁移前 (Jackson 2):

java 复制代码
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.jackson2.SecurityJackson2Modules;

ObjectMapper mapper = new ObjectMapper();
mapper.registerModules(SecurityJackson2Modules.getModules(
    getClass().getClassLoader()));

迁移后 (Jackson 3):

java 复制代码
import tools.jackson.databind.json.JsonMapper;
import org.springframework.security.jackson3.SecurityJacksonModules;

JsonMapper.Builder builder = JsonMapper.builder();
SecurityJacksonModules.getModules(getClass().getClassLoader())
    .forEach(builder::addModule);
JsonMapper mapper = builder.build();

替换单个模块:

迁移前 (Jackson 2):

java 复制代码
import org.springframework.security.jackson2.CoreJackson2Module;
mapper.registerModule(new CoreJackson2Module());

迁移后 (Jackson 3):

java 复制代码
import org.springframework.security.jackson3.SecurityJacksonModules;
// 使用自动模块检测替代
SecurityJacksonModules.getModules(getClass().getClassLoader())
    .forEach(builder::addModule);

授权服务器配置

如果使用 spring-security-oauth2-authorization-server,它现在默认使用 Jackson 3。继续使用 Jackson 2(不推荐):

Maven:

xml 复制代码
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-authorization-server</artifactId>
    <exclusions>
        <exclusion>
            <groupId>tools.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
</dependency>

OAuth 2.0 变更

JWT 类型验证

Spring Security 7.0 默认验证 typ 头部。如果你在 6.5 中禁用了此功能,可以移除这些配置。

Servlet 实现:

java 复制代码
// 6.5 准备 (7.0中可以移除)
@Bean
JwtDecoder jwtDecoder() {
    NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(location)
        .validateTypes(false) // ← 移除这行
        .build();
    jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithValidators(
        new JwtIssuerValidator(location), 
        JwtTypeValidator.jwt())); // ← 移除显式 JwtTypeValidator
    return jwtDecoder;
}

// 7.0 (简化)
@Bean
JwtDecoder jwtDecoder() {
    NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(location)
        .build(); // validateTypes 默认为 false,包含 JwtTypeValidator
    jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(location));
    return jwtDecoder;
}

Reactive 实现:

java 复制代码
// 6.5 准备 (7.0中可以移除)
@Bean
NimbusReactiveJwtDecoder jwtDecoder() {
    NimbusReactiveJwtDecoder jwtDecoder = 
        NimbusReactiveJwtDecoder.withIssuerLocation(location)
            .validateTypes(false) // ← 移除这行
            .build();
    jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithValidators(
        new JwtIssuerValidator(location), 
        JwtTypeValidator.jwt())); // ← 移除显式 JwtTypeValidator
    return jwtDecoder;
}

// 7.0 (简化)
@Bean
NimbusReactiveJwtDecoder jwtDecoder() {
    NimbusReactiveJwtDecoder jwtDecoder = 
        NimbusReactiveJwtDecoder.withIssuerLocation(location)
            .build(); // validateTypes 默认为 false,包含 JwtTypeValidator
    jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(location));
    return jwtDecoder;
}

BearerTokenAuthenticationFilter 配置

BearerTokenAuthenticationFilter 上的 setBearerTokenResolver()setAuthenticationDetailsSource() 方法已弃用。在 BearerTokenAuthenticationConverter 上配置这些。

迁移:

java 复制代码
// 迁移前 (6.x)
BearerTokenAuthenticationFilter filter = 
    new BearerTokenAuthenticationFilter(authenticationManager);
filter.setBearerTokenResolver(myBearerTokenResolver);
filter.setAuthenticationDetailsSource(myAuthenticationDetailsSource);

// 迁移后 (7.0)
BearerTokenAuthenticationConverter authenticationConverter = 
    new BearerTokenAuthenticationConverter();
authenticationConverter.setBearerTokenResolver(myBearerTokenResolver);
authenticationConverter.setAuthenticationDetailsSource(myAuthenticationDetailsSource);

BearerTokenAuthenticationFilter filter = 
    new BearerTokenAuthenticationFilter(authenticationManager, authenticationConverter);

移除密码授权

OAuth 2.0 密码授权已被移除,因为它不再被 OAuth 2.0 规范推荐。适当地迁移到授权码流与 PKCE 或客户端凭证流。

PKCE 默认启用

使用 spring-security-oauth2-authorization-server 时,PKCE(授权码交换证明密钥)现在对所有授权码流默认启用。这提高了安全性,无需配置更改。

SAML 2.0 变更

LogoutResponse 行为

在 Spring Security 7.0 中,当 <saml2:LogoutRequest> 验证失败时,Spring Security 现在返回错误 <saml2:LogoutResponse> 而不是返回 HTTP 401。这符合 SAML 2.0 规范要求。

选择退出(如果需要):

java 复制代码
@Bean
Saml2LogoutResponseResolver logoutResponseResolver(
        RelyingPartyRegistrationRepository registrations) {
    OpenSaml5LogoutResponseResolver delegate = 
        new OpenSaml5LogoutResponseResolver(registrations);
    
    return new Saml2LogoutResponseResolver() {
        @Override
        public Saml2LogoutResponse resolve(HttpServletRequest request, 
                                          Authentication authentication) {
            return delegate.resolve(request, authentication);
        }

        @Override
        public Saml2LogoutResponse resolve(HttpServletRequest request, 
                                          Authentication authentication, 
                                          Saml2AuthenticationException error) {
            return null; // 恢复到 6.x 行为
        }
    };
}

AssertingPartyDetails 移除

AssertingPartyDetails 类已被移除。使用 AssertingPartyMetadata 接口替代。

迁移通常涉及更新方法签名:

java 复制代码
// 迁移前 (6.x)
public void processAssertion(AssertingPartyDetails details) {
    String entityId = details.getEntityId();
    // ...
}

// 迁移后 (7.0)
public void processAssertion(AssertingPartyMetadata metadata) {
    String entityId = metadata.getEntityId();
    // ...
}

Saml2AuthenticatedPrincipal 弃用

Spring Security 7.0 将断言详情与主体分离。凭证现在实现 Saml2ResponseAssertionAccessor 而不是使用 Saml2AuthenticatedPrincipal

默认行为使用 Saml2AssertionAuthentication,它自动处理此功能。

自定义实现可能需要调整:

java 复制代码
// 迁移前 (6.x)
public Authentication authenticate(Authentication authentication) {
    // ... 认证逻辑
    Saml2AuthenticatedPrincipal principal = createPrincipal(response);
    return new Saml2Authentication(principal, saml2Response, authorities);
}

// 迁移后 (7.0 - 如果使用默认)
// ResponseAuthenticationConverter 现在返回 Saml2AssertionAuthentication
// 如果使用默认设置则无需更改

// 迁移后 (7.0 - 如果自定义)
@Bean
OpenSaml5AuthenticationProvider authenticationProvider() {
    OpenSaml5AuthenticationProvider provider = 
        new OpenSaml5AuthenticationProvider();
    ResponseAuthenticationConverter defaults = 
        new ResponseAuthenticationConverter();
    
    // 包装以返回 Saml2AssertionAuthentication
    provider.setResponseAuthenticationConverter(
        defaults.andThen(authentication -> 
            new Saml2AssertionAuthentication(
                authentication.getPrincipal(),
                authentication.getSaml2Response(),
                authentication.getAuthorities()
            )
        )
    );
    return provider;
}

GET 请求支持移除

Saml2AuthenticationTokenConverterOpenSaml5AuthenticationTokenConverter 默认不再处理 GET 请求,因为 SAML 2.0 规范不支持通过 GET 的 <saml2:Response>

这是推荐的行为。如果必须支持 GET 请求(不推荐),请显式启用:

java 复制代码
@Bean
OpenSaml5AuthenticationTokenConverter authenticationConverter(
        RelyingPartyRegistrationRepository registrations) {
    OpenSaml5AuthenticationTokenConverter converter = 
        new OpenSaml5AuthenticationTokenConverter(registrations);
    converter.setShouldConvertGetRequests(true); // 不推荐
    return converter;
}

需要 OpenSAML 5

OpenSAML 4 支持已移除。迁移到 OpenSAML 5。

Maven:

xml 复制代码
<dependency>
    <groupId>org.opensaml</groupId>
    <artifactId>opensaml-core</artifactId>
    <version>5.0.0</version>
</dependency>
<dependency>
    <groupId>org.opensaml</groupId>
    <artifactId>opensaml-saml-api</artifactId>
    <version>5.0.0</version>
</dependency>
<dependency>
    <groupId>org.opensaml</groupId>
    <artifactId>opensaml-saml-impl</artifactId>
    <version>5.0.0</version>
</dependency>

Web 安全变更

会话并发控制

SessionLimit 函数式接口提供更灵活的会话控制策略。虽然基于整数的 API 仍然支持,但函数式方法允许按用户动态会话限制。

java 复制代码
// 简单方法(仍受支持)
http.sessionManagement(session -> session
    .sessionConcurrency(concurrency -> concurrency
        .maximumSessions(1)
        .maxSessionsPreventsLogin(true)
    )
);

// 函数式方法(7.0特性)
http.sessionManagement(session -> session
    .sessionConcurrency(concurrency -> concurrency
        .maximumSessions(SessionLimit.of(authentication -> {
            // 基于用户角色的动态限制
            if (hasRole(authentication, "ADMIN")) {
                return -1; // 管理员无限制
            }
            return 1; // 普通用户限制
        }))
        .maxSessionsPreventsLogin(true)
    )
);

默认登录页面多因素支持

默认登录页面现在支持基于 factor.typefactor.reason 请求参数显示多因素认证提示。这不需要迁移但提供了增强的功能。

模块特定变更

Kerberos 集成

Spring Security Kerberos 扩展现在是 Spring Security 主项目的一部分。将导入从扩展项目更新到主项目。

java 复制代码
// 迁移前 (使用扩展)
import org.springframework.security.extensions.kerberos.*;

// 迁移后 (7.0)
import org.springframework.security.kerberos.*;

Spring 授权服务器

Spring 授权服务器现在是 Spring Security 的一部分。相应更新依赖:

Maven:

xml 复制代码
<!-- 迁移前 (独立项目) -->
<dependency>
    <groupId>org.springframework.security.experimental</groupId>
    <artifactId>spring-authorization-server</artifactId>
</dependency>

<!-- 迁移后 (7.0) -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-authorization-server</artifactId>
</dependency>

LDAP 变更

ApacheDsContainer 和相关 Apache DS 支持已移除。使用 UnboundID 进行嵌入式 LDAP 服务器。

迁移:

xml 复制代码
<!-- 迁移前 (6.x - Apache DS) -->
<ldap-server mode="apacheds" />

<!-- 迁移后 (7.0 - UnboundID) -->
<ldap-server mode="unboundid" />

或在 Java 配置中:

java 复制代码
// 迁移后 (7.0)
@Bean
EmbeddedLdapServerContextSourceFactoryBean contextSourceFactoryBean() {
    EmbeddedLdapServerContextSourceFactoryBean factory = 
        new EmbeddedLdapServerContextSourceFactoryBean();
    factory.setPort(0); // 随机端口
    return factory;
}

测试更新

MockMvc 匹配器

Spring Security 7.0 添加了新的测试工具用于验证权限:

java 复制代码
// 7.0 新增
mockMvc.perform(get("/api/resource")
    .with(user("testuser")))
    .andExpect(status().isOk())
    .andExpect(withAuthorities("ROLE_USER", "ROLE_ADMIN"));

验证和测试

完成迁移步骤后,执行以下验证:

检查清单

  • ✅ 应用程序启动时 XML 配置没有 BeanDefinitionParsingException
  • ✅ 所有安全过滤器正确注册(检查过滤器链的日志)
  • ✅ 所有配置的机制的身份验证都正常工作
  • ✅ 授权规则正确执行
  • ✅ 会话管理按预期工作
  • ✅ CSRF 保护正常工作(如果启用)
  • ✅ OAuth 2.0 流成功完成(如果适用)
  • ✅ SAML 2.0 认证和注销工作(如果适用)
  • ✅ 自定义 AuthorizationManager 实现正常工作
  • ✅ 方法安全注解正确执行
  • ✅ 集成测试通过
  • ✅ 没有已移除 API 的弃用警告

常见问题

问题 解决方案
XML schema 的 BeanDefinitionParsingException 将 schema 位置更新为 spring-security-7.0.xsd 或 spring-security.xsd
Access Decision API 的 ClassNotFoundException 添加 spring-security-access 依赖
and() 方法的编译错误 移除 and() 调用并使用 lambda DSL
Jackson 序列化失败 完成 Jackson 2 到 Jackson 3 的迁移
SAML 注销链中断 验证 Saml2LogoutResponseResolver 返回错误响应
请求匹配器错误 将 AntPathRequestMatcher/MvcRequestMatcher 替换为 PathPatternRequestMatcher

配置实体引用

关键类和接口

组件 位置 目的
SecurityNamespaceHandler config/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java 解析 XML 安全命名空间,强制执行版本要求
SpringSecurityCoreVersion core/src/main/java/org/springframework/security/core/SpringSecurityCoreVersion.java 版本检查和兼容性验证
PathPatternRequestMatcher web/src/main/java/org/springframework/security/web/util/matcher/ 替换 Ant/MVC 匹配器的默认请求匹配器
AuthorizationManager org.springframework.security.authorization.AuthorizationManager 核心授权 API
BearerTokenAuthenticationConverter org.springframework.security.oauth2.server.resource.web.authentication/BearerTokenAuthenticationConverter OAuth 2.0 承载令牌处理配置
Saml2ResponseAssertionAccessor org.springframework.security.saml2.provider.service.authentication/Saml2ResponseAssertionAccessor SAML 断言访问接口
SessionLimit web/src/main/java/org/springframework/security/web/authentication/session/SessionLimit.java 会话限制的函数式接口
AssertingPartyMetadata org.springframework.security.saml2.provider.service.registration/AssertingPartyMetadata SAML 断言方配置接口

总结

Spring Security 7.0 代表了框架的重大现代化,移除了长期弃用的 API 并提高了整体一致性。关键迁移主题包括:

  • 授权API : 从 AccessDecisionManagerAuthorizationManager 的完全转换
  • 配置 : 移除 and() 方法,XML schema 版本要求
  • 请求匹配 : 基于 PathPatternRequestMatcher 的标准化
  • Jackson: 迁移到 Jackson 3
  • OAuth 2.0: 默认启用 PKCE 的增强安全性,移除密码授权
  • SAML 2.0: 改进的规范合规性,要求 OpenSAML 5
  • 模块架构: 集成以前独立的项目

通过遵循本指南中的步骤并使用 Spring Security 6.5 作为准备版本,你可以自信地迁移到 7.0。对于本指南未涵盖的应用程序特定迁移场景,请咨询详细迁移页面。


相关资源:

相关推荐
JIngJaneIL2 小时前
基于java+ vue农产投入线上管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot
东东的脑洞2 小时前
【面试突击二】JAVA基础知识-volatile、synchronized与ReentrantLock深度对比
java·面试
川贝枇杷膏cbppg2 小时前
Redis 的 AOF
java·数据库·redis
⑩-3 小时前
SpringCloud-Nacos 配置中心实战
后端·spring·spring cloud
吃喝不愁霸王餐APP开发者3 小时前
Java后端系统对接第三方外卖API时的幂等性设计与重试策略实践
java·开发语言
TG:@yunlaoda360 云老大3 小时前
华为云国际站代理商的CBR主要有什么作用呢?
java·网络·华为云
速易达网络4 小时前
基于Java TCP 聊天室
java·开发语言·tcp/ip
沿着路走到底4 小时前
JS事件循环
java·前端·javascript
爱笑的眼睛114 小时前
超越 `cross_val_score`:深度解析Scikit-learn交叉验证API的架构、技巧与陷阱
java·人工智能·python·ai