第十二节:Ocelot集成IDS4认证授权-微服务主体架构完成

一. 前言

1.业务背景

  我们前面尝试了在业务服务器上加IDS4校验,实际上是不合理的, 在生产环境中,业务服务器会有很多个,如果把校验加在每个业务服务器上,代码冗余且不易维护(很多情况下业务服务器不直接对外开放),所以我们通常把校验加在Ocelot网关上,也就是说校验通过了,Ocelot网关才会把请求转发给相应的业务服务器上.(我们这里通常是网关和业务服务器在一个内网中,业务服务器不开放外网)

(和前面:Jwt配合中间件校验流程上是一样的,只不过这里的认证和授权都基于IDS4来做)

PS:关于IDS4服务器,可以配置在网关后面,通过网关转发;

   也可以不经网关转发,单独存在, 这里要说明的是,如果经过网关转发,那么对于IDS4而言,只是单纯的转发,不走Ocelot上的校验,其实也很简单,就是不配置AuthenticationProviderKey即可.

 

 

2.用到的项目

(1).Case2下的GateWay_Server :网关服务器

(2).Case2下的ID4_Server:认证+授权服务器

(3).GoodsService + OrderService:二者都是资源服务器

(4).PostMan:充当客户端(即第三方应用)

(5).MyClient2:用控制台充当客户端(即第三方应用)

(6).Consul:网关Ocelot已经集成Consul服务发现了,而且资源服务器也已经注册到Consul中了.

 

二. 核心剖析和测试

1.搭建步骤

(一).启动资源服务器

 (1).启动Consul:【consul.exe agent -dev】

 (2).启动资源服务器:【dotnet GoodsService.dll --urls="http://*:7001" --ip="127.0.0.1" --port=7001 】

            【dotnet OrderService.dll --urls="http://*:7004" --ip="127.0.0.1" --port=7004 】

代码分享:

    [Route("api/[controller]/[action]")]
    [ApiController]
    public class CatalogController : ControllerBase
    {
        [HttpGet]
        public string GetGoodById1(int id)
        {
            var myData = new
            {
                status = "ok",
                goods = new Goods()
                {
                    id = id,
                    goodName = "apple",
                    goodPrice = 6000,
                    addTime = DateTime.Now
                }
            };
            var jsonData = JsonHelp.ToJsonString(myData);
            return jsonData;   //返回前端的数据不能直接点出来
        }
    }
    [Route("api/[controller]/[action]")]
    [ApiController]
    public class BuyController : ControllerBase
    {

        [HttpPost]
        public string pOrder1()
        {
            return "ok";
        }
    }

(二). GateWay_Server网关的基本配置

(1).Nuget安装包【Ocelot 16.0.1】【Ocelot.Provider.Consul 16.0.1】, 并在ConfigureService和Config中进行配置 services.AddOcelot().AddConsul(); 和 app.UseOcelot().Wait();

(2).Nuget安装包【IdentityServer4.AccessTokenValidation 3.0.1】,用于身份校验.

(3).编写配置文件(OcelotConfig.json),属性改为始终复制,在Program中进行加载;

 在配置文件,给GoodsService和OrderService下的节点, 添加 "AuthenticationProviderKey": "OrderServiceKey"/"GoodsServiceKey", 和ConfigureService中的注册进行对应,表示该转发需要走校验.

 (把IDS4获取Token的地址也配置进来,但不做校验,也可以不配置进来)

代码分享

//模式三:将Ocelot与consul结合处理,在consul中已经注册业务服务器地址,在Ocelot端不需要再注册了(推荐用法)
{
  "Routes": [
    {
      "DownstreamPathTemplate": "/api/{url}",
      "DownstreamScheme": "http",
      "ServiceName": "GoodsService", //Consul中的服务名称
      "LoadBalancerOptions": {
        "Type": "RoundRobin" //轮询算法:依次调用在consul中注册的服务器
      },
      "UseServiceDiscovery": true, //启用服务发现(可以省略,因为会默认赋值)
      "UpstreamPathTemplate": "/GoodsService/{url}",
      "UpstreamHttpMethod": [ "Get", "Post" ],
      "AuthenticationOptions": {
        "AuthenticationProviderKey": "GoodsServiceKey",
        "AllowedScopes": []
      }
    },
    {
      "DownstreamPathTemplate": "/api/{url}",
      "DownstreamScheme": "http",
      "ServiceName": "OrderService",
      "LoadBalancerOptions": {
        "Type": "LeastConnection" //最小连接数算法
      },
      "UseServiceDiscovery": true,
      "UpstreamPathTemplate": "/OrderService/{url}",
      "UpstreamHttpMethod": [ "Get", "Post" ],
      "AuthenticationOptions": {
        "AuthenticationProviderKey": "OrderServiceKey",
        "AllowedScopes": []
      }
    },
    //把ID4_Server认证授权服务器也配置进来,但它不再Ocelot层次上加密,单纯的进行转发
    {
      //转发下游(业务服务器)的匹配规则
      "DownstreamPathTemplate": "/{url}",
      //下游请求类型
      "DownstreamScheme": "http",
      //下游的ip和端口,和上面的DownstreamPathTemplate匹配起来
      "DownstreamHostAndPorts": [
        {
          "Host": "127.0.0.1",
          "Port": 7051
        }
      ],
      //上游(即Ocelot)接收规则
      "UpstreamPathTemplate": "/auth/{url}",
      //上游接收请求类型
      "UpstreamHttpMethod": [ "Get", "Post" ]
    }
  ],
  //下面是配置Consul的地址和端口
  "GlobalConfiguration": {
    //对应Consul的ip和Port(可以省略,因为会默认赋值)
    "ServiceDiscoveryProvider": {
      "Host": "127.0.0.1",
      "Port": 8500
    }
  }
}
View Code

(4). 在ConfigureServices注册ID4校验,详细参数见代码说明   特别注意:ApiName必须对应Id4中配置的

代码分享:

  public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            //1.注册Ocelot
            services.AddOcelot().AddConsul();

            //2.注册ID4校验
            services.AddAuthentication("Bearer")
                    .AddIdentityServerAuthentication("GoodsServiceKey", option =>      //这里GoodsServiceKey与Ocelot配置文件中的AuthenticationProviderKey对应,从而进行绑定验证
                    {
                        option.Authority = "http://127.0.0.1:7051";  //这里配置是127.0.0.1,那么通过ID4服务器获取token的时候,就必须写127.0.0.1,不能写localhost.   
                        option.ApiName = "GoodsService";             //必须对应ID4服务器中GetApiResources配置的apiName,此处不能随便写!!
                        option.RequireHttpsMetadata = false;
                    })
                    .AddIdentityServerAuthentication("OrderServiceKey", option =>
                    {
                        option.Authority = "http://127.0.0.1:7051";
                        option.ApiName = "OrderService";
                        option.RequireHttpsMetadata = false;
                    });

            services.AddControllers();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            app.UseRouting();

            //启用Ocelot
            app.UseOcelot().Wait();


            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
View Code

(5).配置IP+端口的命令行启动 【dotnet GateWay_Server.dll --urls="http://*:7050" --ip="127.0.0.1" --port=7050 】

(三). ID4_Server的基本配置

(1). Nuget安装包【IdentityServer4    4.0.2】

(2). 在ConfigureServie注册客户端模式 或 用户名密码模式,根据需要开启或注释哪个, Config中启用IDS4

配置文件-客户端模式

/// <summary>
    /// 客户端模式
    /// </summary>
    public class Config1
    {
        /// <summary>
        /// 配置Api范围集合
        /// 4.x版本新增的配置
        /// </summary>
        /// <returns></returns>
        public static IEnumerable<ApiScope> GetApiScopes()
        {
            return new List<ApiScope>
            {
                new ApiScope("GoodsService"),
                new ApiScope("OrderService")
             };
        }


        /// <summary>
        /// 需要保护的Api资源
        /// 4.x版本新增后续Scopes的配置
        /// </summary>
        /// <returns></returns>
        public static IEnumerable<ApiResource> GetApiResources()
        {
            List<ApiResource> resources = new List<ApiResource>();
            //ApiResource第一个参数是ServiceName,第二个参数是描述
            resources.Add(new ApiResource("GoodsService", "GoodsService服务需要保护哦") { Scopes = { "GoodsService" } });
            resources.Add(new ApiResource("OrderService", "OrderService服务需要保护哦") { Scopes = { "OrderService" } });
            return resources;
        }

        /// <summary>
        /// 可以使用ID4 Server 客户端资源
        /// </summary>
        /// <returns></returns>
        public static IEnumerable<Client> GetClients()
        {
            List<Client> clients = new List<Client>() {
                new Client
                {
                    ClientId = "client1",//客户端ID                             
                    AllowedGrantTypes = GrantTypes.ClientCredentials, //验证类型:客户端验证
                    ClientSecrets ={ new Secret("0001".Sha256())},    //密钥和加密方式
                    AllowedScopes = { "GoodsService", "OrderService" }        //允许访问的api服务
                },
                new Client
                {
                    ClientId = "client2",//客户端ID                             
                    AllowedGrantTypes = GrantTypes.ClientCredentials, //验证类型:客户端验证
                    ClientSecrets ={ new Secret("0002".Sha256())},    //密钥和加密方式
                    AllowedScopes = { "GoodsService"}        //允许访问的api服务
                },
                 new Client
                {
                    ClientId = "client3",//客户端ID                             
                    AllowedGrantTypes = GrantTypes.ClientCredentials, //验证类型:客户端验证
                    ClientSecrets ={ new Secret("0003".Sha256())},    //密钥和加密方式
                    AllowedScopes = {"OrderService" }        //允许访问的api服务
                }
            };
            return clients;
        }
    }
View Code

配置文件-用户名密码模式

 /// <summary>
    /// 用户名密码模式
    /// </summary>
    public class Config2
    {
        /// <summary>
        /// 配置Api范围集合
        /// 4.x版本新增的配置
        /// </summary>
        /// <returns></returns>
        public static IEnumerable<ApiScope> GetApiScopes()
        {
            return new List<ApiScope>
            {
                new ApiScope("GoodsService"),
                new ApiScope("OrderService")
             };
        }


        /// <summary>
        /// 需要保护的Api资源
        /// 4.x版本新增后续Scopes的配置
        /// </summary>
        /// <returns></returns>
        public static IEnumerable<ApiResource> GetApiResources()
        {
            List<ApiResource> resources = new List<ApiResource>();
            //ApiResource第一个参数是ServiceName,第二个参数是描述
            resources.Add(new ApiResource("GoodsService", "GoodsService服务需要保护哦") { Scopes = { "GoodsService" } });
            resources.Add(new ApiResource("OrderService", "OrderService服务需要保护哦") { Scopes = { "OrderService" } });
            return resources;
        }



        /// <summary>
        /// 可以使用ID4 Server 客户端资源
        /// </summary>
        /// <returns></returns>
        public static IEnumerable<Client> GetClients()
        {
            List<Client> clients = new List<Client>() {
                new Client
                {
                    ClientId = "client1",//客户端ID                             
                    AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, //验证类型:客户端验证
                    ClientSecrets ={ new Secret("0001".Sha256())},    //密钥和加密方式
                    AllowedScopes = { "GoodsService", "OrderService" }        //允许访问的api服务
                },
                new Client
                {
                    ClientId = "client2",//客户端ID                             
                    AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, //验证类型:客户端验证
                    ClientSecrets ={ new Secret("0002".Sha256())},    //密钥和加密方式
                    AllowedScopes = { "GoodsService" }        //允许访问的api服务
                },
                 new Client
                {
                    ClientId = "client3",//客户端ID                             
                    AllowedGrantTypes = GrantTypes.ResourceOwnerPasswordAndClientCredentials, //验证类型:用户名密码模式 和 客户端模式
                    ClientSecrets ={ new Secret("0003".Sha256())},    //密钥和加密方式
                    AllowedScopes = {"OrderService" }        //允许访问的api服务
                }
            };
            return clients;
        }

        /// <summary>
        /// 定义可以使用ID4的用户资源
        /// </summary>
        /// <returns></returns>
        public static IEnumerable<TestUser> GetUsers()
        {
            return new List<TestUser>()
            {
                new TestUser
                {
                    SubjectId = "10001",
                    Username = "ypf1",     //账号
                    Password = "ypf001"    //密码
                },
                new TestUser
                {
                    SubjectId = "10002",
                    Username = "ypf2",
                    Password = "ypf002"
                },
                new TestUser
                {
                    SubjectId = "10003",
                    Username = "ypf3",
                    Password = "ypf003"
                }
            };
        }
    }
View Code

Startup类

 public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {

            //1. 客户端模式
            //services.AddIdentityServer()
            //      .AddDeveloperSigningCredential()    //生成Token签名需要的公钥和私钥,存储在bin下tempkey.rsa(生产场景要用真实证书,此处改为AddSigningCredential)
            //      .AddInMemoryApiResources(Config1.GetApiResources())  //存储需要保护api资源
            //      .AddInMemoryApiScopes(Config1.GetApiScopes())        //配置api范围 4.x版本必须配置的
            //      .AddInMemoryClients(Config1.GetClients()); //存储客户端模式(即哪些客户端可以用)


            //2. 用户名密码模式
            services.AddIdentityServer()
                  .AddDeveloperSigningCredential()    //生成Token签名需要的公钥和私钥,存储在bin下tempkey.rsa(生产场景要用真实证书,此处改为AddSigningCredential)
                  .AddInMemoryApiResources(Config2.GetApiResources())  //存储需要保护api资源
                  .AddInMemoryClients(Config2.GetClients()) //存储客户端模式(即哪些客户端可以用)
                  .AddInMemoryApiScopes(Config1.GetApiScopes())        //配置api范围 4.x版本必须配置的
                  .AddTestUsers(Config2.GetUsers().ToList());  //存储哪些用户、密码可以访问 (用户名密码模式)


            services.AddControllers();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();

            //1.启用IdentityServe4
            app.UseIdentityServer();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
View Code

(3). 配置IP+端口的命令行启动 【dotnet ID4_Server.dll --urls="http://*:7051" --ip="127.0.0.1" --port=7051 】

 

2. 用PostMan测试

场景1:用PostMan进行下面测试

  测试Get请求:http://127.0.0.1:7050/GoodsService/Catalog/GetGoodById1?id=123 测试结果:401未授权

  测试Post请求:http://127.0.0.1:7050/OrderService/Buy/pOrder1 测试结果:401未授权

测试结果:

 

场景2:用PostMan进行下面测试

  先请求:http://127.0.0.1:7051/connect/token 表单参数如下,获取token值

  client_id=client1

  grant_type=client_credentials

  client_secret=0001

  然后再Header中要配置 token=Bear xxxxxxxx(上面获取的token值),

(也可用PostMan中Authorization选项卡下,TYPE选择Bearer Token,然后内容直接输入上面获取的token即可)

测试Get请求:http://127.0.0.1:7050/GoodsService/Catalog/GetGoodById1?id=123 测试结果:测试通过,获得返回值

测试Post请求:http://127.0.0.1:7050/OrderService/Buy/pOrder1 测试结果:测试通过,获得返回值

PS: 上述场景2的测试,是直接请求的IDS4服务器,并没有通过Ocelot转发哦,当然也可以请求 http://127.0.0.1:7050/auth/connect/token"来获取(本质上是通过Ocelot转发到了 http://127.0.0.1:7051/connect/token)

测试结果: 

 

 

3.用控制台客户端测试

公用代码

            //认证服务器地址
            string rzAddress = "http://127.0.0.1:7051";
            //通过Ocelot转发到认证服务器地址
            string ocelotRzAddress = "http://127.0.0.1:7050/auth";
            //资源服务器1api地址
            string resAddress1 = "http://127.0.0.1:7050/GoodsService/Catalog/GetGoodById1?id=123";
            //资源服务器2api地址
            string resAddress2 = "http://127.0.0.1:7050/OrderService/Buy/pOrder1 ";

(1).客户端模式(直接请求IDS4服务器)

代码分享

  var client = new HttpClient();
                var disco = await client.GetDiscoveryDocumentAsync(rzAddress);
                if (disco.IsError)
                {
                    Console.WriteLine(disco.Error);
                    return;
                }
                //向认证服务器发送请求,要求获得令牌
                Console.WriteLine("---------------------------- 一.向认证服务器发送请求,要求获得令牌-----------------------------------");
                var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
                {
                    //在上面的地址上拼接:/connect/token,最终:http://xxxx/connect/token
                    Address = disco.TokenEndpoint,
                    ClientId = "client1",
                    ClientSecret = "0001",
                    //空格分隔的请求范围列表,省略的话是默认配置的所有api资源,如: client1对应的是:{ "GoodsService", "OrderService", "ProductService" }  
                    //这里填写的范围可以和配置的相同或者比配置的少,比如{ "GoodsService OrderService"},这里只是一个范围列表,并不是请求哪个api资源必须要  写在里面
                    //但是如果配置的和默认配置出现不同,则认证不能通过 比如{ "ProductService OrderService111"},
                    //综上所述:此处可以不必配置
                    //Scope = "ProductService OrderService111"
                });
                if (tokenResponse.IsError)
                {
                    Console.WriteLine($"认证错误:{tokenResponse.Error}");
                    Console.ReadKey();
                }
                Console.WriteLine(tokenResponse.Json);


                //携带token向资源服务器发送请求
                Console.WriteLine("----------------------------二.携带token向资源服务器发送请求-----------------------------------");
                var apiClient = new HttpClient();
                apiClient.SetBearerToken(tokenResponse.AccessToken);   //设置Token格式  【Bear xxxxxx】
                //2.1 向资源服务器1发送请求
                var response = await apiClient.GetAsync(resAddress1);
                if (!response.IsSuccessStatusCode)
                {
                    Console.WriteLine(response.StatusCode);
                    Console.ReadKey();
                }
                else
                {
                    var content = await response.Content.ReadAsStringAsync();
                    Console.WriteLine($"请求资源服务器1的结果为:{content}");
                }
                //2.2 向资源服务器2发送请求
                var sContent = new StringContent("", Encoding.UTF8, "application/x-www-form-urlencoded");
                var response2 = await apiClient.PostAsync(resAddress2, sContent);
                if (!response2.IsSuccessStatusCode)
                {
                    Console.WriteLine(response2.StatusCode);
                    Console.ReadKey();
                }
                else
                {
                    var content = await response2.Content.ReadAsStringAsync();
                    Console.WriteLine($"请求资源服务器2的结果为:{content}");
                }
View Code

运行结果

 

(2).客户端模式(通过Ocelot转发到IDS4服务器)

代码分享

    var client = new HttpClient();
                //向认证服务器发送请求,要求获得令牌
                Console.WriteLine("---------------------------- 一.向认证服务器发送请求,要求获得令牌-----------------------------------");
                var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
                {
                    Address = ocelotRzAddress + "/connect/token",
                    ClientId = "client1",
                    ClientSecret = "0001",
                    //空格分隔的请求范围列表,省略的话是默认配置的所有api资源,如: client1对应的是:{ "GoodsService", "OrderService", "ProductService" }  
                    //这里填写的范围可以和配置的相同或者比配置的少,比如{ "GoodsService OrderService"},这里只是一个范围列表,并不是请求哪个api资源必须要  写在里面
                    //但是如果配置的和默认配置出现不同,则认证不能通过 比如{ "ProductService OrderService111"},
                    //综上所述:此处可以不必配置
                    //Scope = "ProductService OrderService111"
                });
                if (tokenResponse.IsError)
                {
                    Console.WriteLine($"认证错误:{tokenResponse.Error}");
                    Console.ReadKey();
                }
                Console.WriteLine(tokenResponse.Json);

                //携带token向资源服务器发送请求
                Console.WriteLine("----------------------------二.携带token向资源服务器发送请求-----------------------------------");
                var apiClient = new HttpClient();
                apiClient.SetBearerToken(tokenResponse.AccessToken);   //设置Token格式  【Bear xxxxxx】
                //2.1 向资源服务器1发送请求
                var response = await apiClient.GetAsync(resAddress1);
                if (!response.IsSuccessStatusCode)
                {
                    Console.WriteLine(response.StatusCode);
                    Console.ReadKey();
                }
                else
                {
                    var content = await response.Content.ReadAsStringAsync();
                    Console.WriteLine($"请求资源服务器1的结果为:{content}");
                }
                //2.2 向资源服务器2发送请求
                var sContent = new StringContent("", Encoding.UTF8, "application/x-www-form-urlencoded");
                var response2 = await apiClient.PostAsync(resAddress2, sContent);
                if (!response2.IsSuccessStatusCode)
                {
                    Console.WriteLine(response2.StatusCode);
                    Console.ReadKey();
                }
                else
                {
                    var content = await response2.Content.ReadAsStringAsync();
                    Console.WriteLine($"请求资源服务器2的结果为:{content}");
                }
View Code

运行结果

 

(3).用户名密码模式(直接请求IDS服务器)

代码分享

      var client = new HttpClient();
                var disco = await client.GetDiscoveryDocumentAsync(rzAddress);
                if (disco.IsError)
                {
                    Console.WriteLine(disco.Error);
                    Console.ReadKey();
                }
                //向认证服务器发送请求,要求获得令牌
                Console.WriteLine("---------------------------- 一.向认证服务器发送请求,要求获得令牌-----------------------------------");
                var tokenResponse = await client.RequestPasswordTokenAsync(new PasswordTokenRequest
                {
                    Address = disco.TokenEndpoint,
                    ClientId = "client1",
                    ClientSecret = "0001",
                    UserName = "ypf2",
                    Password = "ypf002"
                    //Scope = ""   //可以不用配置
                });
                if (tokenResponse.IsError)
                {
                    Console.WriteLine($"认证错误:{tokenResponse.Error}");
                    Console.ReadKey();
                }
                Console.WriteLine(tokenResponse.Json);

                //携带token向资源服务器发送请求
                Console.WriteLine("----------------------------二.携带token向资源服务器发送请求-----------------------------------");
                var apiClient = new HttpClient();
                apiClient.SetBearerToken(tokenResponse.AccessToken);   //设置Token格式  【Bear xxxxxx】
                //2.1 向资源服务器1发送请求
                var response = await apiClient.GetAsync(resAddress1);
                if (!response.IsSuccessStatusCode)
                {
                    Console.WriteLine(response.StatusCode);
                    Console.ReadKey();
                }
                else
                {
                    var content = await response.Content.ReadAsStringAsync();
                    Console.WriteLine($"请求资源服务器1的结果为:{content}");
                }
                //2.2 向资源服务器2发送请求
                var sContent = new StringContent("", Encoding.UTF8, "application/x-www-form-urlencoded");
                var response2 = await apiClient.PostAsync(resAddress2, sContent);
                if (!response2.IsSuccessStatusCode)
                {
                    Console.WriteLine(response2.StatusCode);
                    Console.ReadKey();
                }
                else
                {
                    var content = await response2.Content.ReadAsStringAsync();
                    Console.WriteLine($"请求资源服务器2的结果为:{content}");
                }
View Code

运行结果

 

 

 

 

 

 

 

 

!

  • 作       者 : Yaopengfei(姚鹏飞)
  • 博客地址 : http://www.cnblogs.com/yaopengfei/
  • 声     明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
  • 声     明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
 

 

posted @ 2020-06-21 21:25  Yaopengfei  阅读(2338)  评论(8编辑  收藏  举报