(十)React|(十)React Ant Design Pro + .Net5 WebApi(后端环境搭建-IdentityServer4(二)授权模式)
一、前言
先交代一下整个Demo项目结构:
- 一个认证服务(端口5000)
IdentityServer4.Authentication
- 五个授权模式(两个控制台程序,三个MVC项目端口5001)文件夹
GrantClient
- 两个资源服务(WebApi:UserApiResource端口8000,ProductApiResource端口9000)文件夹
ApiResource
![(十)React|(十)React Ant Design Pro + .Net5 WebApi(后端环境搭建-IdentityServer4(二)授权模式)](https://img.it610.com/image/info8/7ea9ce1e063a492692f849cfcbc7c41b.png)
文章图片
二、准备认证服务 + 资源服务 1、认证服务
(1)新建一个MVC项目,安装 IdentityServer4 ,注册五种授权模式客户端,代码如下
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)
{
services.AddControllersWithViews();
services.AddIdentityServer()
.AddDeveloperSigningCredential()//临时证书
.AddInMemoryClients(InMemoryConfig.GetClients())//客户端模式,InMemory内存数据
.AddInMemoryApiScopes(InMemoryConfig.GetApiScopes())//作用域
.AddInMemoryApiResources(InMemoryConfig.GetApiResources())//资源
.AddTestUsers(InMemoryConfig.GetTestUser())//用户
.AddInMemoryIdentityResources(InMemoryConfig.IdentityResources);
}// 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();
}
else
{
app.UseExceptionHandler("/Home/Error");
}app.UseIdentityServer();
//使用IdentityServer4app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
public class InMemoryConfig
{
public static IEnumerable IdentityResources =>
new IdentityResource[]
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
//new IdentityResources.Email(),
//new IdentityResources.Address(),
//new IdentityResources.Phone()
};
///
/// ApiResource 资源列表
///
public static IEnumerable GetApiResources()
{
return new[]
{
new ApiResource("UserApiResource", "获取用户信息API")
{
Scopes={ "UserScope" }
},
new ApiResource("ProductApiResource", "获取商品信息API")
{
Scopes={ "ProductScope" }
}
};
}
///
/// ApiScopes 作用域
///
public static IEnumerable GetApiScopes()
{
return new ApiScope[]
{
new ApiScope("UserScope"),
new ApiScope("ProductScope")
};
}
///
/// Client 客户端
///
public static IEnumerable GetClients()
{
return new[]
{
//客户端模式
new Client
{
ClientId = "ClientCredentials",
ClientName = "ClientCredentials",
ClientSecrets = new [] { new Secret("ClientCredentials".Sha256()) },
AllowedGrantTypes = GrantTypes.ClientCredentials,
AllowedScopes = new [] { "UserScope" }
},
//密码模式
new Client
{
ClientId = "ResourceOwnerPasswordCredentials",
ClientName = "ResourceOwnerPasswordCredentials",
ClientSecrets = new [] { new Secret("ResourceOwnerPasswordCredentials".Sha256()) },
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
AllowedScopes = new []
{
"ProductScope",
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
}
},
//简化模式
new Client
{
ClientId = "Implicit",
ClientName = "Implicit",
AllowedGrantTypes = GrantTypes.Implicit,
RedirectUris = { "https://localhost:5001/signin-oidc" },
PostLogoutRedirectUris = { "https://localhost:5001/signout-callback-oidc" },
RequireConsent = true,
AllowedScopes = new []{
"UserScope",
"ProductScope",
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
}
},
//授权码模式
new Client
{
ClientId = "AuthorizationCode",
ClientName = "AuthorizationCode",
ClientSecrets = new [] { new Secret("AuthorizationCode".Sha256()) },
AllowedGrantTypes = GrantTypes.Code,
RedirectUris = { "https://localhost:5001/signin-oidc" },
PostLogoutRedirectUris = { "https://localhost:5001/signout-callback-oidc" },
RequireConsent = true,
AllowedScopes = new []{
"UserScope",
"ProductScope",
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
}
},
//混合模式
new Client
{
ClientId = "Hybrid",
ClientName = "Hybrid",
ClientSecrets = new [] { new Secret("Hybrid".Sha256()) },
AllowedGrantTypes = GrantTypes.Hybrid,
RedirectUris = { "https://localhost:5001/signin-oidc" },
PostLogoutRedirectUris = { "https://localhost:5001/signout-callback-oidc" },
RequireConsent = true,
RequirePkce = false,
AllowedScopes = new []{
"UserScope",
"ProductScope",
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
//IdentityServerConstants.StandardScopes.Email,
//IdentityServerConstants.StandardScopes.Address,
//IdentityServerConstants.StandardScopes.Phone
}
},
};
}
public static List GetTestUser()
{
return new List(){
new TestUser
{
SubjectId = "1",
Username = "WinterSir",
Password = "WinterSir",
Claims =
{
new Claim(JwtClaimTypes.Name,"WinterSir"),
new Claim(JwtClaimTypes.GivenName,"WinterSir"),
new Claim(JwtClaimTypes.FamilyName,"WinterSir-FamilyName"),
new Claim(JwtClaimTypes.Email,"641187567@qq.com"),
new Claim(JwtClaimTypes.EmailVerified,"true", ClaimValueTypes.Boolean),
new Claim(JwtClaimTypes.WebSite,"http://WinterSir.com"),
new Claim(JwtClaimTypes.Address,@" [ 'street_address': 'Chang Ping', 'locality': 'BeiJing' ,'postal_code’: 102206,'country': 'China'}",
IdentityServerConstants.ClaimValueTypes.Json)
}
}
};
}
}
(2)cmd
dotnet new is4ui
安装Quickstart UI
模板,删除原来 Controllers 中 HomeController 防止冲突,设置5000端口启动![(十)React|(十)React Ant Design Pro + .Net5 WebApi(后端环境搭建-IdentityServer4(二)授权模式)](https://img.it610.com/image/info8/2e8cf700ae854927a0cb5ee4a8629ab1.jpg)
文章图片
2、资源服务
【(十)React|(十)React Ant Design Pro + .Net5 WebApi(后端环境搭建-IdentityServer4(二)授权模式)】新建两个WebApi项目,安装
IdentityServer4.AccessTokenValidation
,分别修改Startup、Controller,设置8000、9000端口启动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)
{services.AddControllers();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "ProductApiResource", Version = "v1" });
});
//集成端口为5000的认证服务
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = "https://localhost:5000";
options.ApiName = "ProductApiResource";
});
}// 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.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "ProductApiResource v1"));
}app.UseRouting();
app.UseAuthentication();
//鉴权app.UseAuthorization();
//授权app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
![(十)React|(十)React Ant Design Pro + .Net5 WebApi(后端环境搭建-IdentityServer4(二)授权模式)](https://img.it610.com/image/info8/290761973b234e82ace29e5524ffb5f6.jpg)
文章图片
三、授权模式 1、客户端授权模式
客户端模式(Client Credentials)指客户端以自己的名义,而不是以用户的名义,向"认证服务"进行认证。如果是提前约束好的客户端,直接给你颁发令牌 token安装
IdentityModel
class Program
{
///
/// 客户端模式(Client Credentials)
///
///
static void Main(string[] args)
{
Console.WriteLine("***************** 客户端模式(Client Credentials)*****************");
var client = new HttpClient();
var disco = client.GetDiscoveryDocumentAsync("https://localhost:5000/").Result;
if (disco.IsError)
{
Console.WriteLine(disco.Error);
return;
}
var tokenResponse = client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = "ClientCredentials",
ClientSecret = "ClientCredentials",
Scope = "UserScope"
}).Result;
if (tokenResponse.IsError)
{
Console.WriteLine(tokenResponse.Error);
return;
}Console.WriteLine("\nToken: " + tokenResponse.AccessToken);
var apiClient = new HttpClient();
apiClient.SetBearerToken(tokenResponse.AccessToken);
var response = apiClient.GetAsync("https://localhost:8000/User/Get").Result;
if (!response.IsSuccessStatusCode)
{
Console.WriteLine(response.StatusCode);
}
else
{
var content = response.Content.ReadAsStringAsync().Result;
Console.WriteLine("\n结果: " + content);
}Console.ReadLine();
}
}
![(十)React|(十)React Ant Design Pro + .Net5 WebApi(后端环境搭建-IdentityServer4(二)授权模式)](https://img.it610.com/image/info8/01838b83070446f2894317d55b3e6193.png)
文章图片
2、密码模式
密码模式(Resource Owner Password Credentials)中客户端使用用户提供的用户名和密码,向"认证服务"进行认证,有较高风险,通常只有在其他授权模式无法执行的情况下,才能考虑使用这种模式。相较于客户端多了一个用户角色。安装
IdentityModel
static void Main(string[] args)
{
Console.WriteLine("***************** 密码模式(Resource Owner Password credentials)***************** ");
var client = new HttpClient();
var disco = client.GetDiscoveryDocumentAsync("https://localhost:5000/").Result;
if (disco.IsError)
{
Console.WriteLine(disco.Error);
return;
}
var tokenResponse = client.RequestPasswordTokenAsync(new PasswordTokenRequest()
{
Address = disco.TokenEndpoint,
ClientId = "ResourceOwnerPasswordCredentials",
ClientSecret = "ResourceOwnerPasswordCredentials",
UserName = "WinterSir",
Password = "WinterSir",
Scope = "ProductScope",
}).Result;
if (tokenResponse.IsError)
{
Console.WriteLine(tokenResponse.Error);
return;
}Console.WriteLine("\nToken: " + tokenResponse.AccessToken);
var apiClient = new HttpClient();
apiClient.SetBearerToken(tokenResponse.AccessToken);
var response = apiClient.GetAsync("https://localhost:9000/Product/Get").Result;
if (!response.IsSuccessStatusCode)
{
Console.WriteLine(response.StatusCode);
}
else
{
var content = response.Content.ReadAsStringAsync().Result;
Console.WriteLine("\n结果: " + content);
}Console.ReadLine();
}
![(十)React|(十)React Ant Design Pro + .Net5 WebApi(后端环境搭建-IdentityServer4(二)授权模式)](https://img.it610.com/image/info8/6c5537746306484a90e632db94cf8510.png)
文章图片
3、简化模式
简化模式(Implicit)比授权码模式少了code环节,所有步骤在浏览器中完成,令牌对访问者是可见的,且客户端不需要认证,该模式是很不安全的,且不支持refresh token,适用于 Web 安全要求不高的场景,设置较短时效的 token。(1)安装
IdentityServer4.AccessTokenValidation、Microsoft.AspNetCore.Authentication.OpenIdConnect
,修改Startuppublic 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)
{
//关闭Jwt映射
JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
//注册授权
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "https://localhost:5000";
//认证服务
options.RequireHttpsMetadata = https://www.it610.com/article/true;
//必须使用Https,否则用户无法登录
options.ClientId ="Implicit";
options.ClientSecret = "Implicit";
options.SaveTokens = true;
//表示Token要存储
});
services.AddControllersWithViews().AddRazorRuntimeCompilation();
}// 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();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseHttpsRedirection();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
(2)_Layout.cshtml添加 注销按钮
Logout
(3)HomeController添加对应功能,需要认证的方法加上特性
[Authorize]
[Authorize]
public IActionResult Privacy()
{
return View();
}//注销
public IActionResult Logout()
{
return SignOut("Cookies", "oidc");
}
(4)修改Privacy.cshtml
@{
ViewData["Title"] = "Privacy Policy";
}
@ViewData["Title"]@using Microsoft.AspNetCore.AuthenticationClaims
@foreach (var claim in User.Claims)
{
- @claim.Type
- @claim.Value
}
Properties
@foreach (var prop in (await Context.AuthenticateAsync()).Properties.Items)
{
- @prop.Key
- @prop.Value
}
(5)效果图
4、授权码模式
授权码模式(Authorization Code)不同于简化模式直接返回token,而是先返回一个授权码,再用授权码去请求token,然后携带访问Api资源。授权码模式是功能最完整、流程最严密的授权模式。(1)安装
IdentityServer4.AccessTokenValidation、Microsoft.AspNetCore.Authentication.OpenIdConnect
,修改Startuppublic 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)
{
//关闭Jwt映射
JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
//注册授权
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "https://localhost:5000";
//认证服务
options.RequireHttpsMetadata = https://www.it610.com/article/true;
//必须使用Https,否则用户无法登录
options.ClientId ="AuthorizationCode";
options.ClientSecret = "AuthorizationCode";
options.ResponseType = "code";
options.Scope.Clear();
options.Scope.Add("UserScope");
options.Scope.Add("ProductScope");
options.Scope.Add(OidcConstants.StandardScopes.OpenId);
options.Scope.Add(OidcConstants.StandardScopes.Profile);
//options.Scope.Add(OidcConstants.StandardScopes.Email);
//options.Scope.Add(OidcConstants.StandardScopes.Phone);
//options.Scope.Add(OidcConstants.StandardScopes.Address);
options.SaveTokens = true;
//表示Token要存储
});
services.AddControllersWithViews().AddRazorRuntimeCompilation();
services.AddControllers().AddJsonOptions(options =>
{
options.JsonSerializerOptions.Encoder = JavaScriptEncoder.Create(UnicodeRanges.All);
});
}// 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();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseHttpsRedirection();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
(2)_Layout.cshtml添加 获取用户按钮、注销按钮
Logout
(3)HomeController添加对应功能,需要认证的方法加上特性
[Authorize]
[Authorize]
public IActionResult Privacy()
{
return View();
}[Authorize]
public async Task User()
{
var client = new HttpClient();
var accessToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
if (string.IsNullOrEmpty(accessToken))
{
return Json(new { msg = "accesstoken 获取失败" });
}
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
var httpResponse = await client.GetAsync("https://localhost:8000/User/Get");
var result = await httpResponse.Content.ReadAsStringAsync();
if (!httpResponse.IsSuccessStatusCode)
{
ViewBag.Result = new { msg = "请求 User/Get 失败", error = result };
}
ViewBag.Result = new { msg = "成功", data = https://www.it610.com/article/result };
return View();
}//注销
public IActionResult Logout()
{
return SignOut("Cookies", "oidc");
}
(4)修改Privacy.cshtml
@{
ViewData["Title"] = "Privacy Policy";
}
@ViewData["Title"]@using Microsoft.AspNetCore.AuthenticationClaims
@foreach (var claim in User.Claims)
{
- @claim.Type
- @claim.Value
}
Properties
@foreach (var prop in (await Context.AuthenticateAsync()).Properties.Items)
{
- @prop.Key
- @prop.Value
}
(5)效果图
5、混合模式
混合模式(Hybrid Flow)
它为我们提供了两全其美的优势,身份令牌通过浏览器传输,因此客户端可以在进行任何更多工作之前对其进行验证。如果验证成功,客户端会通过令牌服务的以获取访问令牌(1)安装
IdentityServer4.AccessTokenValidation、Microsoft.AspNetCore.Authentication.OpenIdConnect
,修改Startuppublic 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)
{
//关闭Jwt映射
JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
//注册授权
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "https://localhost:5000";
//认证服务
options.RequireHttpsMetadata = https://www.it610.com/article/true;
//必须使用Https,否则用户无法登录
options.ClientId ="Hybrid";
options.ClientSecret = "Hybrid";
options.ResponseType = "code id_token";
options.Scope.Clear();
options.Scope.Add("UserScope");
options.Scope.Add("ProductScope");
options.Scope.Add(OidcConstants.StandardScopes.OpenId);
options.Scope.Add(OidcConstants.StandardScopes.Profile);
//options.Scope.Add(OidcConstants.StandardScopes.Email);
//options.Scope.Add(OidcConstants.StandardScopes.Phone);
//options.Scope.Add(OidcConstants.StandardScopes.Address);
//options.Scope.Add(OidcConstants.StandardScopes.0fflineAccess);
//获取到刷新Token
options.SaveTokens = true;
//表示Token要存储
});
services.AddControllersWithViews().AddRazorRuntimeCompilation();
}// 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();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseHttpsRedirection();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
(2)_Layout.cshtml添加 获取产品、注销按钮
Logout
(3)HomeController添加对应功能,需要认证的方法加上特性
[Authorize]
[Authorize]
public IActionResult Privacy()
{
return View();
}
[Authorize]
public async Task Product()
{
var client = new HttpClient();
var accessToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
if (string.IsNullOrEmpty(accessToken))
{
return Json(new { msg = "accesstoken 获取失败" });
}
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
var httpResponse = await client.GetAsync("https://localhost:9000/Product/Get");
var result = await httpResponse.Content.ReadAsStringAsync();
if (!httpResponse.IsSuccessStatusCode)
{
ViewBag.Result = new { msg = "请求 User/Get 失败", error = result };
}
ViewBag.Result = new { msg = "成功", data = https://www.it610.com/article/result };
return View();
}//注销
public IActionResult Logout()
{
return SignOut("Cookies", "oidc");
}
(4)修改Privacy.cshtml
@{
ViewData["Title"] = "Privacy Policy";
}
@ViewData["Title"]@using Microsoft.AspNetCore.AuthenticationClaims
@foreach (var claim in User.Claims)
{
- @claim.Type
- @claim.Value
}
Properties
@foreach (var prop in (await Context.AuthenticateAsync()).Properties.Items)
{
- @prop.Key
- @prop.Value
}
(5)效果图
四、问题踩坑 1、Https
Demo全部用的Https,Mvc客户端配置
RequireHttpsMetadata = https://www.it610.com/article/true
如果使用http遇到认证服无法务登录问题,可参考以下地址https://www.cnblogs.com/i3yuan/p/14033016.html#autoid-20-0-0
2、ResponseType
授权码模式、混合模式需要修改客户端配置ResponseType,
ResponseType = "code" 、 ResponseType = "code id_token"
3、RequirePkce
混合模式需要修改对应服务端注册客户端时配置
RequirePkce = false
,这样不需要客户端提供code challeng![(十)React|(十)React Ant Design Pro + .Net5 WebApi(后端环境搭建-IdentityServer4(二)授权模式)](https://img.it610.com/image/info8/b8723d497276457ba3ba351a547e987c.jpg)
文章图片
4、其他Error
出现错误大概率是客户端、服务端配置项问题,仔细对比一下就OK了
五、前人栽树,后人乘凉 https://www.cnblogs.com/i3yuan/category/1777690.html
六、代码已上传 https://github.com/WinterSir/IdentityServer4.GrantTypesDemo
推荐阅读
- 仅需一个APP就可以像Home|仅需一个APP就可以像Home Assistant一样实现联动()
- Java并发—通过ReentrantLock和Semaphore揭秘AQS独占模式和共享模式
- 搜狗|微信十年,张小龙爆了个大瓜,究竟是谁在监视你的手机()
- 设计模式学习笔记(十)装饰器模式及其应用
- 提升数十倍工作效率丨曼孚科技如何打造新一代TOP1智能标注平台()
- React讲解 - 父组件调用子组件内容【更新中】
- 数据结构|《数据结构》十道链表经典面试题多种方法深度解析
- react实现简单的拖拽功能
- 戏说领域驱动设计(十九)——外验
- C语言|十分钟手撕栈与队列——栈与队列实现详解