IdentityServer4系列 | 资源密码凭证模式

一、前言

从上一篇关于客户端凭证模式中,我们通过创建一个认证授权访问服务,定义一个API和要访问它的客户端,客户端通过IdentityServer上请求访问令牌,并使用它来控制访问API。其中,我们也注意到了在4.x版本中于之前3.x版本之间存在的差异。

所以在这一篇中,我们将通过多种授权模式中的资源所有者密码凭证授权模式进行说明,主要针对介绍IdentityServer保护API的资源,资源密码凭证授权访问API资源。

二、初识

如果你高度信任某个应用Client,也允许用户把用户名和密码,直接告诉该应用Client。该应用Client就使用你的密码,申请令牌,这种方式称为"密码式"(password)。

这种模式适用于鉴权服务器与资源服务器是高度相互信任的,例如两个服务都是同个团队或者同一公司开发的。

2.1 适用范围

资源所有者密码凭证授权模式,适用于当资源所有者与客户端具有良好信任关系的场景,比如客户端是设备的操作系统或具备高权限的应用。授权服务器在开放此种授权模式时必须格外小心,并且只有在别的模式不可用时才允许这种模式。

这种模式下,应用client可能存了用户密码这不安全性问题,所以才需要高可信的应用。

主要适用于用来做遗留项目升级为oauth2的适配授权使用,当然如果client是自家的应用,也是可以的,同时支持refresh token。

例如,A站点 需要添加了 OAuth 2.0 作为对其现有基础架构的一个授权机制。对于现有的客户端转变为这种授权方案,资源所有者密码凭据授权将是最方便的,因为他们只需使用现有的帐户详细信息(比如用户名和密码)来获取访问令牌。

2.2 密码授权流程:

     +----------+| Resource ||  Owner   ||          |+----------+v|    Resource Owner(A) Password Credentials|v+---------+                                  +---------------+|         |>--(B)---- Resource Owner ------->|               ||         |         Password Credentials     | Authorization || Client  |                                  |     Server    ||         |<--(C)---- Access Token ---------<|               ||         |    (w/ Optional Refresh Token)   |               |+---------+                                  +---------------+

资源所有者密码凭证授权流程描述

(A)资源所有者向客户端提供其用户名和密码。

(B)客户端从授权中请求访问令牌服务器的令牌端点,以获取访问令牌。当发起该请求时,授权服务器需要认证客户端的身份。

(C) 授权服务器验证客户端身份,同时也验证资源所有者的凭据,如果都通过,则签发访问令牌。

2.2.1 过程详解


访问令牌请求

参数是否必须含义
grant_type必需授权类型,值固定为“password”。
username必需用户名
password必需密码
scope可选表示授权范围。

同时将允许其他请求参数client_idclient_secret,或在HTTP Basic auth标头中接受客户端ID和密钥。

验证用户名密码

示例:客户端身份验证两种方式

1、Authorization: Bearer base64(resourcesServer:123) 

2、client_id(客户端标识),client_secret(客户端秘钥),username(用户名),password(密码)。

(用户的操作:输入账号和密码)

A 网站要求用户提供 B 网站的用户名和密码。拿到以后,A 就直接向 B 请求令牌。

POST /oauth/token HTTP/1.1
Host: authorization-server.comgrant_type=password
&username=user@example.com
&password=1234luggage
&client_id=xxxxxxxxxx
&client_secret=xxxxxxxxxx

上面URL中,grant_type参数是授权方式,这里的password是“密码式”,username和password是B的用户名和密码。

2.2.2 访问令牌响应

第二步,B 网站验证身份通过后,直接给出令牌。注意,这时不需要跳转,而是把令牌放在 JSON 数据里面,作为 HTTP 回应,A 因此拿到令牌。

响应给用户令牌信息(access_token),如下所示

{"access_token": "访问令牌","token_type": "Bearer","expires_in": 4200,"scope": "server","refresh_token": "刷新令牌"
}

用户使用这个令牌访问资源服务器,当令牌失效时使用刷新令牌去换取新的令牌。

这种方式需要用户给出自己的用户名/密码,显然风险很大,因此只适用于其他授权方式都无法采用的情况,而且必须是用户高度信任的应用。

三、实践

在示例实践中,我们将创建一个授权访问服务,定义一个API和要访问它的客户端,客户端通过IdentityServer上请求访问令牌,并使用它来访问API。

3.1 搭建 Authorization Server 服务

搭建认证授权服务

3.1.1 安装Nuget包

IdentityServer4 程序包

3.1.2 配置内容

建立配置内容文件Config.cs

    public static class Config{public static IEnumerable<IdentityResource> IdentityResources =>new IdentityResource[]{new IdentityResources.OpenId(),new IdentityResources.Profile(),};public static IEnumerable<ApiScope> ApiScopes =>new ApiScope[]{new ApiScope("password_scope1")};public static IEnumerable<ApiResource> ApiResources =>new ApiResource[]{new ApiResource("api1","api1"){Scopes={ "password_scope1" },ApiSecrets={new Secret("apipwd".Sha256())}  //api密钥}};public static IEnumerable<Client> Clients =>new Client[]{new Client{ClientId = "password_client",ClientName = "Resource Owner Password",AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,ClientSecrets = { new Secret("511536EF-F270-4058-80CA-1C89C192F69A".Sha256()) },AllowedScopes = { "password_scope1" }},};}

因为是资源所有者密码凭证授权的方式,所以我们通过代码的方式来创建几个测试用户。

新建测试用户文件TestUsers.cs

    public class TestUsers{public static List<TestUser> Users{get{var address = new{street_address = "One Hacker Way",locality = "Heidelberg",postal_code = 69118,country = "Germany"};return new List<TestUser>{new TestUser{SubjectId = "1",Username = "i3yuan",Password = "123456",Claims ={new Claim(JwtClaimTypes.Name, "i3yuan Smith"),new Claim(JwtClaimTypes.GivenName, "i3yuan"),new Claim(JwtClaimTypes.FamilyName, "Smith"),new Claim(JwtClaimTypes.Email, "i3yuan@email.com"),new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean),new Claim(JwtClaimTypes.WebSite, "http://i3yuan.top"),new Claim(JwtClaimTypes.Address, JsonSerializer.Serialize(address), IdentityServerConstants.ClaimValueTypes.Json)}}};}}}

返回一个TestUser的集合。

通过以上添加好配置和测试用户后,我们需要将用户注册到IdentityServer4服务中,接下来继续介绍。

3.1.3 注册服务

在startup.cs中ConfigureServices方法添加如下代码:

        public void ConfigureServices(IServiceCollection services){var builder = services.AddIdentityServer().AddTestUsers(TestUsers.Users); //添加测试用户// in-memory, code configbuilder.AddInMemoryIdentityResources(Config.IdentityResources);builder.AddInMemoryApiScopes(Config.ApiScopes);builder.AddInMemoryApiResources(Config.ApiResources);builder.AddInMemoryClients(Config.Clients);// not recommended for production - you need to store your key material somewhere securebuilder.AddDeveloperSigningCredential();}

3.1.4 配置管道

在startup.cs中Configure方法添加如下代码:

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env){if (env.IsDevelopment()){app.UseDeveloperExceptionPage();}app.UseRouting();app.UseIdentityServer();app.UseEndpoints(endpoints =>{endpoints.MapGet("/", async context =>{await context.Response.WriteAsync("Hello World!");});});}

以上内容是快速搭建简易IdentityServer项目服务的方式。

这搭建 Authorization Server 服务跟上一篇客户端凭证模式有何不同之处呢?

  1. 在Config中配置客户端(client)中定义了一个AllowedGrantTypes的属性,这个属性决定了Client可以被哪种模式被访问,GrantTypes.ClientCredentials客户端凭证模式GrantTypes.ResourceOwnerPassword资源所有者密码凭证授权。所以在本文中我们需要添加一个Client用于支持资源所有者密码凭证授权模式(ResourceOwnerPassword)。

  2. 因为资源所有者密码凭证授权需要用到用户名和密码所以要添加用户,而客户端凭证模式不需要,这也是两者的不同之处。

3.2 搭建API资源

实现对API资源进行保护

3.2.1 快速搭建一个API项目

3.2.2 安装Nuget包

IdentityServer4.AccessTokenValidation 包

3.2.3 注册服务

在startup.cs中ConfigureServices方法添加如下代码:

    public void ConfigureServices(IServiceCollection services){services.AddControllersWithViews();services.AddAuthorization();services.AddAuthentication("Bearer").AddIdentityServerAuthentication(options =>{options.Authority = "http://localhost:5001";options.RequireHttpsMetadata = false;options.ApiName = "api1";options.ApiSecret = "apipwd"; //对应ApiResources中的密钥});}

AddAuthentication把Bearer配置成默认模式,将身份认证服务添加到DI中。

AddIdentityServerAuthentication把IdentityServer的access token添加到DI中,供身份认证服务使用。

3.2.4 配置管道

在startup.cs中Configure方法添加如下代码:

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env){if (env.IsDevelopment()){app.UseDeveloperExceptionPage();}    app.UseRouting();app.UseAuthentication();app.UseAuthorization();app.UseEndpoints(endpoints =>{endpoints.MapDefaultControllerRoute();});}

UseAuthentication将身份验证中间件添加到管道中;

UseAuthorization 将启动授权中间件添加到管道中,以便在每次调用主机时执行身份验证授权功能。

3.2.5 添加API资源接口

[Route("api/[Controller]")]
[ApiController]
public class IdentityController:ControllerBase
{[HttpGet("getUserClaims")][Authorize]public IActionResult GetUserClaims(){return new JsonResult(from c in User.Claims select new { c.Type, c.Value });}
}

在IdentityController 控制器中添加 [Authorize] , 在进行请求资源的时候,需进行认证授权通过后,才能进行访问。

这搭建API资源跟上一篇客户端凭证模式有何不同之处呢?

我们可以发现这跟上一篇基本相似,但是可能需要注意的地方应该是ApiNameApiSecret,要跟你配置的API资源名称和API资源密钥相同。

3.3 搭建Client客户端

实现对API资源的访问和获取资源

3.3.1 搭建一个窗体程序

3.3.2 安装Nuget包

IdentityModel 包

3.3.3 获取令牌

客户端与授权服务器进行身份验证并向令牌端点请求访问令牌。授权服务器对客户端进行身份验证,如果有效,颁发访问令牌。

IdentityModel 包括用于发现 IdentityServer 各个终结点(EndPoint)的客户端库。

我们可以使用从 IdentityServer 元数据获取到的Token终结点请求令牌:

        private void getToken_Click(object sender, EventArgs e){var client = new HttpClient();var disco = client.GetDiscoveryDocumentAsync(this.txtIdentityServer.Text).Result;if (disco.IsError){this.tokenList.Text = disco.Error;return;}//请求tokentokenResponse = client.RequestPasswordTokenAsync(new PasswordTokenRequest{Address = disco.TokenEndpoint,ClientId =this.txtClientId.Text,ClientSecret = this.txtClientSecret.Text,Scope = this.txtApiScopes.Text,UserName=this.txtUserName.Text,Password=this.txtPassword.Text}).Result;if (tokenResponse.IsError){this.tokenList.Text = disco.Error;return;}this.tokenList.Text = JsonConvert.SerializeObject(tokenResponse.Json);this.txtToken.Text = tokenResponse.AccessToken;}

3.3.4 调用API

要将Token发送到API,通常使用HTTP Authorization标头。这是使用SetBearerToken扩展方法完成。

        private void getApi_Click(object sender, EventArgs e){//调用认证apiif (string.IsNullOrEmpty(txtToken.Text)){MessageBox.Show("token值不能为空");return;}var apiClient = new HttpClient();//apiClient.SetBearerToken(tokenResponse.AccessToken);apiClient.SetBearerToken(this.txtToken.Text);var response = apiClient.GetAsync(this.txtApi.Text).Result;if (!response.IsSuccessStatusCode){this.resourceList.Text = response.StatusCode.ToString();}else{this.resourceList.Text = response.Content.ReadAsStringAsync().Result;}}

这搭建Client客户端跟上一篇客户端凭证模式有何不同之处呢?

  1. 客户端请求token多了两个参数,一个用户名,一个密码

  2. 请求Token中使用IdentityModel包的方法RequestPasswordTokenAsync,实现用户密码方式获取令牌。

以上展示的代码有不明白的,可以看本篇项目源码,项目地址为 :资源所有者密码凭证模式

https://github.com/i3yuan/Yuan.IdentityServer4.Demo/tree/main/DiffAuthMode/ResourceOwnerPasswords

3.4 效果

3.4.1 项目测试

3.4.2 postman测试

四、拓展

从上一篇的客户端凭证模式到这一篇的资源所有者资源密码凭证模式,我们都已经初步掌握了大致的授权流程,以及项目搭建获取访问受保护的资源,但是我们也可能发现了,如果是仅仅为了访问保护的API资源的话,加不加用户和密码好像也没什么区别呢。

但是如果仔细对比两种模式在获取token,以及访问api返回的数据可以发现,资源所有者密码凭证模式返回的Claim的数量信息要多一些,但是客户端模式返回的明显少了一些,这是因为客户端不涉及用户信息。所以资源密码凭证模式可以根据用户信息做具体的资源权限判断。

比如,在TestUser有一个Claims属性,允许自已添加Claim,有一个ClaimTypes枚举列出了可以直接添加的Claim。所以我们可以为用户设置角色,来判断角色的权限功能,做简单的权限管理。

4.1 添加用户角色

在之前创建的TestUsers.cs文件的User方法中,添加Cliam的角色熟悉,如下:

public class TestUsers
{public static List<TestUser> Users{get{var address = new{street_address = "One Hacker Way",locality = "Heidelberg",postal_code = 69118,country = "Germany"};return new List<TestUser>{new TestUser{SubjectId = "1",Username = "i3yuan",Password = "123456",Claims ={new Claim(JwtClaimTypes.Name, "i3yuan Smith"),new Claim(JwtClaimTypes.GivenName, "i3yuan"),new Claim(JwtClaimTypes.FamilyName, "Smith"),new Claim(JwtClaimTypes.Email, "i3yuan@email.com"),new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean),new Claim(JwtClaimTypes.WebSite, "http://i3yuan.top"),new Claim(JwtClaimTypes.Address, JsonSerializer.Serialize(address), IdentityServerConstants.ClaimValueTypes.Json),new Claim(JwtClaimTypes.Role,"admin")  //添加角色},}};}}
}

4.2 配置API资源需要的Cliam

因为要用到ApiResourcesApiResources的构造函数有一个重载支持传进一个Claim集合,用于允许该Api资源可以携带那些Claim, 所以在项目下的Config类的ApiResources做出如下修改:

        public static IEnumerable<ApiResource> ApiResources =>new ApiResource[]{new ApiResource("api1","api1"){Scopes={ "password_scope1" },UserClaims={JwtClaimTypes.Role},  //添加Cliam 角色类型ApiSecrets={new Secret("apipwd".Sha256())}}};

4.3 添加支持Role验证

在API资源项目中,修改下被保护Api的,使其支持Role验证。

    [HttpGet("getUserClaims")]//[Authorize][Authorize(Roles ="admin")]public IActionResult GetUserClaims(){return new JsonResult(from c in User.Claims select new { c.Type, c.Value });}

4.4 效果

可以看到,为我们添加了一个Role Claim,效果如下:

五、总结

  1. 本篇主要阐述以资源所有者密码凭证授权,编写一个客户端,以及受保护的资源,并通过客户端请求IdentityServer上请求获取访问令牌,从而获取受保护的资源。

  2. 这种模式主要使用client_id和client_secret以及用户名密码通过应用Client(客户端)直接获取秘钥,但是存在client可能存了用户密码这不安全性问题,如果client是自家高可信的应用,也是可以使用的,同时如果遗留项目升级为oauth2的授权机制也是适配适用的。

  3. 在后续会对其中的其他授权模式,数据库持久化问题,以及如何应用在API资源服务器中和配置在客户端中,会进一步说明。

  4. 如果有不对的或不理解的地方,希望大家可以多多指正,提出问题,一起讨论,不断学习,共同进步。

  5. 项目地址

    https://github.com/i3yuan/Yuan.IdentityServer4.Demo/tree/main/DiffAuthMode/ResourceOwnerPasswords

六、附加

Resource Owner Password Validation资料

Password Grant资料

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

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

相关文章

深入探究ASP.NET Core Startup的初始化

前言Startup类相信大家都比较熟悉,在我们使用ASP.NET Core开发过程中经常用到的类&#xff0c;我们通常使用它进行IOC服务注册&#xff0c;配置中间件信息等。虽然它不是必须的&#xff0c;但是将这些操作统一在Startup中做处理&#xff0c;会在实际开发中带来许多方便。当我们…

python远程监控服务器多个日志_python压测+paramiko远程监下载日志+js测试报告

关于压测客户端netty nio压测端package com.nio.test;import io.netty.bootstrap.Bootstrap;import io.netty.buffer.ByteBuf;import io.netty.buffer.Unpooled;import io.netty.channel.ChannelFuture;import io.netty.channel.ChannelInitializer;import io.netty.channel.Ch…

【源码】常用的人脸识别数据库以及上篇性别识别源码

上一篇《使用ML.NET模型生成器来完成图片性别识别》发布后&#xff0c;很多朋友希望得到源码&#xff0c;这里附上地址&#xff1a;https://github.com/xin-lai/GenderRecognition常用的人脸数据库对于部分朋友说&#xff0c;找不到训练的数据&#xff0c;这里也给出部分数据&a…

deb包如何改支持12系统_对一个deb包的解压、修改、重新打包全过程方法

出于多种原因&#xff0c;有的时候需要直接对deb包中的各种文件内容进行修改主要有三个问题需要解决&#xff1a;0、如何将deb包文件进行解包呢&#xff1f;1、修改要修改的文件&#xff1f;2、对修改后的内容进行生成deb包&#xff1f;解包命令为#解压出包中的文件到extract目…

程序员过关斩将--真的可以用版本号的方式来保证MQ消费消息的幂等性?

灵魂拷问MQ消息的消费为什么有时候要求幂等性&#xff1f;你们都说可以用版本号来解决幂等性消费&#xff1f;什么才是消息幂等性消费的根本性问题&#xff1f;随着系统的复杂性不断增加&#xff0c;多数系统都会引入MQ来进行解耦&#xff0c;其实从引入MQ的初衷来说&#xff0…

realloc函_[转载]realloc函数的使用及注意事项(转)

原型&#xff1a;externvoid *realloc(void *mem_address, unsigned int newsize);用法&#xff1a;#include 功能&#xff1a;改变mem_address所指内存区域的大小为newsize长度。说明&#xff1a;如果重新分配成功则返回指向被分配内存的指针&#xff0c;否则返回空指针NULL。…

InfluxDB 2.0 之Flux语法篇

由于项目 IoTSharp 需要支持 InfluxDB &#xff0c; 因此进行了初步尝试&#xff0c; 虽然 Flux 语法初次学习&#xff0c; 但查询语句却似曾相识&#xff0c; 如果改一下 标点符号&#xff0c; 你会完全认为他是 C#的拉姆达表达式&#xff0c; 首先看一下写入数据:using (var…

spring的钩子_spring提供的钩子,你知道哪些

俗话说得好“工欲善其事必先利其器”&#xff0c;现如今springboot与springcloud已成为快速构建web应用的利器。作为一个爪洼工程师&#xff0c;知道如下的spring扩展点&#xff0c;可能会让你编写出扩展性、维护性更高的代码。spring提供的钩子&#xff0c;你知道哪些bean的生…

.Net 5性能改进

起因在.Net Core跳过4.0,避免和先.Net Framework 4.0同名,版本号变为5.0,同时也不在叫.Net Core改为.Net 5(统一的叫法),先看看官方对.Net版本规划.本文主要是根据https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-5/ 翻译而来.不完全翻译.顺序也有所调…

ffmpeg库编译加文字_1.编译ffmpeg库

1.下载ffmpeg#!/bin/bashsource"ffmpeg-4.1"if [ ! -r $source ]thencurl http://ffmpeg.org/releases/${source}.tar.bz2 | tar xj || exit 1ficurl 表示下载&#xff0c;后边跟下载的地址。tar表示解压或者压缩。 x表示解压&#xff0c;j表示是否需要解压bz2压缩包…

C#中形态各异的class

本篇是基本知识&#xff0c;老码农请无视&#xff01;&#xff01;&#xff01;普通静态抽象密封分部修饰关键字无staticabstractsealedpartial构造函数调用时机实例化(new)时内部任意静态成员调用时子类实例化(new)时实例化(new)时实例化(new)时包含成员字段属性方法事件索引器…

mysql 笛卡尔积_Mysql内连接、左连接会出现笛卡尔积的理解

先简单解释一下笛卡尔积。 现在,我们有两个集合A和B。 A = {0,1} B = {2,3,4} 集合 AB 和 BA的结果集就可以分别表示为以下这种形式: AB = {(0,2),(1,2),(0,3),(1,3),(0,4),(1,4)}; BA = {(2,0),(2,1),(3,0),(3,1),(4,0),(4,1)}; 以上AB和BA的结…

开放数字世界中的复杂图数据挑战 —— 以教育与开源场景为例

摘要&#xff1a;开源开放的数字世界开始成为时代的潮流&#xff0c;云原生、数据中台、智能PRA开始成为数字世界中的新一代中流砥柱。随着第四范式的普遍流行&#xff0c;各个行业中的数字化转型都会带了海量的具有无限关联的复杂图数据。本报告将以教育与开源两个场景为例&am…

mysql anyvalue函数_Mysql 的ANY_VALUE()函数和 ONLY_FULL_GROUP_BY 模式

Mysql 的ANY_VALUE()函数和 ONLY_FULL_GROUP_BY 模式1、ONLY_FULL_GROUP_BY 引发在mysql 5.7版本以上进行一些ORDER BY 或者 GROUP BY 时&#xff0c;会出现如下错误[Err] 1055 - Expression #1 of ORDER BY clause is not in GROUP BY clause and contains nonaggregated colu…

在IIS中部署SPA应用,多么痛的领悟!

目前公司的Web项目是SPA应用&#xff0c;采用前后端分离开发&#xff0c;所以有时也会倒腾Vue框架。“前后端应用最终以容器形态、在k8s中部署, 为此我搭建了基于Gitlab flow的Devops流程。在Devops实践中&#xff0c;容器部署成为良方和事实标准。但是在开发和自测阶段&#x…

mysql闪回工具下载_MySQL闪回工具之myflash 和 binlog2sql

实践利用binlog2sql查询两个binlog之间的SQL&#xff1a;必须是两个binlog日志&#xff0c;指定start-file和stop-filebinlog2sql -h127.0.0.1 -P3309 -udba -pxxxxxx -dsakila -t employee --start-filemysql-bin.000112 --stop-filemysql-bin.000113 > /tmp/db.sql利用bin…

MySQL大表优化方案

背景阿里云RDS FOR MySQL&#xff08;MySQL5.7版本&#xff09;数据库业务表每月新增数据量超过千万,随着数据量持续增加,我们业务出现大表慢查询,在业务高峰期主业务表的慢查询需要几十秒严重影响业务方案概述一、数据库设计及索引优化MySQL数据库本身高度灵活&#xff0c;造成…

使用Azure静态Web应用部署Blazor Webassembly应用

上一次演示了如何使用Azure静态web应用部署VUE前端项目&#xff08;使用Azure静态web应用全自动部署VUE站点&#xff09;。我们知道静态web应用支持VUE&#xff0c;react&#xff0c;angular等项目的部署。除了支持这些常见前端框架&#xff0c;静态web应用同样支持微软推出的最…

mysql无法创建新用户_如何mysql禁止创建新用户

展开全部使用户不具有e69da5e6ba9062616964757a686964616f31333337376264Create User权限或者deny Create User权限下面是权限列表mysql> show privileges \G*************************** 1. row ***************************Privilege: AlterContext: TablesComment: To al…

TIOBE 11 月榜单:Python 挤掉 Java,Java的下跌趋势确立了?

喜欢就关注我们吧&#xff01;TIOBE 公布了 2020 年 11 月的编程语言排行榜。Python 已成功跃居榜单第二名&#xff0c;本月排名率为 12.12%&#xff1b;Java 被挤到第三位&#xff0c;排名率降至 11.68%。自有 TIOBE 榜单以来&#xff0c;C 和 Java 之前一直占据着前两名的位置…