一、前言

IdentityServer4已经分享了一些应用实战的文章,从架构到授权中心的落地应用,也伴随着对IdentityServer4掌握了一些使用规则,但是很多原理性东西还是一知半解,故我这里持续性来带大家一起来解读它的相关源代码,本文先来看看为什么Controller或者Action中添加Authorize或者全局中添加AuthorizeFilter过滤器就可以实现该资源受到保护,需要通过access_token才能通过相关的授权呢?今天我带大家来了解AuthorizeAttributeAuthorizeFilter的关系及代码解读。

二、代码解读

解读之前我们先来看看下面两种标注授权方式的代码:

标注方式
 [Authorize]
[HttpGet]
public async Task<object> Get()
{
var userId = User.UserId();
return new
{
name = User.Name(),
userId = userId,
displayName = User.DisplayName(),
merchantId = User.MerchantId(),
};
}

代码中通过[Authorize]标注来限制该api资源的访问

全局方式
public void ConfigureServices(IServiceCollection services)
{
//全局添加AuthorizeFilter 过滤器方式
services.AddControllers(options=>options.Filters.Add(new AuthorizeFilter())); services.AddAuthorization();
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = "http://localhost:5000"; //配置Identityserver的授权地址
options.RequireHttpsMetadata = false; //不需要https
options.ApiName = OAuthConfig.UserApi.ApiName; //api的name,需要和config的名称相同
});
}

全局通过添加AuthorizeFilter过滤器方式进行全局api资源的限制

AuthorizeAttribute

先来看看AuthorizeAttribute源代码:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class AuthorizeAttribute : Attribute, IAuthorizeData
{
/// <summary>
/// Initializes a new instance of the <see cref="AuthorizeAttribute"/> class.
/// </summary>
public AuthorizeAttribute() { } /// <summary>
/// Initializes a new instance of the <see cref="AuthorizeAttribute"/> class with the specified policy.
/// </summary>
/// <param name="policy">The name of the policy to require for authorization.</param>
public AuthorizeAttribute(string policy)
{
Policy = policy;
} /// <summary>
/// 收取策略
/// </summary>
public string Policy { get; set; } /// <summary>
/// 授权角色
/// </summary>
public string Roles { get; set; } /// <summary>
/// 授权Schemes
/// </summary>
public string AuthenticationSchemes { get; set; }
}

代码中可以看到AuthorizeAttribute继承了IAuthorizeData抽象接口,该接口主要是授权数据的约束定义,定义了三个数据属性

  • Prolicy :授权策略
  • Roles : 授权角色
  • AuthenticationSchemes :授权Schemes 的支持

    Asp.Net Core 中的http中间件会根据IAuthorizeData这个来获取有哪些授权过滤器,来实现过滤器的拦截并执行相关代码。

    我们看看AuthorizeAttribute代码如下:
public interface IAuthorizeData
{
/// <summary>
/// Gets or sets the policy name that determines access to the resource.
/// </summary>
string Policy { get; set; } /// <summary>
/// Gets or sets a comma delimited list of roles that are allowed to access the resource.
/// </summary>
string Roles { get; set; } /// <summary>
/// Gets or sets a comma delimited list of schemes from which user information is constructed.
/// </summary>
string AuthenticationSchemes { get; set; }
}

我们再来看看授权中间件UseAuthorization)的核心代码:

public static IApplicationBuilder UseAuthorization(this IApplicationBuilder app)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
} VerifyServicesRegistered(app); return app.UseMiddleware<AuthorizationMiddleware>();
}

代码中注册了AuthorizationMiddleware这个中间件,AuthorizationMiddleware中间件源代码如下:

 public class AuthorizationMiddleware
{
// Property key is used by Endpoint routing to determine if Authorization has run
private const string AuthorizationMiddlewareInvokedWithEndpointKey = "__AuthorizationMiddlewareWithEndpointInvoked";
private static readonly object AuthorizationMiddlewareWithEndpointInvokedValue = new object(); private readonly RequestDelegate _next;
private readonly IAuthorizationPolicyProvider _policyProvider; public AuthorizationMiddleware(RequestDelegate next, IAuthorizationPolicyProvider policyProvider)
{
_next = next ?? throw new ArgumentNullException(nameof(next));
_policyProvider = policyProvider ?? throw new ArgumentNullException(nameof(policyProvider));
} public async Task Invoke(HttpContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
} var endpoint = context.GetEndpoint(); if (endpoint != null)
{
// EndpointRoutingMiddleware uses this flag to check if the Authorization middleware processed auth metadata on the endpoint.
// The Authorization middleware can only make this claim if it observes an actual endpoint.
context.Items[AuthorizationMiddlewareInvokedWithEndpointKey] = AuthorizationMiddlewareWithEndpointInvokedValue;
} // 通过终结点路由元素IAuthorizeData来获得对于的AuthorizeAttribute并关联到AuthorizeFilter中
var authorizeData = endpoint?.Metadata.GetOrderedMetadata<IAuthorizeData>() ?? Array.Empty<IAuthorizeData>();
var policy = await AuthorizationPolicy.CombineAsync(_policyProvider, authorizeData);
if (policy == null)
{
await _next(context);
return;
} // Policy evaluator has transient lifetime so it fetched from request services instead of injecting in constructor
var policyEvaluator = context.RequestServices.GetRequiredService<IPolicyEvaluator>(); var authenticateResult = await policyEvaluator.AuthenticateAsync(policy, context); // Allow Anonymous skips all authorization
if (endpoint?.Metadata.GetMetadata<IAllowAnonymous>() != null)
{
await _next(context);
return;
} // Note that the resource will be null if there is no matched endpoint
var authorizeResult = await policyEvaluator.AuthorizeAsync(policy, authenticateResult, context, resource: endpoint); if (authorizeResult.Challenged)
{
if (policy.AuthenticationSchemes.Any())
{
foreach (var scheme in policy.AuthenticationSchemes)
{
await context.ChallengeAsync(scheme);
}
}
else
{
await context.ChallengeAsync();
} return;
}
else if (authorizeResult.Forbidden)
{
if (policy.AuthenticationSchemes.Any())
{
foreach (var scheme in policy.AuthenticationSchemes)
{
await context.ForbidAsync(scheme);
}
}
else
{
await context.ForbidAsync();
} return;
} await _next(context);
}
}

代码中核心拦截并获得AuthorizeFilter过滤器的代码

var authorizeData = endpoint?.Metadata.GetOrderedMetadata<IAuthorizeData>() ?? Array.Empty<IAuthorizeData>();

前面我分享过一篇关于 Asp.Net Core EndPoint 终结点路由工作原理解读 的文章里面讲解到通过EndPoint终结点路由来获取ControllerAction中的Attribute特性标注,这里也是通过该方法来拦截获取对于的AuthorizeAttribute的.

而获取到相关authorizeData授权数据后,下面的一系列代码都是通过判断来进行AuthorizeAsync授权执行的方法,这里就不详细分享它的授权认证的过程了。

细心的同学应该已经发现上面的代码有一个比较特殊的代码:

if (endpoint?.Metadata.GetMetadata<IAllowAnonymous>() != null)
{
await _next(context);
return;
}

代码中通过endpoint终结点路由来获取是否标注有AllowAnonymous的特性,如果有则直接执行下一个中间件,不进行下面的AuthorizeAsync授权认证方法,

这也是为什么ControllerAction上标注AllowAnonymous可以跳过授权认证的原因了。

AuthorizeFilter 源码

有的人会问AuthorizeAttirbuteAuthorizeFilter有什么关系呢?它们是一个东西吗?

我们再来看看AuthorizeFilter源代码,代码如下:

public class AuthorizeFilter : IAsyncAuthorizationFilter, IFilterFactory
{
/// <summary>
/// Initializes a new <see cref="AuthorizeFilter"/> instance.
/// </summary>
public AuthorizeFilter()
: this(authorizeData: new[] { new AuthorizeAttribute() })
{
} /// <summary>
/// Initialize a new <see cref="AuthorizeFilter"/> instance.
/// </summary>
/// <param name="policy">Authorization policy to be used.</param>
public AuthorizeFilter(AuthorizationPolicy policy)
{
if (policy == null)
{
throw new ArgumentNullException(nameof(policy));
} Policy = policy;
} /// <summary>
/// Initialize a new <see cref="AuthorizeFilter"/> instance.
/// </summary>
/// <param name="policyProvider">The <see cref="IAuthorizationPolicyProvider"/> to use to resolve policy names.</param>
/// <param name="authorizeData">The <see cref="IAuthorizeData"/> to combine into an <see cref="IAuthorizeData"/>.</param>
public AuthorizeFilter(IAuthorizationPolicyProvider policyProvider, IEnumerable<IAuthorizeData> authorizeData)
: this(authorizeData)
{
if (policyProvider == null)
{
throw new ArgumentNullException(nameof(policyProvider));
} PolicyProvider = policyProvider;
} /// <summary>
/// Initializes a new instance of <see cref="AuthorizeFilter"/>.
/// </summary>
/// <param name="authorizeData">The <see cref="IAuthorizeData"/> to combine into an <see cref="IAuthorizeData"/>.</param>
public AuthorizeFilter(IEnumerable<IAuthorizeData> authorizeData)
{
if (authorizeData == null)
{
throw new ArgumentNullException(nameof(authorizeData));
} AuthorizeData = authorizeData;
} /// <summary>
/// Initializes a new instance of <see cref="AuthorizeFilter"/>.
/// </summary>
/// <param name="policy">The name of the policy to require for authorization.</param>
public AuthorizeFilter(string policy)
: this(new[] { new AuthorizeAttribute(policy) })
{
} /// <summary>
/// The <see cref="IAuthorizationPolicyProvider"/> to use to resolve policy names.
/// </summary>
public IAuthorizationPolicyProvider PolicyProvider { get; } /// <summary>
/// The <see cref="IAuthorizeData"/> to combine into an <see cref="IAuthorizeData"/>.
/// </summary>
public IEnumerable<IAuthorizeData> AuthorizeData { get; } /// <summary>
/// Gets the authorization policy to be used.
/// </summary>
/// <remarks>
/// If<c>null</c>, the policy will be constructed using
/// <see cref="AuthorizationPolicy.CombineAsync(IAuthorizationPolicyProvider, IEnumerable{IAuthorizeData})"/>.
/// </remarks>
public AuthorizationPolicy Policy { get; } bool IFilterFactory.IsReusable => true; // Computes the actual policy for this filter using either Policy or PolicyProvider + AuthorizeData
private Task<AuthorizationPolicy> ComputePolicyAsync()
{
if (Policy != null)
{
return Task.FromResult(Policy);
} if (PolicyProvider == null)
{
throw new InvalidOperationException(
Resources.FormatAuthorizeFilter_AuthorizationPolicyCannotBeCreated(
nameof(AuthorizationPolicy),
nameof(IAuthorizationPolicyProvider)));
} return AuthorizationPolicy.CombineAsync(PolicyProvider, AuthorizeData);
} internal async Task<AuthorizationPolicy> GetEffectivePolicyAsync(AuthorizationFilterContext context)
{
// Combine all authorize filters into single effective policy that's only run on the closest filter
var builder = new AuthorizationPolicyBuilder(await ComputePolicyAsync());
for (var i = 0; i < context.Filters.Count; i++)
{
if (ReferenceEquals(this, context.Filters[i]))
{
continue;
} if (context.Filters[i] is AuthorizeFilter authorizeFilter)
{
// Combine using the explicit policy, or the dynamic policy provider
builder.Combine(await authorizeFilter.ComputePolicyAsync());
}
} var endpoint = context.HttpContext.GetEndpoint();
if (endpoint != null)
{
// When doing endpoint routing, MVC does not create filters for any authorization specific metadata i.e [Authorize] does not
// get translated into AuthorizeFilter. Consequently, there are some rough edges when an application uses a mix of AuthorizeFilter
// explicilty configured by the user (e.g. global auth filter), and uses endpoint metadata.
// To keep the behavior of AuthFilter identical to pre-endpoint routing, we will gather auth data from endpoint metadata
// and produce a policy using this. This would mean we would have effectively run some auth twice, but it maintains compat.
var policyProvider = PolicyProvider ?? context.HttpContext.RequestServices.GetRequiredService<IAuthorizationPolicyProvider>();
var endpointAuthorizeData = endpoint.Metadata.GetOrderedMetadata<IAuthorizeData>() ?? Array.Empty<IAuthorizeData>(); var endpointPolicy = await AuthorizationPolicy.CombineAsync(policyProvider, endpointAuthorizeData);
if (endpointPolicy != null)
{
builder.Combine(endpointPolicy);
}
} return builder.Build();
} /// <inheritdoc />
public virtual async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
} if (!context.IsEffectivePolicy(this))
{
return;
} // IMPORTANT: Changes to authorization logic should be mirrored in security's AuthorizationMiddleware
var effectivePolicy = await GetEffectivePolicyAsync(context);
if (effectivePolicy == null)
{
return;
} var policyEvaluator = context.HttpContext.RequestServices.GetRequiredService<IPolicyEvaluator>(); var authenticateResult = await policyEvaluator.AuthenticateAsync(effectivePolicy, context.HttpContext); // Allow Anonymous skips all authorization
if (HasAllowAnonymous(context))
{
return;
} var authorizeResult = await policyEvaluator.AuthorizeAsync(effectivePolicy, authenticateResult, context.HttpContext, context); if (authorizeResult.Challenged)
{
context.Result = new ChallengeResult(effectivePolicy.AuthenticationSchemes.ToArray());
}
else if (authorizeResult.Forbidden)
{
context.Result = new ForbidResult(effectivePolicy.AuthenticationSchemes.ToArray());
}
} IFilterMetadata IFilterFactory.CreateInstance(IServiceProvider serviceProvider)
{
if (Policy != null || PolicyProvider != null)
{
// The filter is fully constructed. Use the current instance to authorize.
return this;
} Debug.Assert(AuthorizeData != null);
var policyProvider = serviceProvider.GetRequiredService<IAuthorizationPolicyProvider>();
return AuthorizationApplicationModelProvider.GetFilter(policyProvider, AuthorizeData);
} private static bool HasAllowAnonymous(AuthorizationFilterContext context)
{
var filters = context.Filters;
for (var i = 0; i < filters.Count; i++)
{
if (filters[i] is IAllowAnonymousFilter)
{
return true;
}
} // When doing endpoint routing, MVC does not add AllowAnonymousFilters for AllowAnonymousAttributes that
// were discovered on controllers and actions. To maintain compat with 2.x,
// we'll check for the presence of IAllowAnonymous in endpoint metadata.
var endpoint = context.HttpContext.GetEndpoint();
if (endpoint?.Metadata?.GetMetadata<IAllowAnonymous>() != null)
{
return true;
} return false;
}
}

代码中继承了 IAsyncAuthorizationFilter, IFilterFactory两个抽象接口,分别来看看这两个抽象接口的源代码

IAsyncAuthorizationFilter源代码如下:
/// <summary>
/// A filter that asynchronously confirms request authorization.
/// </summary>
public interface IAsyncAuthorizationFilter : IFilterMetadata
{
///定义了授权的方法
Task OnAuthorizationAsync(AuthorizationFilterContext context);
}

IAsyncAuthorizationFilter代码中继承了IFilterMetadata接口,同时定义了OnAuthorizationAsync抽象方法,子类需要实现该方法,然而AuthorizeFilter中也已经实现了该方法,稍后再来详细讲解该方法,我们再继续看看IFilterFactory抽象接口,代码如下:

public interface IFilterFactory : IFilterMetadata
{ bool IsReusable { get; } //创建IFilterMetadata 对象方法
IFilterMetadata CreateInstance(IServiceProvider serviceProvider);
}

我们回到AuthorizeFilter 源代码中,该源代码中提供了四个构造初始化方法同时包含了AuthorizeDataPolicy属性,我们看看它的默认构造方法代码

public class AuthorizeFilter : IAsyncAuthorizationFilter, IFilterFactory
{
public IEnumerable<IAuthorizeData> AuthorizeData { get; } //默认构造函数中默认创建了AuthorizeAttribute 对象
public AuthorizeFilter()
: this(authorizeData: new[] { new AuthorizeAttribute() })
{
} //赋值AuthorizeData
public AuthorizeFilter(IEnumerable<IAuthorizeData> authorizeData)
{
if (authorizeData == null)
{
throw new ArgumentNullException(nameof(authorizeData));
} AuthorizeData = authorizeData;
}
}

上面的代码中默认的构造函数默认给构建了一个AuthorizeAttribute对象,并且赋值给了IEnumerable<IAuthorizeData>的集合属性;

好了,看到这里AuthorizeFilter过滤器也是默认构造了一个AuthorizeAttribute的对象,也就是构造了授权所需要的IAuthorizeData信息.

同时AuthorizeFilter实现的OnAuthorizationAsync方法中通过GetEffectivePolicyAsync这个方法获得有效的授权策略,并且进行下面的授权AuthenticateAsync的执行

AuthorizeFilter代码中提供了HasAllowAnonymous方法来实现是否Controller或者Action上标注了AllowAnonymous特性,用于跳过授权

HasAllowAnonymous代码如下:

private static bool HasAllowAnonymous(AuthorizationFilterContext context)
{
var filters = context.Filters;
for (var i = 0; i < filters.Count; i++)
{
if (filters[i] is IAllowAnonymousFilter)
{
return true;
}
}
//同样通过上下文的endpoint 来获取是否标注了AllowAnonymous特性
var endpoint = context.HttpContext.GetEndpoint();
if (endpoint?.Metadata?.GetMetadata<IAllowAnonymous>() != null)
{
return true;
} return false;
}

到这里我们再回到全局添加过滤器的方式代码:

 services.AddControllers(options=>options.Filters.Add(new AuthorizeFilter()));

分析到这里 ,我很是好奇,它是怎么全局添加进去的呢?我打开源代码看了下,源代码如下:

public class MvcOptions : IEnumerable<ICompatibilitySwitch>
{ public MvcOptions()
{
CacheProfiles = new Dictionary<string, CacheProfile>(StringComparer.OrdinalIgnoreCase);
Conventions = new List<IApplicationModelConvention>();
Filters = new FilterCollection();
FormatterMappings = new FormatterMappings();
InputFormatters = new FormatterCollection<IInputFormatter>();
OutputFormatters = new FormatterCollection<IOutputFormatter>();
ModelBinderProviders = new List<IModelBinderProvider>();
ModelBindingMessageProvider = new DefaultModelBindingMessageProvider();
ModelMetadataDetailsProviders = new List<IMetadataDetailsProvider>();
ModelValidatorProviders = new List<IModelValidatorProvider>();
ValueProviderFactories = new List<IValueProviderFactory>();
} //过滤器集合
public FilterCollection Filters { get; }
}

FilterCollection相关核心代码如下:

public class FilterCollection : Collection<IFilterMetadata>
{ public IFilterMetadata Add<TFilterType>() where TFilterType : IFilterMetadata
{
return Add(typeof(TFilterType));
} //其他核心代码为贴出来
}

代码中提供了Add方法,约束了IFilterMetadata类型的对象,这也是上面的过滤器中为什么都继承了IFilterMetadata的原因。

到这里代码解读和实现原理已经分析完了,如果有分析不到位之处还请多多指教!!!

结论:授权中间件通过获取IAuthorizeData来获取AuthorizeAttribute对象相关的授权信息,并构造授权策略对象进行授权认证的,而AuthorizeFilter过滤器也会默认添加AuthorizeAttribute的授权相关数据IAuthorizeData并实现OnAuthorizationAsync方法,同时中间件中通过授权策略提供者IAuthorizationPolicyProvider来获得对于的授权策略进行授权认证.

Asp.Net Core AuthorizeAttribute 和AuthorizeFilter 跟进及源码解读的更多相关文章

  1. ASP.Net Core MVC6 RC2 启动过程分析[偏源码分析]

    入口程序 如果做过Web之外开发的人,应该记得这个是标准的Console或者Winform的入口.为什么会这样呢? .NET Web Development and Tools Blog ASP.NE ...

  2. asp.net core结合docker实现自动化获取源码、部署、更新

    之前入坑dotnet core,由于一开始就遇到在windows上编译发布的web无法直接放到centos上执行.之后便直接研究docker,实现在容器中编译发布.然后就越玩越大,后来利用git的ho ...

  3. ASP.NET Core错误处理中间件[4]: 响应状态码页面

    StatusCodePagesMiddleware中间件与ExceptionHandlerMiddleware中间件类似,它们都是在后续请求处理过程中"出错"的情况下利用一个错误处 ...

  4. ASP.NET程序读取二代身份证(附源码)

    原文:ASP.NET程序读取二代身份证(附源码) 一般来说winform应用程序解决这个问题起来时很容易的,web应用程序就麻烦一点了. 这里我说说我的解决思路: 一.你必要有联机型居民身份证阅读器一 ...

  5. winserver的consul部署实践与.net core客户端使用(附demo源码)

    winserver的consul部署实践与.net core客户端使用(附demo源码)   前言 随着微服务兴起,服务的管理显得极其重要.都知道微服务就是”拆“,把臃肿的单块应用,拆分成多个轻量级的 ...

  6. Asp.net MVC - 使用PRG模式(附源码)

    阅读目录: 一. 传统的Asp.net页面问题 二.Asp.net MVC中也存在同样的问题 三.使用PRG模式 四.PRG模式在MVC上的实现 一. 传统的Asp.net页面问题 一个传统的Asp. ...

  7. ASP.NET MVC通用权限管理系统(响应布局)源码更新介绍

    一.asp.net mvc 通用权限管理系统(响应布局)源码主要以下特点: AngelRM(Asp.net MVC)是基于asp.net(C#)MVC+前端bootstrap+ztree+lodash ...

  8. ASP.NET Core Web 支付功能接入 微信-扫码支付篇

    这篇文章将介绍ASP.NET Core中使用 开源项目 Payment,实现接入微信-扫码支付及异步通知功能. 开发环境:Win 10 x64.VS2017 15.6.4..NET Core SDK ...

  9. 【转载】ASP.NET Core Web 支付功能接入 微信-扫码支付篇

    转自:http://www.cnblogs.com/essenroc/p/8630730.html 这篇文章将介绍ASP.NET Core中使用 开源项目 Payment,实现接入微信-扫码支付及异步 ...

随机推荐

  1. php 克隆 clone

    php 克隆 clone 在实际编程过程中,我们常常要遇到这种情况:有一个对象A,在某一时刻A中已经包含了一些有效值,此时可能会需要一个和A完全相同新对象B,并且此后对B任何改动都不会影响到A中的值, ...

  2. 对Java tutorial-examples中hello2核心代码分析

    1.在hello2中有两个.java源文件分别是GreetingServlet.Java和ResponseServlet.jva文件主要对以下核心代码做主要分析. String username = ...

  3. linux查看端口号占用命令-top

    题记 对于top命令来说,基本是都是linux命令入门中,第一个使用的命令,在windows中电脑如果卡顿,回去任务栏中查看cpu和内存的使用情况.top命令实现的就是这个重要的功能. 在系统维护的过 ...

  4. 获取网站title的脚本

    脚本在此 公司的商城需要添加一个脚本,这个脚本就是观察首页页面是否正常,虽然已经配置了zabbix监控网站是否200,但是有一些特殊的情况,比如网页可以打开但是页面是"file not fo ...

  5. Ubuntu日常使用总结

    Contents 使用了将近一年的Ubuntu,感觉不用windows也可以处理日常的事务.并且我相信只要合理利用Ubuntu,一定可以取代你手中的Windows.我不是说Ubuntu有多么好,只是从 ...

  6. Ubuntu 14.10 进入单用户模式

    1. 开机,进入grub界面 2. 此时会有一个选项:Advanced Options for Ubuntu(ubuntu高级), 选中直接回车 3. 看到里面有很多选项,选中后面带recovery ...

  7. Python——12类的继承

    */ * Copyright (c) 2016,烟台大学计算机与控制工程学院 * All rights reserved. * 文件名:text.cpp * 作者:常轩 * 微信公众号:Worldhe ...

  8. 编写一个可复用的SpringBoot应用运维脚本

    前提 作为Java开发者,很多场景下会使用SpringBoot开发Web应用,目前微服务主流SpringCloud全家桶也是基于SpringBoot搭建的.SpringBoot应用部署到服务器上,需要 ...

  9. 组件化思路:以selection为例子,使用prop-types实现组件化控制,重用

    需求 书接上文,UI 积累之select section 这里又来两个需求了. 当我点击选择了option后,我应该看到的是我选择的option的内容 多例重用,即同样是个selection,我只是需 ...

  10. A. Reorder the Array

    You are given an array of integers. Vasya can permute (change order) its integers. He wants to do it ...