(十)React Ant Design Pro + .Net5 WebApi:后端环境搭建-IdentityServer4(二)授权模式
##一、前言 先交代一下整个Demo项目结构:
- 一个认证服务(端口5000)
IdentityServer4.Authentication
- 五个授权模式(两个控制台程序,三个MVC项目端口5001)文件夹
GrantClient
- 两个资源服务(WebApi:UserApiResource端口8000,ProductApiResource端口9000)文件夹
ApiResource
##二、准备认证服务 + 资源服务 ###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(); //使用IdentityServer4 app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); }); } }
public class InMemoryConfig { public static IEnumerable<IdentityResource> IdentityResources => new IdentityResource[] { new IdentityResources.OpenId(), new IdentityResources.Profile(), //new IdentityResources.Email(), //new IdentityResources.Address(), //new IdentityResources.Phone() }; /// <summary> /// ApiResource 资源列表 /// </summary> public static IEnumerable<ApiResource> GetApiResources() { return new[] { new ApiResource("UserApiResource", "获取用户信息API") { Scopes={ "UserScope" } }, new ApiResource("ProductApiResource", "获取商品信息API") { Scopes={ "ProductScope" } } }; } /// <summary> /// ApiScopes 作用域 /// </summary> public static IEnumerable<ApiScope> GetApiScopes() { return new ApiScope[] { new ApiScope("UserScope"), new ApiScope("ProductScope") }; } /// <summary> /// Client 客户端 /// </summary> public static IEnumerable<Client> 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<TestUser> GetTestUser() { return new List<TestUser>(){ 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端口启动 ###2、资源服务 新建两个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(); }); } }
##三、授权模式 ###1、客户端授权模式
客户端模式(Client Credentials)指客户端以自己的名义,而不是以用户的名义,向"认证服务"进行认证。如果是提前约束好的客户端,直接给你颁发令牌 token
安装
IdentityModel
class Program { /// <summary> /// 客户端模式(Client Credentials) /// </summary> /// <param name="args"></param> 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(); } }
###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(); }
###3、简化模式
简化模式(Implicit)比授权码模式少了code环节,所有步骤在浏览器中完成,令牌对访问者是可见的,且客户端不需要认证,该模式是很不安全的,且不支持refresh token,适用于 Web 安全要求不高的场景,设置较短时效的 token。
(1)安装
IdentityServer4.AccessTokenValidation、Microsoft.AspNetCore.Authentication.OpenIdConnect,修改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) { //关闭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 = 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添加 注销按钮
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between"> <ul class="navbar-nav flex-grow-1"> <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a> </li> <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a> </li> </ul> <a class="nav-link text-dark float-right" asp-area="" asp-controller="Home" asp-action="Logout">Logout</a> </div>
(3)HomeController添加对应功能,需要认证的方法加上特性
[Authorize]
[Authorize] public IActionResult Privacy() { return View(); } //注销 public IActionResult Logout() { return SignOut("Cookies", "oidc"); }
(4)修改Privacy.cshtml
@{ ViewData["Title"] = "Privacy Policy"; } <h1>@ViewData["Title"]</h1> @using Microsoft.AspNetCore.Authentication <h2>Claims</h2> <dl> @foreach (var claim in User.Claims) { <dt>@claim.Type</dt> <dd>@claim.Value</dd> } </dl> <h2>Properties</h2> <dl> @foreach (var prop in (await Context.AuthenticateAsync()).Properties.Items) { <dt>@prop.Key</dt> <dd>@prop.Value</dd> } </dl>
(5)效果图 ###4、授权码模式
授权码模式(Authorization Code)不同于简化模式直接返回token,而是先返回一个授权码,再用授权码去请求token,然后携带访问Api资源。授权码模式是功能最完整、流程最严密的授权模式。
(1)安装
IdentityServer4.AccessTokenValidation、Microsoft.AspNetCore.Authentication.OpenIdConnect,修改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) { //关闭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 = 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添加 获取用户按钮、注销按钮
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between"> <ul class="navbar-nav flex-grow-1"> <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a> </li> <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a> </li> <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="User">UserApi</a> </li> </ul> <a class="nav-link text-dark float-right" asp-area="" asp-controller="Home" asp-action="Logout">Logout</a> </div>
(3)HomeController添加对应功能,需要认证的方法加上特性
[Authorize]
[Authorize] public IActionResult Privacy() { return View(); } [Authorize] public async Task<IActionResult> 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 = result }; return View(); } //注销 public IActionResult Logout() { return SignOut("Cookies", "oidc"); }
(4)修改Privacy.cshtml
@{ ViewData["Title"] = "Privacy Policy"; } <h1>@ViewData["Title"]</h1> @using Microsoft.AspNetCore.Authentication <h2>Claims</h2> <dl> @foreach (var claim in User.Claims) { <dt>@claim.Type</dt> <dd>@claim.Value</dd> } </dl> <h2>Properties</h2> <dl> @foreach (var prop in (await Context.AuthenticateAsync()).Properties.Items) { <dt>@prop.Key</dt> <dd>@prop.Value</dd> } </dl>
(5)效果图 ###5、混合模式 混合模式(Hybrid Flow)
它为我们提供了两全其美的优势,身份令牌通过浏览器传输,因此客户端可以在进行任何更多工作之前对其进行验证。如果验证成功,客户端会通过令牌服务的以获取访问令牌
(1)安装
IdentityServer4.AccessTokenValidation、Microsoft.AspNetCore.Authentication.OpenIdConnect,修改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) { //关闭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 = 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添加 获取产品、注销按钮
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between"> <ul class="navbar-nav flex-grow-1"> <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a> </li> <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a> </li> <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Product">ProductApi</a> </li> </ul> <a class="nav-link text-dark float-right" asp-area="" asp-controller="Home" asp-action="Logout">Logout</a> </div>
(3)HomeController添加对应功能,需要认证的方法加上特性
[Authorize]
[Authorize] public IActionResult Privacy() { return View(); } [Authorize] public async Task<IActionResult> 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 = result }; return View(); } //注销 public IActionResult Logout() { return SignOut("Cookies", "oidc"); }
(4)修改Privacy.cshtml
@{ ViewData["Title"] = "Privacy Policy"; } <h1>@ViewData["Title"]</h1> @using Microsoft.AspNetCore.Authentication <h2>Claims</h2> <dl> @foreach (var claim in User.Claims) { <dt>@claim.Type</dt> <dd>@claim.Value</dd> } </dl> <h2>Properties</h2> <dl> @foreach (var prop in (await Context.AuthenticateAsync()).Properties.Items) { <dt>@prop.Key</dt> <dd>@prop.Value</dd> } </dl>
(5)效果图 ##四、问题踩坑 ###1、Https Demo全部用的Https,Mvc客户端配置
RequireHttpsMetadata = 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 ###4、其他Error 出现错误大概率是客户端、服务端配置项问题,仔细对比一下就OK了 ##五、前人栽树,后人乘凉 https://www.cnblogs.com/i3yuan/category/1777690.html ##六、代码已上传 https://github.com/WinterSir/IdentityServer4.GrantTypesDemo
- (十一)React Ant Design Pro + .Net5 WebApi:后端环境搭建-IdentityServer4(三)持久化
- React+Ant Design开发环境搭建的实现步骤
- 3分钟搭建Ant Design Pro前端开发环境( MyClouds的前端选型)
- ant design pro 的model,service,connect的使用以及如何调取后端服务器的数据并渲染到本地
- 极客react之Ant Design Pro系列快速入门(五)--更改控件的默认样式
- 【react自制全家桶】一、Webstrom+React+Ant Design+echarts搭建react项目
- ant design环境搭建过程中遇到的问题--Windows-dva-cli
- react(ant-design-pro)路由-router踩坑
- 极客react之Ant Design Pro系列快速入门(九)-- 使用地图(高德地图)
- 极客react之Ant Design Pro系列快速入门(一)--启动篇
- 极客react之Ant Design Pro系列快速入门(二)--登录与默认路径篇
- 极客react之Ant Design Pro系列快速入门(四)--界面新增
- 极客react之Ant Design Pro系列快速入门(六)--绑定model
- react+ant design pro得首次使用
- 极客react之Ant Design Pro系列快速入门(七)-- 使用新的布局
- ant-design环境搭建
- 从零开始使用webpack(4.x)+bable+react+ant-design配置单页面应用开发环境(附模板)
- 极客react之Ant Design Pro系列快速入门(三)--菜单与权限
- 搭建express+reactjs前后端分离开发环境
- react+webpack+wepack-dev-server的环境中ant design图标离线的方法