• 重新整理 .net core 实践篇——— 权限源码阅读四十五]


    前言

    简单介绍一下权限源码阅读一下。

    正文

    一直有人对授权这个事情上争论不休,有的人认为在输入账户密码给后台这个时候进行了授权,因为认为发送了一个身份令牌,令牌里面可能有些用户角色信息,认为这就是授权,有的人认为这只是获取令牌的过程。

    现实生活中有一个是授权证书,那么有人认为token 是授权证书,但这只是颁发证书。账户密码获取获取身份令牌也不是认证,认证是证明你的身份令牌有效的过程。

    那么netcore 中是如何解释授权的:

    授权是指确定用户可执行的操作的过程。故而实际上,获取身份令牌只是获取令牌,授权是指在访问过程中,确认是否可以访问的过程。身份令牌中有角色,有些是根据角色还确定是否可以访问的,这就是授权了。

    不过随着业务的复杂,网关可以根据角色授权接口,也可以根据自己的策略了,授权的过程五花八门的。

    在网关中一般有认证和授权两部分,先认证再授权,先确定合法身份在来确定一下授权。

    先来看下认证吧,有些人一认证就想到了jwt,或者想到了具体的认证方式,其实认证就是你的系统认为它符合了合法身份,和具体的东西没有关系,是一个抽象的概念。

    app.UseAuthentication();
    

    通过上面这个看下认证过程。

    然后看下具体的中间件。

    public AuthenticationMiddleware(RequestDelegate next, IAuthenticationSchemeProvider schemes)
    {
    	if (next == null)
    	{
    		throw new ArgumentNullException(nameof(next));
    	}
    	if (schemes == null)
    	{
    		throw new ArgumentNullException(nameof(schemes));
    	}
    
    	_next = next;
    	Schemes = schemes;
    }
    

    从这里看呢,IAuthenticationSchemeProvider 提供了认证解决方案,可以看下这个接口。

    /// <summary>
    /// Responsible for managing what authenticationSchemes are supported.
    /// </summary>
    public interface IAuthenticationSchemeProvider
    {
    	/// <summary>
    	/// Returns all currently registered <see cref="AuthenticationScheme"/>s.
    	/// </summary>
    	/// <returns>All currently registered <see cref="AuthenticationScheme"/>s.</returns>
    	Task<IEnumerable<AuthenticationScheme>> GetAllSchemesAsync();
    
    	/// <summary>
    	/// Returns the <see cref="AuthenticationScheme"/> matching the name, or null.
    	/// </summary>
    	/// <param name="name">The name of the authenticationScheme.</param>
    	/// <returns>The scheme or null if not found.</returns>
    	Task<AuthenticationScheme?> GetSchemeAsync(string name);
    
    	/// <summary>
    	/// Returns the scheme that will be used by default for <see cref="IAuthenticationService.AuthenticateAsync(HttpContext, string)"/>.
    	/// This is typically specified via <see cref="AuthenticationOptions.DefaultAuthenticateScheme"/>.
    	/// Otherwise, this will fallback to <see cref="AuthenticationOptions.DefaultScheme"/>.
    	/// </summary>
    	/// <returns>The scheme that will be used by default for <see cref="IAuthenticationService.AuthenticateAsync(HttpContext, string)"/>.</returns>
    	Task<AuthenticationScheme?> GetDefaultAuthenticateSchemeAsync();
    
    	/// <summary>
    	/// Returns the scheme that will be used by default for <see cref="IAuthenticationService.ChallengeAsync(HttpContext, string, AuthenticationProperties)"/>.
    	/// This is typically specified via <see cref="AuthenticationOptions.DefaultChallengeScheme"/>.
    	/// Otherwise, this will fallback to <see cref="AuthenticationOptions.DefaultScheme"/>.
    	/// </summary>
    	/// <returns>The scheme that will be used by default for <see cref="IAuthenticationService.ChallengeAsync(HttpContext, string, AuthenticationProperties)"/>.</returns>
    	Task<AuthenticationScheme?> GetDefaultChallengeSchemeAsync();
    
    	/// <summary>
    	/// Returns the scheme that will be used by default for <see cref="IAuthenticationService.ForbidAsync(HttpContext, string, AuthenticationProperties)"/>.
    	/// This is typically specified via <see cref="AuthenticationOptions.DefaultForbidScheme"/>.
    	/// Otherwise, this will fallback to <see cref="GetDefaultChallengeSchemeAsync"/> .
    	/// </summary>
    	/// <returns>The scheme that will be used by default for <see cref="IAuthenticationService.ForbidAsync(HttpContext, string, AuthenticationProperties)"/>.</returns>
    	Task<AuthenticationScheme?> GetDefaultForbidSchemeAsync();
    
    	/// <summary>
    	/// Returns the scheme that will be used by default for <see cref="IAuthenticationService.SignInAsync(HttpContext, string, System.Security.Claims.ClaimsPrincipal, AuthenticationProperties)"/>.
    	/// This is typically specified via <see cref="AuthenticationOptions.DefaultSignInScheme"/>.
    	/// Otherwise, this will fallback to <see cref="AuthenticationOptions.DefaultScheme"/>.
    	/// </summary>
    	/// <returns>The scheme that will be used by default for <see cref="IAuthenticationService.SignInAsync(HttpContext, string, System.Security.Claims.ClaimsPrincipal, AuthenticationProperties)"/>.</returns>
    	Task<AuthenticationScheme?> GetDefaultSignInSchemeAsync();
    
    	/// <summary>
    	/// Returns the scheme that will be used by default for <see cref="IAuthenticationService.SignOutAsync(HttpContext, string, AuthenticationProperties)"/>.
    	/// This is typically specified via <see cref="AuthenticationOptions.DefaultSignOutScheme"/>.
    	/// Otherwise, this will fallback to <see cref="GetDefaultSignInSchemeAsync"/> .
    	/// </summary>
    	/// <returns>The scheme that will be used by default for <see cref="IAuthenticationService.SignOutAsync(HttpContext, string, AuthenticationProperties)"/>.</returns>
    	Task<AuthenticationScheme?> GetDefaultSignOutSchemeAsync();
    
    	/// <summary>
    	/// Registers a scheme for use by <see cref="IAuthenticationService"/>. 
    	/// </summary>
    	/// <param name="scheme">The scheme.</param>
    	void AddScheme(AuthenticationScheme scheme);
    
    	/// <summary>
    	/// Registers a scheme for use by <see cref="IAuthenticationService"/>. 
    	/// </summary>
    	/// <param name="scheme">The scheme.</param>
    	/// <returns>true if the scheme was added successfully.</returns>
    	bool TryAddScheme(AuthenticationScheme scheme)
    	{
    		try
    		{
    			AddScheme(scheme);
    			return true;
    		}
    		catch {
    			return false;
    		}
    	}
    
    	/// <summary>
    	/// Removes a scheme, preventing it from being used by <see cref="IAuthenticationService"/>.
    	/// </summary>
    	/// <param name="name">The name of the authenticationScheme being removed.</param>
    	void RemoveScheme(string name);
    
    	/// <summary>
    	/// Returns the schemes in priority order for request handling.
    	/// </summary>
    	/// <returns>The schemes in priority order for request handling</returns>
    	Task<IEnumerable<AuthenticationScheme>> GetRequestHandlerSchemesAsync();
    }
    

    虽然没有看到具体的provider,但是呢,可以通过接口注释看个大概哈。

    比如说AuthenticationScheme 是认证方案的意思,从英文表面理解哈。然后里面有方法增删改查,意味着我们可以有多种认证方式。

    这其实是刚需,因为比如以前颁发的身份令牌和现在接口颁发的身份令牌不一样了,那么为了无缝衔接,可以认可两种认证方式。

    那么看下AuthenticationScheme 认证方案里面有些啥吧。

    /// <summary>
    /// AuthenticationSchemes assign a name to a specific <see cref="IAuthenticationHandler"/>
    /// handlerType.
    /// </summary>
    public class AuthenticationScheme
    {
    	/// <summary>
    	/// Initializes a new instance of <see cref="AuthenticationScheme"/>.
    	/// </summary>
    	/// <param name="name">The name for the authentication scheme.</param>
    	/// <param name="displayName">The display name for the authentication scheme.</param>
    	/// <param name="handlerType">The <see cref="IAuthenticationHandler"/> type that handles this scheme.</param>
    	public AuthenticationScheme(string name, string? displayName, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type handlerType)
    	{
    		if (name == null)
    		{
    			throw new ArgumentNullException(nameof(name));
    		}
    		if (handlerType == null)
    		{
    			throw new ArgumentNullException(nameof(handlerType));
    		}
    		if (!typeof(IAuthenticationHandler).IsAssignableFrom(handlerType))
    		{
    			throw new ArgumentException("handlerType must implement IAuthenticationHandler.");
    		}
    
    		Name = name;
    		HandlerType = handlerType;
    		DisplayName = displayName;
    	}
    
    	/// <summary>
    	/// The name of the authentication scheme.
    	/// </summary>
    	public string Name { get; }
    
    	/// <summary>
    	/// The display name for the scheme. Null is valid and used for non user facing schemes.
    	/// </summary>
    	public string? DisplayName { get; }
    
    	/// <summary>
    	/// The <see cref="IAuthenticationHandler"/> type that handles this scheme.
    	/// </summary>
    	[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
    	public Type HandlerType { get; }
    }
    

    这里面有name 和displayname,一个是名称,一个是显示名称,相信很多人都见到过这样的类,里面有name 还有 displayname。

    不要那么计较,显示名称是为了好大家好而已。比如说我们的sex 表示性别,那么displayname 可以写显示名称。

    比如说你的一个计划类,里面可以有name 和 displayname。name 是JC159,displayname 是瞎扯计划,JC159 多难理解啊,瞎扯计划多好理解,瞎扯啊。

    然后里面有一个是HandlerType,叫做处理类型,处理认证计划的类型。上面有注释,这个类型继承IAuthenticationHandler这个接口,那么也就是这个方案将由实现IAuthenticationHandler的类来处理,具体看实际的处理方案,比如jwt。

    然后看下中间件的invoke。

    /// <summary>
    /// Invokes the middleware performing authentication.
    /// </summary>
    /// <param name="context">The <see cref="HttpContext"/>.</param>
    public async Task Invoke(HttpContext context)
    {
    	context.Features.Set<IAuthenticationFeature>(new AuthenticationFeature
    	{
    		OriginalPath = context.Request.Path,
    		OriginalPathBase = context.Request.PathBase
    	});
    
    	// Give any IAuthenticationRequestHandler schemes a chance to handle the request
    	var handlers = context.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>();
    	foreach (var scheme in await Schemes.GetRequestHandlerSchemesAsync())
    	{
    		var handler = await handlers.GetHandlerAsync(context, scheme.Name) as IAuthenticationRequestHandler;
    		if (handler != null && await handler.HandleRequestAsync())
    		{
    			return;
    		}
    	}
    
    	var defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync();
    	if (defaultAuthenticate != null)
    	{
    		var result = await context.AuthenticateAsync(defaultAuthenticate.Name);
    		if (result?.Principal != null)
    		{
    			context.User = result.Principal;
    		}
    	}
    
    	await _next(context);
    }
    

    一段一段看吧。

    context.Features.Set<IAuthenticationFeature>(new AuthenticationFeature
    {
    	OriginalPath = context.Request.Path,
    	OriginalPathBase = context.Request.PathBase
    });
    

    Features 是经过一个集合,比如我们经过中间件,我们可以向里面写入一些东西,然后供下一个中间件使用都行,有点像是游戏里面背包的功能。

    var handlers = context.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>();
    foreach (var scheme in await Schemes.GetRequestHandlerSchemesAsync())
    {
    	var handler = await handlers.GetHandlerAsync(context, scheme.Name) as IAuthenticationRequestHandler;
    	if (handler != null && await handler.HandleRequestAsync())
    	{
    		return;
    	}
    }
    

    这里面就是获取就是获取相应的认真方案处理器,然后进行执行HandleRequestAsync。

    值得主意的是,这里面并不是执行IAuthenticationHandler的方法,而是IAuthenticationRequestHandler的方法。

    所以这并不意味这我们的写入的每个方案都必须通过,而是如果我们的写入的每个认证方案继承IAuthenticationRequestHandler,那么必须通过其中的HandleRequestAsync方法。

    然后看下IAuthenticationRequestHandler 这个哈。

    /// <summary>
    /// Used to determine if a handler wants to participate in request processing.
    /// </summary>
    public interface IAuthenticationRequestHandler : IAuthenticationHandler
    {
    	/// <summary>
    	/// Gets a value that determines if the request should stop being processed.
    	/// <para>
    	/// This feature is supported by the Authentication middleware
    	/// which does not invoke any subsequent <see cref="IAuthenticationHandler"/> or middleware configured in the request pipeline
    	/// if the handler returns <see langword="true" />.
    	/// </para>
    	/// </summary>
    	/// <returns><see langword="true" /> if request processing should stop.</returns>
    	Task<bool> HandleRequestAsync();
    }
    

    继续往下看:

    var defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync();
    if (defaultAuthenticate != null)
    {
    	var result = await context.AuthenticateAsync(defaultAuthenticate.Name);
    	if (result?.Principal != null)
    	{
    		context.User = result.Principal;
    	}
    }
    

    继续往下看哈,然后里面也有这个哈,如果有默认的认证方案,那么context.User 会通过默认认证方案的处理器进行获取。也就是说如果我们设置了默认方案,那么就会通过默认方案来进行认证。

    await _next(context);
    

    这个表示继续往下执行了。

    那么来看下具体服务的认证吧,比如说jwt的。

    services.AddAuthentication("Bearer")
    	// 添加JwtBearer服务
     .AddJwtBearer(o =>
     {
    	 o.TokenValidationParameters = tokenValidationParameters;
    	 o.Events = new JwtBearerEvents
    	 {
    		 OnAuthenticationFailed = context =>
    		 {
    			 // 如果过期,则把<是否过期>添加到,返回头信息中
    			 if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
    			 {
    				 context.Response.Headers.Add("Token-Expired", "true");
    			 }
    			 return Task.CompletedTask;
    		 }
    	 };
     });
    

    首先来看一下:

    services.AddAuthentication("Bearer")
    

    这里面就是设置默认的认证方案:

    public static AuthenticationBuilder AddAuthentication(this IServiceCollection services, string defaultScheme)
    	=> services.AddAuthentication(o => o.DefaultScheme = defaultScheme);
    
    public static AuthenticationBuilder AddAuthentication(this IServiceCollection services, Action<AuthenticationOptions> configureOptions) {
    	if (services == null)
    	{
    		throw new ArgumentNullException(nameof(services));
    	}
    
    	if (configureOptions == null)
    	{
    		throw new ArgumentNullException(nameof(configureOptions));
    	}
    
    	var builder = services.AddAuthentication();
    	services.Configure(configureOptions);
    	return builder;
    }
    

    看一下:var builder = services.AddAuthentication();

    这个哈,这个才是具体增加具体服务的。

    public static AuthenticationBuilder AddAuthentication(this IServiceCollection services)
    {
    	if (services == null)
    	{
    		throw new ArgumentNullException(nameof(services));
    	}
    
    	services.AddAuthenticationCore();
    	services.AddDataProtection();
    	services.AddWebEncoders();
    	services.TryAddSingleton<ISystemClock, SystemClock>();
    	return new AuthenticationBuilder(services);
    }
    

    然后看下services.AddAuthenticationCore();,为什么看下这个呢?难道我提前看了这个东西吗?

    不是,因为我们知道分层的时候有个Core的层,是具体实现的,那么这种带core 一般就是具体实现方式了。

    public static IServiceCollection AddAuthenticationCore(this IServiceCollection services)
    {
    	if (services == null)
    	{
    		throw new ArgumentNullException(nameof(services));
    	}
    
    	services.TryAddScoped<IAuthenticationService, AuthenticationService>();
    	services.TryAddSingleton<IClaimsTransformation, NoopClaimsTransformation>(); // Can be replaced with scoped ones that use DbContext
    	services.TryAddScoped<IAuthenticationHandlerProvider, AuthenticationHandlerProvider>();
    	services.TryAddSingleton<IAuthenticationSchemeProvider, AuthenticationSchemeProvider>();
    	return services;
    }
    

    前面我们看了这个IAuthenticationHandlerProvider 和IAuthenticationSchemeProvider ,那么这里可以看到他们的具体实现是AuthenticationHandlerProvider和AuthenticationSchemeProvider。

    前面提及到会通过handletype来获取具体的处理器,那么来看下具体怎么实现的吧。

    /// <summary>
    /// Returns the handler instance that will be used.
    /// </summary>
    /// <param name="context">The context.</param>
    /// <param name="authenticationScheme">The name of the authentication scheme being handled.</param>
    /// <returns>The handler instance.</returns>
    public async Task<IAuthenticationHandler> GetHandlerAsync(HttpContext context, string authenticationScheme)
    {
    	if (_handlerMap.ContainsKey(authenticationScheme))
    	{
    		return _handlerMap[authenticationScheme];
    	}
    
    	var scheme = await Schemes.GetSchemeAsync(authenticationScheme);
    	if (scheme == null)
    	{
    		return null;
    	}
    	var handler = (context.RequestServices.GetService(scheme.HandlerType) ??
    		ActivatorUtilities.CreateInstance(context.RequestServices, scheme.HandlerType))
    		as IAuthenticationHandler;
    	if (handler != null)
    	{
    		await handler.InitializeAsync(scheme, context);
    		_handlerMap[authenticationScheme] = handler;
    	}
    	return handler;
    }
    

    看这个GetHandlerAsync,是通过依赖注入的方式来获取的,根据方案里面的GetHandlerAsync。

    那么从这里就能猜到jwt的具体实现了,那么直接来看吧。

    services.AddAuthentication("Bearer")
                    // 添加JwtBearer服务
                 .AddJwtBearer(o =>
                 {
                     o.TokenValidationParameters = tokenValidationParameters;
                     o.Events = new JwtBearerEvents
                     {
                         OnAuthenticationFailed = context =>
                         {
                             // 如果过期,则把<是否过期>添加到,返回头信息中
                             if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
                             {
                                 context.Response.Headers.Add("Token-Expired", "true");
                             }
                             return Task.CompletedTask;
                         }
                     };
                 });
    
    

    其实不建议这么写的,应该是:

    直接标明这里使用的策略,之所以这个能够生效,是因为默认的是

    红框框部分是Bearer,但是不友好,对框架不熟,容易形成误导。

    继续往下看:

    public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<JwtBearerOptions> configureOptions)
    {
    	builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<JwtBearerOptions>, JwtBearerPostConfigureOptions>());
    	return builder.AddScheme<JwtBearerOptions, JwtBearerHandler>(authenticationScheme, displayName, configureOptions);
    }
    

    AddScheme 就是具体的注入了:

    /// <summary>
    /// Adds a <see cref="AuthenticationScheme"/> which can be used by <see cref="IAuthenticationService"/>.
    /// </summary>
    /// <typeparam name="TOptions">The <see cref="AuthenticationSchemeOptions"/> type to configure the handler."/>.</typeparam>
    /// <typeparam name="THandler">The <see cref="AuthenticationHandler{TOptions}"/> used to handle this scheme.</typeparam>
    /// <param name="authenticationScheme">The name of this scheme.</param>
    /// <param name="displayName">The display name of this scheme.</param>
    /// <param name="configureOptions">Used to configure the scheme options.</param>
    /// <returns>The builder.</returns>
    public virtual AuthenticationBuilder AddScheme<TOptions, THandler>(string authenticationScheme, string displayName, Action<TOptions> configureOptions)
    	where TOptions : AuthenticationSchemeOptions, new()
    	where THandler : AuthenticationHandler<TOptions>
    	=> AddSchemeHelper<TOptions, THandler>(authenticationScheme, displayName, configureOptions);
    

    然后加入到认证方案中去,JwtBearerOptions 就是这个方案的配置,JwtBearerHandler就是具体的处理,看下AddSchemeHelper。

    private AuthenticationBuilder AddSchemeHelper<TOptions, THandler>(string authenticationScheme, string displayName, Action<TOptions> configureOptions)
    	where TOptions : class, new()
    	where THandler : class, IAuthenticationHandler
    {
    	Services.Configure<AuthenticationOptions>(o =>
    	{
    		o.AddScheme(authenticationScheme, scheme => {
    			scheme.HandlerType = typeof(THandler);
    			scheme.DisplayName = displayName;
    		});
    	});
    	if (configureOptions != null)
    	{
    		Services.Configure(authenticationScheme, configureOptions);
    	}
    	Services.AddTransient<THandler>();
    	return this;
    }
    

    分步骤看下:

    Services.Configure<AuthenticationOptions>(o =>
    {
    	o.AddScheme(authenticationScheme, scheme => {
    		scheme.HandlerType = typeof(THandler);
    		scheme.DisplayName = displayName;
    	});
    });
    

    这一步就是添加具体的认证方案。

    if (configureOptions != null)
    {
          Services.Configure(authenticationScheme, configureOptions);
    }
    Services.AddTransient<THandler>();
    

    这一步就是注入配置文件,并且将处理器注入到ioc中,这里就是JwtBearerHandler了。

    JwtBearerHandler 就不看了,就是一些具体的实现,根据配置文件,然后处理,就属于jwt的知识了。

    补充

    这里扩容一下配置的知识,主要解释一下JwtBearerHandler 是如何根据不同的authenticationScheme 获取不同的配置的。

    Services.Configure(authenticationScheme, configureOptions);
    
    public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, Action<TOptions> configureOptions)
    	where TOptions : class
    {
    	if (services == null)
    	{
    		throw new ArgumentNullException(nameof(services));
    	}
    
    	if (configureOptions == null)
    	{
    		throw new ArgumentNullException(nameof(configureOptions));
    	}
    
    	services.AddOptions();
    	services.AddSingleton<IConfigureOptions<TOptions>>(new ConfigureNamedOptions<TOptions>(name, configureOptions));
    	return services;
    }
    

    看到吧,实际上获IConfigureOptions,会获取一组ConfigureNamedOptions,然后通过name筛选出来。

    看下JwtBearerHandler:

    public class JwtBearerHandler : AuthenticationHandler<JwtBearerOptions>
    public JwtBearerHandler(IOptionsMonitor<JwtBearerOptions> options, ILoggerFactory logger, UrlEncoder encoder, IDataProtectionProvider dataProtection, ISystemClock clock)
    	: base(options, logger, encoder, clock)
    { }
    

    将options 传给了AuthenticationHandler。

    那么看下AuthenticationHandler 中如何处理的吧。

    初始化的时候:

    /// <summary>
    /// Initialize the handler, resolve the options and validate them.
    /// </summary>
    /// <param name="scheme"></param>
    /// <param name="context"></param>
    /// <returns></returns>
    public async Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
    {
    	if (scheme == null)
    	{
    		throw new ArgumentNullException(nameof(scheme));
    	}
    	if (context == null)
    	{
    		throw new ArgumentNullException(nameof(context));
    	}
    
    	Scheme = scheme;
    	Context = context;
    
    	Options = OptionsMonitor.Get(Scheme.Name) ?? new TOptions();
    	Options.Validate(Scheme.Name);
    
    	await InitializeEventsAsync();
    	await InitializeHandlerAsync();
    }
    

    进行一波筛选而来的哈。

    下一节看下授权的源码吧。

  • 相关阅读:
    sqlserver 动态行转列
    c#指定日期格式
    The 14th Zhejiang Provincial Collegiate Programming Contest Sponsored by TuSimple
    Codeforces Round #410 (Div. 2)A B C D 暴力 暴力 思路 姿势/随机
    Codeforces Round #409 (rated, Div. 2, based on VK Cup 2017 Round 2) A B C D 暴力 水 二分 几何
    poj 2096 概率dp
    HDU 4405 概率dp
    Codeforces Round #408 (Div. 2) A B C 模拟 模拟 set
    Codeforces Round #301 (Div. 2)A B C D 水 模拟 bfs 概率dp
    HDU 1005 矩阵快速幂
  • 原文地址:https://www.cnblogs.com/aoximin/p/15582365.html
Copyright © 2020-2023  润新知