【springsecurity oauth2授权中心】简单案例跑通流程

news2025/7/15 4:04:41

项目被拆分开,需要一个授权中心使得每个项目都去授权中心登录获取用户权限。而单一项目里权限使用的是spring-security来控制的,每个controller方法上都有 @PreAuthorize("hasAuthority('hello')") 注解来控制权限,想以最小的改动来实现,就学习了一下 spring-boot-starter-oauth2-authorization-server 发现可以满足我的要求,下面是最简单的一个能跑通的案例。

创建项目

创建一个maven项目,新建两个模块

  • authorization-server
  • resource-server

创建好结构如下图
在这里插入图片描述
根目录下的pom.xml内容

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.example</groupId>
    <artifactId>spring-security-oauth2-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-security-oauth2-demo</name>
    <description>spring-security-oauth2-demo</description>
    <packaging>pom</packaging>

    <modules>
        <module>authorization-server</module>
        <module>resource-server</module>
    </modules>

    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.springframework.boot</groupId>
                            <artifactId>spring-boot-configuration-processor</artifactId>
                        </path>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

授权中心

模块 authorization-server 为授权中心 pom.xml内容

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.example</groupId>
        <artifactId>spring-security-oauth2-demo</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <artifactId>authorization-server</artifactId>
    <name>authorization-server</name>
    <description>authorization-server</description>

    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

application.yml

server:
  port: 9000
  servlet:
    session:
      cookie:
        name: AuthorizationServer

spring:
  security:
    user:
      name: admin
      password: admin

logging:
  level:
    root: INFO
    org.springframework.web: DEBUG
    org.springframework.security: DEBUG
    org.springframework.security.oauth2: DEBUG

创建配置类 AuthorizationServerConfig

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;

import java.time.Duration;
import java.util.Collection;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;

@Configuration
@EnableWebSecurity
public class AuthorizationServerConfig {

    @Bean
    @Order(1)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);

        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
                .oidc(Customizer.withDefaults());

        http.exceptionHandling(exceptions ->
                        exceptions.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login")))
                .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));

        return http.build();
    }

    @Bean
    @Order(2)
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(authorize -> authorize
                        .anyRequest().authenticated()
                )
                .formLogin(Customizer.withDefaults());

        return http.build();
    }

    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails user = User.withUsername("user")
                .password("password")
                .authorities("hello", "user")
                .build();

        return new InMemoryUserDetailsManager(user);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    @Bean
    public RegisteredClientRepository registeredClientRepository() {
        RegisteredClient client = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId("client")
                .clientSecret("secret")
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                .redirectUri("http://localhost:8081/login/oauth2/code/client")
                .scope(OidcScopes.OPENID)
                .scope("user")
                .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
                .tokenSettings(TokenSettings.builder()
                        .accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)
                        .accessTokenTimeToLive(Duration.ofHours(2))
                        .build())
                .build();

        return new InMemoryRegisteredClientRepository(client);
    }

    @Bean
    public AuthorizationServerSettings authorizationServerSettings() {
        return AuthorizationServerSettings.builder()
                .issuer("http://localhost:9000")
                .build();
    }

    @Bean
    public OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() {
        return context -> {
            if (context.getTokenType().getValue().equals("access_token")) {
                Collection<? extends GrantedAuthority> authorities = context.getPrincipal().getAuthorities();
                List<String> authorityNames = authorities.stream()
                        .map(GrantedAuthority::getAuthority)
                        .collect(Collectors.toList());
                context.getClaims().claim("authorities", authorityNames);
            }
        };
    }
}

资源服务器

模块 resource-server 为资源服务器,pom.xml如下

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.example</groupId>
        <artifactId>spring-security-oauth2-demo</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <artifactId>resource-server</artifactId>
    <name>resource-server</name>
    <description>resource-server</description>

    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

application.yml

server:
  port: 8081
  servlet:
    session:
      cookie:
        name: ResourceServer

logging:
  level:
    root: info
    org.springframework.web: debug
    org.springframework.security: debug
    org.springframework.security.oauth2: debug

写三个接口,两个需要不同权限,一个仅需要登录即可访问

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DemoController {

    @GetMapping("/api/hello")
    @PreAuthorize("hasAuthority('hello')")
    public String hello() {
        return "Hello, you have 'hello' authority!";
    }

    @GetMapping("/api/user")
    @PreAuthorize("hasRole('user')")
    public String user() {
        return "Hello, you have 'user' role!";
    }

    @GetMapping("/api/test")
    public String test() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        return "Hello World";
    }
}

添加配置类

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class ResourceServerConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(authorize -> authorize
                        .anyRequest().authenticated()
                )
                .oauth2ResourceServer(oauth2 -> oauth2
                        .jwt(jwt -> jwt
                                .jwkSetUri("http://localhost:9000/oauth2/jwks")
                                .jwtAuthenticationConverter(jwtAuthenticationConverter()) // 使用自定义转换器
                        )
                );
        return http.build();
    }

    @Bean
    public JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
        grantedAuthoritiesConverter.setAuthoritiesClaimName("authorities"); // 指定JWT中权限字段名
        grantedAuthoritiesConverter.setAuthorityPrefix(""); // 去掉默认的"SCOPE_"前缀

        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
        return jwtAuthenticationConverter;
    }

}

测试

先启动授权服务器,再启动资源服务器,然后将参数拼接好在浏览器里直接访问

http://localhost:9000/oauth2/authorize?response_type=code&client_id=client&redirect_uri=http://localhost:8081/login/oauth2/code/client&scope=user

回车后会打开登录页面
在这里插入图片描述
输入授权服务器配置类里配置的用户名和密码进行登录
在这里插入图片描述
用户名和密码验证成功后会跳转到下一个确认授权页面,勾上需要授权的用户信息点击确定
在这里插入图片描述
确定后授权服务器会生成一个code,并通过回调地址传给请求方(我这用的是浏览器,可以在浏览器地址栏里看到)
在这里插入图片描述
拿到code后,打开postman,请求授权服务器的 /oauth2/token 接口用code换access_token
在这里插入图片描述在这里插入图片描述
前面在资源服务器的配置类里将用户登录后的权限信息一块打包进jwt里,所以access_token里现在就有权限信息,可以打开 jwt.io 进行查看
在这里插入图片描述
在postman里请求一个资源服务器的接口 /api/hello
在这里插入图片描述
再请求接口 /api/user 因为用户只有 user权限而没有 user 角色,但/api/user 接口配置的是必须要有 user 角色才能访问,所以响应结果就是403了
在这里插入图片描述

总结

  • 这是一个最简单的使用spring-boot-starter-oauth2-authorization-server实现的授权中心
  • 资源服务器之前如果使用的是springsecurity做的权限控制,几乎不需要修改代码
  • 流程跑通后,后面完善起来就很快了

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

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

相关文章

2025TGCTF Web WP复现

AAA 偷渡阴平 <?php$tgctf2025$_GET[tgctf2025];if(!preg_match("/0|1|[3-9]|\~|\|\|\#|\\$|\%|\^|\&|\*|\&#xff08;|\&#xff09;|\-|\|\|\{|\[|\]|\}|\:|\|\"|\,|\<|\.|\>|\/|\?|\\\\/i", $tgctf2025)){//hint&#xff1a;你可以对着键盘…

「GitHub热榜」AIGC系统源码:AI问答+绘画+PPT+音乐生成一站式

—零门槛搭建私有化AI内容工厂&#xff0c;源码开放商业落地指南 为什么全栈AIGC系统成为企业刚需&#xff1f; 1. 传统方案的致命缺陷 痛点 使用ChatGPTMidjourneyCanva 本全栈方案 工具割裂 需切换5平台 一个系统全搞定 成本 年费50万 一次部署永久免费 数据安全 …

【C++】 —— 笔试刷题day_19

一、小易的升级之路 题目解析 小易现在要打游戏&#xff0c;现在游戏角色的初始能力值为a&#xff0c;我们会遇到n个怪&#xff0c;这些怪物的防御值为b1、b2、b3...&#xff0c;如果我们的能力值要高于或者等于怪物的防御值&#xff0c;那我们的能力值就会加bi&#xff1b;如…

Kubernetes》》K8S》》Pod的健康检查

K8s概念总结 》》》Pod的生命周期阶段 Pod的生命周期可以简单描述&#xff1a;首先Pod被创建&#xff0c;紧接着Pod被调度到Node节点进行部署。 Pod是非常忠诚的&#xff0c;一旦被分配到Node节点后&#xff0c;就不会离开这个Node节点&#xff0c;直到它被删除&#xff0c;删除…

计算机视觉——基于使用 OpenCV 与 Python 实现相机标定畸变校正

概述 相机标定是一种旨在通过确定相机的内参&#xff08;焦距、光学中心、畸变系数&#xff09;和外参&#xff08;相机的位置和方向&#xff09;&#xff0c;提高图像在现实世界中的几何精度的过程。该过程可以纠正相机拍摄的图像中的畸变&#xff0c;使相机能够准确感知现实…

Python作业4 文本词云统计,生成词云

编写程序&#xff0c;统计两会政府工作报告热词频率&#xff0c;并生成词云。 2025两会政府工作报告 import jieba import wordcloud from collections import Counter import re# 读取文件 with open("gov.txt", "r", encoding"gbk") as f:t …

Jenkins 2.492.2 LTS 重置管理员密码

文章目录 1. Jenkins 关闭用户认证2. jenkins 修改密码 如果忘记了 Jenkins 的管理员密码的话&#xff0c;也不用担心&#xff0c;只要你有权限访问 Jenkins 的根目录&#xff0c;就可以轻松地重置密码。 1. Jenkins 关闭用户认证 // 查看 jenkins 家目录&#xff08;使用 doc…

【裁判文书网DES3数据解密】逆向分析

点击翻页&#xff0c;出现请求&#xff0c;可以看到请求参数有个ciphertext密文&#xff0c;响应数据也是密文 打上断点&#xff0c;点击翻页&#xff0c;断住 可以看到postData里面的ciphertext已经生成 往前跟栈&#xff0c;可以发现是var ciphertext cipher(); funct…

花园灌溉问题

#include <bits/stdc.h> using namespace std;// 设置最大行列数&#xff08;题目限制 n, m ≤ 100&#xff09; const int N 104;// 标记某个格子是否已经被水浇灌 bool used[N][N];// 队列&#xff0c;用于 BFS&#xff0c;存储当前水源的位置 queue<pair<int,i…

《AI大模型应知应会100篇》第22篇:系统提示词(System Prompt)设计与优化

第22篇&#xff1a;系统提示词(System Prompt)设计与优化 摘要 在大语言模型&#xff08;LLM&#xff09;应用中&#xff0c;系统提示词&#xff08;System Prompt&#xff09;是控制模型行为的核心工具之一。它不仅定义了模型的身份、角色和行为规范&#xff0c;还直接影响输…

Jsp技术入门指南【六】jsp脚本原理及隐式对象

Jsp技术入门指南【六】jsp脚本原理及隐式对象 前言一、JSP 脚本元素1.1 声明1.2 表达式1.3 脚本标签 二、JSP 的隐式对象是什么三、隐式对象详解outrequestsessionapplicationconfigexception 前言 在之前的博客中&#xff0c;我们已经介绍了JSP的环境搭建、编译文件查找以及生…

Jsp技术入门指南【五】详细讲解jsp结构页面

Jsp技术入门指南【五】详细讲解jsp结构页面 前言一、JSP页面的结构二、JSP页面的部件1. 指令&#xff08;核心控制部件&#xff09;2. 动作&#xff08;页面交互部件&#xff0c;了解即可&#xff09;3. 脚本&#xff08;Java逻辑嵌入部件&#xff09; 三、JSP指令详解1.1 JSP指…

Beyond Compare 30天评估到期 解决方法

Beyond Compare 30天评估到期 解决方法 一、问题二、解决办法2.1 第一步&#xff1a;打开注册表2.2 第二步&#xff1a;删除cacheID 三、效果 一、问题 Beyond Compare提示评估到期&#xff0c;重装也无效&#xff0c;只需简单两步&#xff0c;轻轻松松出困境。 二、解决办法…

Arduino无线体感机器手——问题汇总

文章不介绍具体参数&#xff0c;有需求可去网上搜索。 特别声明&#xff1a;不论年龄&#xff0c;不看学历。既然你对这个领域的东西感兴趣&#xff0c;就应该不断培养自己提出问题、思考问题、探索答案的能力。 提出问题&#xff1a;提出问题时&#xff0c;应说明是哪款产品&a…

学习设计模式《一》——简单工厂

一、基础概念 1.1、接口 简单的说&#xff1a;接口是【用来实现类的行为定义、约束类的行为】&#xff08;即&#xff1a;定义可以做什么&#xff09;&#xff1b;接口可以包含【实例方法】、【属性】、【事件】、【索引器】或这四种成员类型的任意组合。 接口的优点&#xff1…

010数论——算法备赛

数论 模运算 一般求余都是对正整数的操作&#xff0c;如果对负数&#xff0c;不同编程语言结果可能不同。 C/javapythona>m,0<a%m<m-1 a<m,a%ma~5%32~-5%3 -21(-5)%(-3) -2~5%(-3)2-1正数&#xff1a;&#xff08;ab&#xff09;%m((a%m)(b%m))%m~正数&#xff…

NAT、代理服务、内网穿透

NAT、代理服务、内网穿透 1、NAT1.1、NAT过程1.2、NAPT2、内网穿透3、内网打洞3、代理服务器3.1、正向代理3.2、反向代理1、NAT 1.1、NAT过程 之前我们讨论了IPv4协议中IP地址数量不充足的问题。NAT技术是当前解决IP地址不够用的主要手段,是路由器的一个重要功能。 NAT能够将…

【unity实战】Unity动画层级(Animation Layer)的Sync同步和Timing定时参数使用介绍,同步动画层制作角色的受伤状态

文章目录 前言方案一&#xff1a;复制粘贴原有层级的状态机1、实现2、问题 方法二&#xff1a;勾选Sync同步动画层1、简单实现同步2、同步blend tree的问题3、动画状态的播放时长4、下层状态覆盖了上层状态 专栏推荐完结 前言 如何制作角色的受伤状态&#xff1f; 玩家角色在…

Uniapp调用native.js使用经典蓝牙串口通讯方法及问题解决

本人尝试在uniapp环境下开发一款安卓应用&#xff0c;需要与使用经典蓝牙协议的设备进行串口通讯&#xff0c;而uniapp官方给出的蓝牙操作接口目前只支持BLE&#xff08;低功耗蓝牙&#xff09;&#xff0c;用该接口无法正常获取到我想要连接的设备。 通过大量搜索&#xff0c…

C++23 新特性:行拼接前去除空白符 (P2223R2)

文章目录 1\. 什么是行拼接前去除空白符2\. 为什么需要这一特性3\. 示例代码输出结果 4\. 编译器支持5\. 优势与应用场景5.1 提高代码可读性5.2 减少潜在错误5.3 适用于多行字符串 6\. 其他相关特性7\. 总结 C 语言一直在不断进化&#xff0c;以满足现代软件开发的需求。C23 标…