Asp.Net Core基于JWT認證的數(shù)據(jù)接口網(wǎng)關(guān)實例代碼
前言
近日,應(yīng)一位朋友的邀請寫了個Asp.Net Core基于JWT認證的數(shù)據(jù)接口網(wǎng)關(guān)Demo。朋友自己開了個公司,接到的一個升級項目,客戶要求用Aps.Net Core做數(shù)據(jù)網(wǎng)關(guān)服務(wù)且基于JWT認證實現(xiàn)對前后端分離的數(shù)據(jù)服務(wù)支持,于是想到我一直做.Net開發(fā),問我是否對.Net Core有所了解?能不能做個簡單Demo出來看看?我說,分道揚鑣之后我不是調(diào)用別人的接口就是提供接口給別人調(diào)用,于是便有了以下示例代碼。
示例要求能演示獲取Token及如何使用該Token訪問數(shù)據(jù)資源,在Demo中實現(xiàn)了JWT的頒發(fā)及驗證以及重寫一個ActionAuthorizeAttribute實現(xiàn)對具體數(shù)據(jù)接口的調(diào)用權(quán)限控制,先看一下項目截圖:
[項目截圖]
項目文件介紹
解決方案下只有一個項目,項目名稱就叫Jwt.Gateway,包含主要文件有:
- Controllers目錄下的ApiActionFilterAttribute.cs文件,繼承Microsoft.AspNetCore.Mvc.Filters.ActionFilterAttribute,用于校驗接口調(diào)用者對具體接口的訪問權(quán)限。
- Controllers目錄下的ApiBase.cs文件,繼承Microsoft.AspNetCore.Mvc.Controller,具有Microsoft.AspNetCore.Authorization.Authorize特性引用,用于讓所有數(shù)據(jù)接口用途的控制器繼承,定義有CurrentAppKey屬性(來訪應(yīng)用程序的身份標(biāo)識)并在OnActionExecuting事件中統(tǒng)一分析Claims并賦值。
- Controllers目錄下的TokenController.cs控制器文件,用于對調(diào)用方應(yīng)用程序獲取及注銷Token。
- Controllers目錄下的UsersController.cs控制器文件,繼承ApiBase.cs,作為數(shù)據(jù)調(diào)用示例。
- MiddleWares目錄下的ApiCustomException.cs文件,是一個數(shù)據(jù)接口的統(tǒng)一異常處理中間件。
- Models目錄下的ApiResponse.cs文件,用于做數(shù)據(jù)接口的統(tǒng)一數(shù)據(jù)及錯誤信息輸出實體模型。
- Models目錄下的User.cs文件,示例數(shù)據(jù)實體模型。
- Program.cs及Startup.cs文件就不介紹了,隨便建個空項目都有。
項目文件代碼
ApiActionFilterAttribute.cs
Controllers目錄下的ApiActionFilterAttribute.cs文件,繼承Microsoft.AspNetCore.Mvc.Filters.ActionFilterAttribute,用于校驗接口調(diào)用者對具體接口的訪問權(quán)限。
設(shè)想每一個到訪的請求都是一個應(yīng)用程序,每一個應(yīng)用程序都分配有基本的Key和Password,每一個應(yīng)用程序具有不同的接口訪問權(quán)限,所以在具體的數(shù)據(jù)接口上應(yīng)該聲明該接口所要求的權(quán)限值,比如修改用戶信息的接口應(yīng)該在接口方法上聲明需要具有“修改用戶”的權(quán)限,用例: [ApiActionFilter("用戶修改")]
。
大部分情況下一個接口(方法)對應(yīng)一個操作,這樣基本上就能應(yīng)付了,但是不排除有時候可能需要多個權(quán)限組合進行驗證,所以該文件中有一個對多個權(quán)限值進行校驗的“與”和“和”枚舉,用例: [ApiActionFilter(new string[] { "用戶修改", "用戶錄入", "用戶刪除" },ApiActionFilterAttributeOption.AND)]
,這樣好像就差不多了。
由于在一個接口調(diào)用之后可能需要將該接口所聲明需要的權(quán)限值記入日志等需求,因此權(quán)限值集合將被寫入到HttpContext.Items["Permissions"]中以方便可能的后續(xù)操作訪問,看代碼:
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Filters; namespace Jwt.Gateway.Controllers { public enum ApiActionFilterAttributeOption { OR,AND } public class ApiActionFilterAttribute : Microsoft.AspNetCore.Mvc.Filters.ActionFilterAttribute { List<string> Permissions = new List<string>(); ApiActionFilterAttributeOption Option = ApiActionFilterAttributeOption.AND; public ApiActionFilterAttribute(string permission) { Permissions.Add(permission); } public ApiActionFilterAttribute(string[] permissions, ApiActionFilterAttributeOption option) { foreach(var permission in permissions) { if (Permissions.Contains(permission)) { continue; } Permissions.Add(permission); } Option = option; } public override void OnActionExecuting(ActionExecutingContext context) { var key = GetAppKey(context); List<string> keyPermissions = GetAppKeyPermissions(key); var isAnd = Option == ApiActionFilterAttributeOption.AND; var permissionsCount = Permissions.Count; var keyPermissionsCount = keyPermissions.Count; for (var i = 0; i < permissionsCount; i++) { bool flag = false; for (var j = 0; j < keyPermissions.Count; j++) { if (flag = string.Equals(Permissions[i], keyPermissions[j], StringComparison.OrdinalIgnoreCase)) { break; } } if (flag) { continue; } if (isAnd) { throw new Exception("應(yīng)用“" + key + "”缺少“" + Permissions[i] + "”的權(quán)限"); } } context.HttpContext.Items.Add("Permissions", Permissions); base.OnActionExecuting(context); } private string GetAppKey(ActionExecutingContext context) { var claims = context.HttpContext.User.Claims; if (claims == null) { throw new Exception("未能獲取到應(yīng)用標(biāo)識"); } var claimKey = claims.ToList().Find(o => string.Equals(o.Type, "AppKey", StringComparison.OrdinalIgnoreCase)); if (claimKey == null) { throw new Exception("未能獲取到應(yīng)用標(biāo)識"); } return claimKey.Value; } private List<string> GetAppKeyPermissions(string appKey) { List<string> li = new List<string> { "用戶明細","用戶列表","用戶錄入","用戶修改","用戶刪除" }; return li; } } } ApiActionAuthorizeAttribute.cs
ApiBase.cs
Controllers目錄下的ApiBase.cs文件,繼承Microsoft.AspNetCore.Mvc.Controller,具有Microsoft.AspNetCore.Authorization.Authorize特性引用,用于讓所有數(shù)據(jù)接口用途的控制器繼承,定義有CurrentAppKey屬性(來訪應(yīng)用程序的身份標(biāo)識)并在OnActionExecuting事件中統(tǒng)一分析Claims并賦值。
通過驗證之后,Aps.Net Core會在HttpContext.User.Claims中將將來訪者的身份信息記錄下來,我們可以通過該集合得到來訪者的身份信息。
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; namespace Jwt.Gateway.Controllers { [Microsoft.AspNetCore.Authorization.Authorize] public class ApiBase : Microsoft.AspNetCore.Mvc.Controller { private string _CurrentAppKey = ""; public string CurrentAppKey { get { return _CurrentAppKey; } } public override void OnActionExecuting(ActionExecutingContext context) { var claims = context.HttpContext.User.Claims.ToList(); var claim = claims.Find(o => o.Type == "appKey"); if (claim == null) { throw new Exception("未通過認證"); } var appKey = claim.Value; if (string.IsNullOrEmpty(appKey)) { throw new Exception("appKey不合法"); } _CurrentAppKey = appKey; base.OnActionExecuting(context); } } } ApiBase.cs
TokenController.cs
Controllers目錄下的TokenController.cs控制器文件,用于對調(diào)用方應(yīng)用程序獲取及注銷Token。
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace Jwt.Gateway.Controllers { [Route("api/[controller]/[action]")] public class TokenController : Controller { private readonly Microsoft.Extensions.Configuration.IConfiguration _configuration; public TokenController(Microsoft.Extensions.Configuration.IConfiguration configuration) { _configuration = configuration; } // /api/token/get public IActionResult Get(string appKey, string appPassword) { try { if (string.IsNullOrEmpty(appKey)) { throw new Exception("缺少appKey"); } if (string.IsNullOrEmpty(appKey)) { throw new Exception("缺少appPassword"); } if (appKey != "myKey" && appPassword != "myPassword")//固定的appKey及appPassword,實際項目中應(yīng)該來自數(shù)據(jù)庫或配置文件 { throw new Exception("配置不存在"); } var key = new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(_configuration["JwtSecurityKey"])); var creds = new Microsoft.IdentityModel.Tokens.SigningCredentials(key, Microsoft.IdentityModel.Tokens.SecurityAlgorithms.HmacSha256); var claims = new List<System.Security.Claims.Claim>(); claims.Add(new System.Security.Claims.Claim("appKey", appKey));//僅在Token中記錄appKey var token = new System.IdentityModel.Tokens.Jwt.JwtSecurityToken( issuer: _configuration["JwtTokenIssuer"], audience: _configuration["JwtTokenAudience"], claims: claims, expires: DateTime.Now.AddMinutes(30), signingCredentials: creds); return Ok(new Models.ApiResponse { status = 1, message = "OK", data = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler().WriteToken(token) }); } catch(Exception ex) { return Ok(new Models.ApiResponse { status = 0, message = ex.Message, data = "" }); } } // /api/token/delete public IActionResult Delete(string token) { //code: 加入黑名單,使其無效 return Ok(new Models.ApiResponse { status = 1, message = "OK", data = "" }); } } } TokenController.cs
UsersController.cs
Controllers目錄下的UsersController.cs控制器文件,繼承ApiBase.cs,作為數(shù)據(jù)調(diào)用示例。
該控制器定義了對User對象常規(guī)的明細、列表、錄入、修改、刪除等操作。
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; namespace Jwt.Gateway.Controllers { [Produces("application/json")] [Route("api/[controller]/[action]")] public class UsersController : ApiBase { /* * 1.要訪問訪問該控制器提供的接口請先通過"/api/token/get"獲取token * 2.訪問該控制器提供的接口http請求頭必須具有值為"Bearer+空格+token"的Authorization鍵,格式參考: * "Authorization"="Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiQXBwIiwiYXBwS2V5IjoibXlLZXkiLCJleHAiOjE1NTE3ODc2MDMsImlzcyI6IkdhdGV3YXkiLCJhdWQiOiJhdWRpZW5jZSJ9.gQ9_Q7HUT31oFyfl533T-bNO5IWD2drl0NmD1JwQkMI" */ /// <summary> /// 臨時用戶測試數(shù)據(jù),實際項目中應(yīng)該來自數(shù)據(jù)庫等媒介 /// </summary> static List<Models.User> _Users = null; static object _Lock = new object(); public UsersController() { if (_Users == null) { lock (_Lock) { if (_Users == null) { _Users = new List<Models.User>(); var now = DateTime.Now; for(var i = 0; i < 10; i++) { var num = i + 1; _Users.Add(new Models.User { UserId = num, UserName = "name"+num, UserPassword = "pwd"+num, UserJoinTime = now }); } } } } } // /api/users/detail [ApiActionFilter("用戶明細")] public IActionResult Detail(long userId) { /* //獲取appKey(在ApiBase中寫入) var appKey = CurrentAppKey; //獲取使用的權(quán)限(在ApiActionAuthorizeAttribute中寫入) var permissions = HttpContext.Items["Permissions"]; */ var user = _Users.Find(o => o.UserId == userId); if (user == null) { throw new Exception("用戶不存在"); } return Ok(new Models.ApiResponse { data = user, status = 1, message = "OK" }); } // /api/users/list [ApiActionFilter("用戶列表")] public IActionResult List(int page, int size) { page = page < 1 ? 1 : page; size = size < 1 ? 1 : size; var total = _Users.Count(); var pages = total % size == 0 ? total / size : ((long)Math.Floor((double)total / size + 1)); if (page > pages) { return Ok(new Models.ApiResponse { data = new List<Models.User>(), status = 1, message = "OK", total = total }); } var li = new List<Models.User>(); var startIndex = page * size - size; var endIndex = startIndex + size - 1; if (endIndex > total - 1) { endIndex = total - 1; } for(; startIndex <= endIndex; startIndex++) { li.Add(_Users[startIndex]); } return Ok(new Models.ApiResponse { data = li, status = 1, message = "OK", total = total }); } // /api/users/add [ApiActionFilter("用戶錄入")] public IActionResult Add() { return Ok(new Models.ApiResponse { status = 1, message = "OK" }); } // /api/users/update [ApiActionFilter(new string[] { "用戶修改", "用戶錄入", "用戶刪除" },ApiActionFilterAttributeOption.AND)] public IActionResult Update() { return Ok(new Models.ApiResponse { status = 1, message = "OK" }); } // /api/users/delete [ApiActionFilter("用戶刪除")] public IActionResult Delete() { return Ok(new Models.ApiResponse { status = 1, message = "OK" }); } } } UsersController.cs
ApiCustomException.cs
MiddleWares目錄下的ApiCustomException.cs文件,是一個數(shù)據(jù)接口的統(tǒng)一異常處理中間件。
該文件整理并抄襲自:https://www.cnblogs.com/ShenNan/p/10197231.html
在此特別感謝一下作者的先行貢獻,并請原諒我無恥的抄襲。
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; namespace Jwt.Gateway.MiddleWares { //參考: https://www.cnblogs.com/ShenNan/p/10197231.html public enum ApiCustomExceptionHandleType { JsonHandle = 0, PageHandle = 1, Both = 2 } public class ApiCustomExceptionMiddleWareOption { public ApiCustomExceptionMiddleWareOption( ApiCustomExceptionHandleType handleType = ApiCustomExceptionHandleType.JsonHandle, IList<PathString> jsonHandleUrlKeys = null, string errorHandingPath = "") { HandleType = handleType; JsonHandleUrlKeys = jsonHandleUrlKeys; ErrorHandingPath = errorHandingPath; } public ApiCustomExceptionHandleType HandleType { get; set; } public IList<PathString> JsonHandleUrlKeys { get; set; } public PathString ErrorHandingPath { get; set; } } public class ApiCustomExceptionMiddleWare { private RequestDelegate _next; private ApiCustomExceptionMiddleWareOption _option; private IDictionary<int, string> _exceptionStatusCodeDic; public ApiCustomExceptionMiddleWare(RequestDelegate next, ApiCustomExceptionMiddleWareOption option) { _next = next; _option = option; _exceptionStatusCodeDic = new Dictionary<int, string> { { 401, "未授權(quán)的請求" }, { 404, "找不到該頁面" }, { 403, "訪問被拒絕" }, { 500, "服務(wù)器發(fā)生意外的錯誤" } //其余狀態(tài)自行擴展 }; } public async Task Invoke(HttpContext context) { Exception exception = null; try { await _next(context); } catch (Exception ex) { context.Response.Clear(); context.Response.StatusCode = 200;//手動設(shè)置狀態(tài)碼(總是成功) exception = ex; } finally { if (_exceptionStatusCodeDic.ContainsKey(context.Response.StatusCode) && !context.Items.ContainsKey("ExceptionHandled")) { var errorMsg = string.Empty; if (context.Response.StatusCode == 500 && exception != null) { errorMsg = $"{_exceptionStatusCodeDic[context.Response.StatusCode]}\r\n{(exception.InnerException != null ? exception.InnerException.Message : exception.Message)}"; } else { errorMsg = _exceptionStatusCodeDic[context.Response.StatusCode]; } exception = new Exception(errorMsg); } if (exception != null) { var handleType = _option.HandleType; if (handleType == ApiCustomExceptionHandleType.Both) { var requestPath = context.Request.Path; handleType = _option.JsonHandleUrlKeys != null && _option.JsonHandleUrlKeys.Count( k => requestPath.StartsWithSegments(k, StringComparison.CurrentCultureIgnoreCase)) > 0 ? ApiCustomExceptionHandleType.JsonHandle : ApiCustomExceptionHandleType.PageHandle; } if (handleType == ApiCustomExceptionHandleType.JsonHandle) await JsonHandle(context, exception); else await PageHandle(context, exception, _option.ErrorHandingPath); } } } private Jwt.Gateway.Models.ApiResponse GetApiResponse(Exception ex) { return new Jwt.Gateway.Models.ApiResponse() { status = 0, message = ex.Message }; } private async Task JsonHandle(HttpContext context, Exception ex) { var apiResponse = GetApiResponse(ex); var serialzeStr = Newtonsoft.Json.JsonConvert.SerializeObject(apiResponse); context.Response.ContentType = "application/json"; await context.Response.WriteAsync(serialzeStr, System.Text.Encoding.UTF8); } private async Task PageHandle(HttpContext context, Exception ex, PathString path) { context.Items.Add("Exception", ex); var originPath = context.Request.Path; context.Request.Path = path; try { await _next(context); } catch { } finally { context.Request.Path = originPath; } } } public static class ApiCustomExceptionMiddleWareExtensions { public static IApplicationBuilder UseApiCustomException(this IApplicationBuilder app, ApiCustomExceptionMiddleWareOption option) { return app.UseMiddleware<ApiCustomExceptionMiddleWare>(option); } } } ApiCustomException.cs
配置相關(guān)
appsettings.json
算法'HS256'要求SecurityKey.KeySize大于'128'位,所以JwtSecurityKey可不要太短了哦。
{ "Urls": "http://localhost:60000", "AllowedHosts": "*", "JwtSecurityKey": "areyouokhhhhhhhhhhhhhhhhhhhhhhhhhhh", "JwtTokenIssuer": "Jwt.Gateway", "JwtTokenAudience": "App" } appsettings.json
Startup.cs
關(guān)于JWT的配置可以在通過JwtBearerOptions加入一些自己的事件處理邏輯,共有4個事件可供調(diào)用:
OnAuthenticationFailed,OnMessageReceived,OnTokenValidated,OnChallenge, 本示例中是在OnTokenValidated中插入Token黑名單的校驗邏輯。黑名單應(yīng)該是Jwt應(yīng)用場景中主動使Token過期的主流做法了。
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Jwt.Gateway.MiddleWares; using Microsoft.Extensions.DependencyInjection; namespace Jwt.Gateway { public class Startup { private readonly Microsoft.Extensions.Configuration.IConfiguration _configuration; public Startup(Microsoft.Extensions.Configuration.IConfiguration configuration) { _configuration = configuration; } public void ConfigureServices(IServiceCollection services) { services.AddAuthentication(Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.Events = new Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerEvents { /*OnMessageReceived = context => { context.Token = context.Request.Query["access_token"]; return Task.CompletedTask; },*/ OnTokenValidated = context => { var token = ((System.IdentityModel.Tokens.Jwt.JwtSecurityToken)context.SecurityToken).RawData; if (InBlacklist(token)) { context.Fail("token in blacklist"); } return Task.CompletedTask; } }; options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidAudience = _configuration["JwtTokenAudience"], ValidIssuer = _configuration["JwtTokenIssuer"], IssuerSigningKey = new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(_configuration["JwtSecurityKey"])) }; }); services.AddMvc().AddJsonOptions(option=> { option.SerializerSettings.DateFormatString = "yyyy-MM-dd HH:mm:ss.fff"; }); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseApiCustomException(new ApiCustomExceptionMiddleWareOption( handleType: ApiCustomExceptionHandleType.Both, jsonHandleUrlKeys: new PathString[] { "/api" }, errorHandingPath: "/home/error")); app.UseAuthentication(); app.UseMvc(); } bool InBlacklist(string token) { //code: 實際項目中應(yīng)該查詢數(shù)據(jù)庫或配置文件進行比對 return false; } } } Startup.cs
Program.cs
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace Jwt.Gateway { public class Program { public static void Main(string[] args) { BuildWebHost(args).Run(); } public static IWebHost BuildWebHost(string[] args) { var config = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json", optional: true) .Build(); return WebHost.CreateDefaultBuilder(args) .UseKestrel() .UseConfiguration(config) .UseStartup<Startup>() .Build(); } } } Program.cs
運行截圖
[運行截圖-獲取Token]
[運行截圖-配置Fiddler調(diào)用接口獲取數(shù)據(jù)]
[運行截圖-獲取到數(shù)據(jù)]
如果Token校驗失敗將會返回401錯誤!
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,謝謝大家對腳本之家的支持。
- .NET?Core支持Cookie和JWT混合認證、授權(quán)的方法
- .net?core?api接口JWT方式認證Token
- ASP.NET?Core應(yīng)用JWT進行用戶認證及Token的刷新方案
- asp.net core3.1cookie和jwt混合認證授權(quán)實現(xiàn)多種身份驗證方案
- AntDesign Pro + .NET Core 實現(xiàn)基于JWT的登錄認證功能
- .Net Core官方JWT授權(quán)驗證的全過程
- 詳解ASP.NET Core Web Api之JWT刷新Token
- ASP.NET Core使用JWT認證授權(quán)的方法
- ASP.NET Core學(xué)習(xí)之使用JWT認證授權(quán)詳解
- 淺談ASP.NET Core 中jwt授權(quán)認證的流程原理
- ASP.Net Core3.0中使用JWT認證的實現(xiàn)
- .NET core 3.0如何使用Jwt保護api詳解
- asp.net core集成JWT的步驟記錄
- .net core webapi jwt 更為清爽的認證詳解
- .Net Core實現(xiàn)JWT授權(quán)認證
相關(guān)文章
ASP.NET網(wǎng)站聊天室的設(shè)計與實現(xiàn)(第3節(jié))
這篇文章主要介紹了ASP.NET網(wǎng)站聊天室的設(shè)計與實現(xiàn),了解Session、Application對象的屬性和事件,并且掌握利用它們在頁面間保存和傳遞數(shù)據(jù)的方法,需要的朋友可以參考下2015-08-08.net中如何以純二進制的形式在內(nèi)存中繪制一個對象
這篇文章主要介紹了如何以純二進制的形式在內(nèi)存中繪制一個對象,本文通過實例代碼給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-07-07在ASP.NET Core5.0中訪問HttpContext的方法步驟
這篇文章主要介紹了在ASP.NET Core5.0中訪問HttpContext的方法步驟,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-11-11ASP.NET Core MVC 中實現(xiàn)中英文切換的示例代碼
這篇文章主要介紹了ASP.NET Core MVC 中實現(xiàn)中英文切換的示例代碼,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-02-02利用ASP.NET MVC+Bootstrap搭建個人博客之打造清新分頁Helper(三)
這篇文章主要介紹了利用ASP.NET MVC+Bootstrap搭建個人博客之打造清新分頁Helper(三)的相關(guān)資料,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2016-06-06asp.net下無法循環(huán)綁定投票的標(biāo)題和選項的解決方法
asp.net下無法循環(huán)綁定投票的標(biāo)題和選項與無法循環(huán)獲得用戶的選擇的解決方法。2010-12-12