【從零開始搭建自己的.NET Core Api框架】(七)授權認證進階篇

来源:https://www.cnblogs.com/RayWang/archive/2018/08/29/9536524.html
-Advertisement-
Play Games

系列目錄 一. 創建項目並集成swagger 1.1 創建 1.2 完善 二. 搭建項目整體架構 三. 集成輕量級ORM框架——SqlSugar 3.1 搭建環境 3.2 實戰篇:利用SqlSugar快速實現CRUD 3.3 生成實體類 四. 集成JWT授權驗證 五. 實現CORS跨域 六. 集成泛 ...


系列目錄

.  創建項目並集成swagger

  1.1 創建

  1.2 完善

二. 搭建項目整體架構

三. 集成輕量級ORM框架——SqlSugar

  3.1 搭建環境

  3.2 實戰篇:利用SqlSugar快速實現CRUD

  3.3 生成實體類

四. 集成JWT授權驗證

五. 實現CORS跨域

六. 集成泛型倉儲

七. 授權認證進階篇

 


 源碼稍後上傳GitHub~

該篇是第四篇“實戰!帶你半小時實現介面的授權認證”的進階篇。

先說一下之前的版本:

之前在第四篇的時候曾經試著集成過一次JWT授權認證,當時搭的第一版是JWT本身的授權認證機制,但是為了實現“令牌”的滑動過期效果,結果最後改成了使用緩存機制。

所以寫到最後發現,其實就是變相的Session認證機制,因為發放“令牌”的時候完全可以不用JWT,直接生成一個GUID也是可以的。

後來想了一下,這樣為了實現令牌滑動過期而破壞了授權認證的獨立性,感覺得不償失。於是就決定”進階“下,在授權認證模塊去掉緩存機制,只使用JWT本身的驗證機制。

另外,這次還添加了一些關於對身份驗證的優化。之前一個介面只能標明允許一種身份的用戶訪問,修改後可以實現一個介面同時允許多個身份訪問(比如同時允許客戶端和後臺管理員兩種身份的令牌訪問)。

 BTW,為了完整性考慮,下麵有部分內容和之前第四篇相同,有需要的可以跳著看。

  1. 根

根據維基百科定義,JWT(讀作 [/dʒɒt/]),即JSON Web Tokens,是一種基於JSON的、用於在網路上聲明某種主張的令牌(token)規範。

JWT通常由三部分組成: 頭信息(header), 消息體(payload)和簽名(signature)。它是一種用於雙方之間傳遞安全信息的表述性聲明規範。

JWT作為一個開放的標準(RFC 7519),定義了一種簡潔的、自包含的方法,從而使通信雙方實現以JSON對象的形式安全的傳遞信息。

 

以上是JWT的官方解釋,可以看出JWT並不是一種只能許可權驗證的工具,而是一種標準化的數據傳輸規範。所以,只要是在系統之間需要傳輸簡短但卻需要一定安全等級的數據時,都可以使用JWT規範來傳輸。規範是不因平臺而受限制的,這也是JWT做為授權驗證可以跨平臺的原因。

如果理解還是有困難的話,我們可以拿JWT和JSON類比:

JSON是一種輕量級的數據交換格式,是一種數據層次結構規範。它並不是只用來給介面傳遞數據的工具,只要有層級結構的數據都可以使用JSON來存儲和表示。當然,JSON也是跨平臺的,不管是Win還是Linux,.NET還是Java,都可以使用它作為數據傳輸形式。

 

該篇的主要目的是實戰,所以關於JWT本身的優點,以及使用JWT作為系統授權驗證的優缺點,這裡就不細說了,感興趣的可以自己去查閱相關資料。

 

 1.1 在授權驗證系統中,JWT是怎麼工作的呢?

如果將JWT運用到Web Api的授權驗證中,那麼它的工作原理是這樣的:

 

1)客戶端向授權服務系統發起請求,申請獲取“令牌”。

2)授權服務根據用戶身份,生成一張專屬“令牌”,並將該“令牌”以JWT規範返回給客戶端

3)客戶端將獲取到的“令牌”放到http請求的headers中後,向主服務系統發起請求。主服務系統收到請求後會從headers中獲取“令牌”,並從“令牌”中解析出該用戶的身份許可權,然後做出相應的處理(同意或拒絕返回資源)

 

可以看出,JWT授權服務是可以脫離我們的主服務系統而作為一個獨立系統存在的。

 1.2 令牌是什麼?JWT就是令牌嗎?

前面說了其實把JWT理解為一種規範更為貼切,但是往往大家把根據JWT規則生成的加密字元串也叫作JWT,還有人直接稱呼JWT為令牌。本文為了闡述方便,特此做了一些區分:

 1.2.1 JWT:

本文所說的JWT皆指的是JWT規範

 1.2.2 JWT字元串:

本文所說的“JWT字元串”是指通過JWT規則加密後生成的字元串,它由三部分組成:Header(頭部)、Payload(數據)、Signature(簽名),將這三部分由‘.’連接而組成的一長串加密字元串就成為JWT字元串。

1)Header

由且只由兩個數據組成,一個是“alg”(加密規範)指定了該JWT字元串的加密規則,另一個是“typ”(JWT字元串類型)。例如:

{
  "alg": "HS256",
  "typ": "JWT"
}

將這組JSON格式的數據通過Base64Url格式編碼後,生成的字元串就是我們JWT字元串的第一個部分。

2)Payload

由一組數據組成,它負責傳遞數據,我們可以添加一些已註冊聲明,比如“iss”(JWT字元串的頒發人名稱)、“exp”(該JWT字元串的過期時間)、“sub”(身份)、“aud”(受眾),除了這些,我們還可根據需要添加自定義的需要傳輸的數據,一般是發起請求的用戶的信息。例如:

{
  “iss”:"RayPI",
  "sub": "Client",
  "name": "張三",
  "uid": 1
}

將該JSON格式的數據通過Base64Url格式編碼後,生成的字元串就是我們JWT字元串的第二部分。

3)Signature

數字簽名,由4個因素所同時決定:編碼後的header字元串,編碼後的payload字元串,之前在頭部聲明的加密演算法,我們自定義的一個秘鑰字元串(secret)。例如:

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

所以簽名可以安全地驗證一個JWT的合法性(有沒有被篡改過)。

最後,給一個實際生成後的JWT字元串的完整樣例:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJDbGllbnQiLCJqdGkiOiIwZTRjYzVkNC0yMmIzLTQwYzUtOTBjMy0wOTk0MjFjNWRjMjkiLCJpYXQiOiIyMDE4LzcvMyAyOjE3OjQ5IiwiZXhwIjoxNTMwNjI3NDY5LCJpc3MiOiJSYXlQSSJ9.98pAaDVhNwVfiSHQVeXKhYE2ML6WK_f9rYC-iwyQEpU

我們可以拿著這個JWT字元串到https://jwt.io/#debugger試著解析出前兩部分的內容。

 1.2.3 令牌:

本文的“令牌”指的是用於http傳輸headers中用於驗證授權的JSON數據,它是key和value兩部分組成,在本文中,key為“Authorization”,value為“Bearer {JWT字元串}”,其中value除了JWT字元串外,還在前面添加了“Bearer ”字元串,這裡可以把它理解為大家約約定俗成的規定即可,沒有實際的作用。例如:

{ "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJDbGllbnQiLCJqdGkiOiIwZTRjYzVkNC0yMmIzLTQwYzUtOTBjMy0wOTk0MjFjNWRjMjkiLCJpYXQiOiIyMDE4LzcvMyAyOjE3OjQ5IiwiZXhwIjoxNTMwNjI3NDY5LCJpc3MiOiJSYXlQSSJ9.98pAaDVhNwVfiSHQVeXKhYE2ML6WK_f9rYC-iwyQEpU" }

 1.3 授權?認證?傻傻分不清楚

先問一個問題:認證和授權是一回事嗎?

答案顯然是否定的。因為大家都喜歡把這兩個詞放在一起說,所以很容易就混淆了他們含義。在.NET Core中,認證的單詞是“Authentication”,而授權的單詞是“Authorization”。

認證,驗證身份的意思。即驗證當前請求的用戶是否為合法用戶(放在當前場景下,就是驗證用戶攜帶的令牌是否為一個合法令牌);

授權,給用戶頒發許可權的意思。即給驗證通過的用戶授予相應的許可權(放在當前場景下,就是根據令牌中解析出的用戶身份,賦予該http請求,該http請求使用該身份就可以訪問對應的介面)

所以,我們下麵要實現的總體思路是:

在每個介面上都標明該介面允許什麼樣的身份訪問(比如“Client”代表客戶端,“Admin”代表後臺管理員)。

在用戶登錄成功後,我們將該用戶的身份(是Client還是Admin)等信息生成JWT規範的令牌返回。客戶端將返回的令牌存儲好(一般是存在Cookie中),以後每次調用介面都要將該令牌攜帶上。

服務端收到請求後,提取令牌,先進行認證,如果不合法(比如被篡改),將駁回請求。如果認證通過,則從令牌中提取身份,進行授權操作,將該身份賦予http請求,放行請求。

請求進到介面後會和介面標明的允許訪問身份進行匹配,如果該介面允許該身份訪問,則返回相應請求資源,如果不允許,則駁回請求。

 

思路明白了,下麵實戰起來就不會亂了。

  2. 道

 搭建完的項目架構應該是這樣的:

 

這裡有三塊工作區域:

1)JwtHelper是一個Jwt幫助類,裡面有兩個函數,一個函數幫助生成Jwt字元串並返回,一個幫助從Jwt字元串逆向解析出數據。

2)JwtAuthorizationFilter.cs是一個授權中間件

3)Startup中添加了認證服務和授權服務

 

2.1 JwtHelper

using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
//
using RayPI.Model.ConfigModel;

namespace RayPI.Helper
{
    public class JwtHelper
    {
        /// <summary>
        /// 頒發JWT字元串
        /// </summary>
        /// <param name="tokenModel"></param>
        /// <returns></returns>
        public static string IssueJWT(TokenModel tokenModel)
        {
            var dateTime = DateTime.UtcNow;
            var claims = new Claim[]
            {
                new Claim(JwtRegisteredClaimNames.Jti,tokenModel.Uid.ToString()),//用戶Id
                new Claim("Role", tokenModel.Role),//身份
                new Claim("Project", tokenModel.Project),//身份
                new Claim(JwtRegisteredClaimNames.Iat,dateTime.ToString(),ClaimValueTypes.Integer64)
            };
            //秘鑰
            var jwtConfig = new JwtAuthConfigModel();
            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtConfig.JWTSecretKey));
            var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
            //過期時間
            double exp = 0;
            switch (tokenModel.TokenType)
            {
                case "Web":
                    exp = jwtConfig.WebExp;
                    break;
                case "App":
                    exp = jwtConfig.AppExp;
                    break;
                case "MiniProgram":
                    exp = jwtConfig.MiniProgramExp;
                    break;
                case "Other":
                    exp = jwtConfig.OtherExp;
                    break;
            }
            var jwt = new JwtSecurityToken(
                issuer: "RayPI",
                claims: claims, //聲明集合
                expires: dateTime.AddHours(exp),
                signingCredentials: creds);

            var jwtHandler = new JwtSecurityTokenHandler();
            var encodedJwt = jwtHandler.WriteToken(jwt);

            return encodedJwt;
        }

        /// <summary>
        /// 解析
        /// </summary>
        /// <param name="jwtStr"></param>
        /// <returns></returns>
        public static TokenModel SerializeJWT(string jwtStr)
        {
            var jwtHandler = new JwtSecurityTokenHandler();
            JwtSecurityToken jwtToken = jwtHandler.ReadJwtToken(jwtStr);
            object role = new object(); ;
            object project = new object();
            try
            {
                jwtToken.Payload.TryGetValue("Role", out role);
                jwtToken.Payload.TryGetValue("Project", out project);
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                throw;
            }
            var tm = new TokenModel
            {
                Uid = long.Parse(jwtToken.Id),
                Role = role.ToString(),
                Project = project.ToString()
            };
            return tm;
        }
    }

    /// <summary>
    /// 令牌
    /// </summary>
    public class TokenModel
    {
        /// <summary>
        /// 用戶Id
        /// </summary>
        public long Uid { get; set; }
        /// <summary>
        /// 身份
        /// </summary>
        public string Role { get; set; }
        /// <summary>
        /// 項目名稱
        /// </summary>
        public string Project { get; set; }
        /// <summary>
        /// 令牌類型
        /// </summary>
        public string TokenType { get; set; }
    }
}
JwtHelper

 

其中JwtAuthConfigModel是一個存儲配置文件的類,裡面讀取了配置文件中的jwt秘鑰和幾個過期時間。這裡就不放了,可以在代碼里直接寫死。

2.2. JwtAuthorizationFilter

using Microsoft.AspNetCore.Http;
using RayPI.Bussiness;
using RayPI.Helper;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;

namespace RayPI.AuthHelp
{
    /// <summary>
    /// 授權中間件
    /// </summary>
    public class JwtAuthorizationFilter
    {
        private readonly RequestDelegate _next;

        /// <summary>
        /// 
        /// </summary>
        /// <param name="next"></param>
        public JwtAuthorizationFilter(RequestDelegate next)
        {
            _next = next;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="httpContext"></param>
        /// <returns></returns>
        public Task Invoke(HttpContext httpContext)
        {
            //檢測是否包含'Authorization'請求頭,如果不包含則直接放行
            if (!httpContext.Request.Headers.ContainsKey("Authorization"))
            {
                return _next(httpContext);
            }
            var tokenHeader = httpContext.Request.Headers["Authorization"];
            tokenHeader = tokenHeader.ToString().Substring("Bearer ".Length).Trim();

            TokenModel tm = JwtHelper.SerializeJWT(tokenHeader);

            //BaseBLL.TokenModel = tm;//將tokenModel存入baseBll

            //授權
            var claimList = new List<Claim>();
            var claim = new Claim(ClaimTypes.Role, tm.Role);
            claimList.Add(claim);
            var identity = new ClaimsIdentity(claimList);
            var principal = new ClaimsPrincipal(identity);
            httpContext.User = principal;

            return _next(httpContext);
        }
    }
}
JwtAuthorizationFilter

 

2.3 Startup

完整代碼:

using System.Collections.Generic;
using System.IO;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.PlatformAbstractions;
using Swashbuckle.AspNetCore.Swagger;
using RayPI.Model.ConfigModel;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using RayPI.AuthHelp;

namespace RayPI
{
    /// <summary>
    /// 
    /// </summary>
    public class Startup
    {
        /// <summary>
        /// 
        /// </summary>
        /// <param name="env"></param>
        public Startup(IHostingEnvironment env)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);

            this.Configuration = builder.Build();

            BaseConfigModel.SetBaseConfig(Configuration);
        }
        /// <summary>
        /// 
        /// </summary>
        public IConfiguration Configuration { get; }

        /// <summary>
        /// This method gets called by the runtime. Use this method to add services to the container.
        /// </summary>
        /// <param name="services"></param>
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc().AddJsonOptions(options =>
            {
                options.SerializerSettings.DateFormatString = "yyyy-MM-dd HH:mm:ss";//設置時間格式
            });

            #region Swagger
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new Info
                {
                    Version = "v1.1.0",
                    Title = "Ray WebAPI",
                    Description = "框架集合",
                    TermsOfService = "None",
                    Contact = new Swashbuckle.AspNetCore.Swagger.Contact { Name = "RayWang", Email = "[email protected]", Url = "http://www.cnblogs.com/RayWang" }
                });
                //添加註釋服務
                var basePath = PlatformServices.Default.Application.ApplicationBasePath;
                var apiXmlPath = Path.Combine(basePath, "APIHelp.xml");
                var entityXmlPath = Path.Combine(basePath, "EntityHelp.xml"); 
                c.IncludeXmlComments(apiXmlPath, true);//控制器層註釋(true表示顯示控制器註釋)
                c.IncludeXmlComments(entityXmlPath);

                //添加控制器註釋
                //c.DocumentFilter<SwaggerDocTag>();

                //添加header驗證信息
                //c.OperationFilter<SwaggerHeader>();
                var security = new Dictionary<string, IEnumerable<string>> { { "Bearer", new string[] { } }, };
                c.AddSecurityRequirement(security);//添加一個必須的全局安全信息,和AddSecurityDefinition方法指定的方案名稱要一致,這裡是Bearer。
                c.AddSecurityDefinition("Bearer", new ApiKeyScheme
                {
                    Description = "JWT授權(數據將在請求頭中進行傳輸) 參數結構: \"Authorization: Bearer {token}\"",
                    Name = "Authorization",//jwt預設的參數名稱
                    In = "header",//jwt預設存放Authorization信息的位置(請求頭中)
                    Type = "apiKey"
                });
            });
            #endregion

            #region 認證
            services.AddAuthentication(x =>
                {
                    x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                    x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
                })
                .AddJwtBearer(o =>
                {
                    JwtAuthConfigModel jwtConfig=new JwtAuthConfigModel();
                    o.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidIssuer = "RayPI",
                        ValidAudience = "wr",
                        IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtConfig.JWTSecretKey)),

                        /***********************************TokenValidationParameters的參數預設值***********************************/
                        RequireSignedTokens = true,
                        // SaveSigninToken = false,
                        // ValidateActor = false,
                        // 將下麵兩個參數設置為false,可以不驗證Issuer和Audience,但是不建議這樣做。
                        ValidateAudience = false,
                        ValidateIssuer = true,
                        ValidateIssuerSigningKey = true,
                        // 是否要求Token的Claims中必須包含 Expires
                        RequireExpirationTime = true,
                        // 允許的伺服器時間偏移量
                        // ClockSkew = TimeSpan.FromSeconds(300),
                        // 是否驗證Token有效期,使用當前時間與Token的Claims中的NotBefore和Expires對比
                        ValidateLifetime = true
                    };
                });
            #endregion

            #region 授權
            services.AddAuthorization(options =>
            {
                options.AddPolicy("RequireClient", policy => policy.RequireRole("Client").Build());
                options.AddPolicy("RequireAdmin", policy => policy.RequireRole("Admin").Build());
                options.AddPolicy("RequireAdminOrClient", policy => policy.RequireRole("Admin,Client").Build());
            });
            #endregion

            #region CORS
            services.AddCors(c =>
            {
                c.AddPolicy("Any", policy =>
                 {
                     policy.AllowAnyOrigin()
                     .AllowAnyMethod()
                     .AllowAnyHeader()
                     .AllowCredentials();
                 });

                c.AddPolicy("Limit", policy =>
                 {
                     policy
                     .WithOrigins("localhost:8083")
                     .WithMethods("get", "post", "put", "delete")
                     //.WithHeaders("Authorization");
                     .AllowAnyHeader();
                 });
            });
            #endregion
        }

        /// <summary>
        /// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        /// </summary>
        /// <param name="app"></param>
        /// <param name="env"></param>
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            #region Swagger
            app.UseSwagger();
            app.UseSwaggerUI(c =>
            {
                c.SwaggerEndpoint("/swagger/v1/swagger.json", "ApiHelp V1");
            });
            #endregion

            //認證
            app.UseAuthentication();

            //授權
            app.UseMiddleware<JwtAuthorizationFilter>();

            app.UseMvc();

            app.UseStaticFiles();//用於訪問wwwroot下的文件 
        }
    }
}
Startup 

Tips:

這裡有一個坑,不太瞭解依賴註入和中間件的人很容易踩到(其實就是我自己了)

在Startup.cs的Configure函數中,裡面每個app.UseXXXXX();是有一定順序。可以理解為,這裡添加中間件的順序就是客戶端發起http請求時所經過的順序。

之前我因為把“app.UseMvc();”寫到了認證授權上面去了,結果導致怎麼Debug都找不到問題。。。

所以一定要先app.UseAuthentication()認證,然後app.UseMiddleware<JwtAuthorizationFilter>()授權,最後再app.UseMvc()

 

  3.果

 搭建完成之後,下麵就是測試了。

選擇一個測試控制器,在其頭上標註[Authorize]屬性

(Policy和Roles兩種寫法是一個意思,但是必須要是Startup中已經申明的,否則Swagger會直接報錯)

 

 F5運行,在swagger ui中調用一個需要授權驗證的介面(根據Id獲取學生信息)

 

輸入1,先不進行任何授權認證的操作,直接點擊Excute嘗試調用,返回結果如下:

 

狀態碼500,還返回了一大段html代碼,我們可以將介面的完整地址輸入到瀏覽器地址欄進行訪問,就可以看到這段html代碼的頁面了:

 

可以看到介面返回了一個錯誤頁,原因就是中間件在http請求的頭部(headers)中沒有找到“Authorization"欄位里的”令牌“。

 

現在,我們先調用獲取JWT介面(實際項目中不應該有該介面,分發令牌的功能應該集成到登陸功能中,但是這裡為了簡單直觀,我將分發令牌的功能直接寫成了介面,以供測試),輸入相應的客戶端信息,Excute:

 

 

介面會生成”令牌“,返回JWT字元串:

 

我們要複製這串JWT字元串,然後將其添加到http請求的Headers中去。測試方法有兩個:

 

1)可以新建一個html頁面,模擬前端寫個ajax調用介面,在ajax添加headers欄位,如下:

$.ajax({
                url: "http://localhost:3608/api/Admin/Student/1",
                type: ”get“,
                dataType: "json",
                //data: {},
                async: false,
          //手動高亮 headers: {
"Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJBZG1pbiIsImp0aSI6IjhjMDEwMzI2LTE4M2MtNGQ5ZC1iMDFjLWFjM2EzNTIzODYxOCIsImlhdCI6IjIwMTgvNy8yIDE1OjAzOjQ4IiwiZXhwIjoxNTMwNTg3MDI4LCJpc3MiOiJSYXlQSSJ9.1Bb7hwoDD12n8ymcQsu79Xm-GDq14GERhS9b-1l1kmg" }, success: function (d) { alert(JSON.stringify(d)); }, error: function (d) { alert(JSON.stringify(d)) } });

 

2)如果你的swagger像我一樣,集成了添加Authrize頭部功能,那麼可以點擊這個按鈕進行添加(如果你的swagger看不到這個按鈕,可以參考我之前的章節【從零開始搭建自己的.NET Core Api框架】(一)創建項目並集成swagger:1.2 完善,對swagger進行相關的設置)

 

這裡除了JWT字元串外,前面還需要手動寫入“Bearer ”(有一個空格)字元串。點擊Authorize保存"令牌"。

 

再次調用剛纔的”根據id獲取學生信息“介面,發現獲取成功:

可以看到swagger向http請求的headers中添加了我們剛纔保存的”令牌“。

 

參考內容:

https://jwt.io/

https://www.cnblogs.com/webenh/p/9039322.html


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • IO多路復用實現併發伺服器 IO多路復用技術 我們把socket交給操作系統去監控 epoll 是惰性的事件回調:惰性事件回調 是由用戶進程 自己調用的,操作系統只起到 通知的作用,目前Linux上效率最高的 IO多路復用 技術。 併發服務實現: 服務端 客戶端 ...
  • Java常見報錯信息: Java 常見異常種類 Java Exception: 1、Error 2、Runtime Exception 運行時異常 3、Exception 4、throw 用戶自定義異常 異常類分兩大類型:Error類代表了編譯和系統的錯誤,不允許捕獲;Exception類代表了標準 ...
  • [TOC] 0. 序言 工作的需要,最近在接手一個C++項目。自己在校學習期間,因為懶惰,對於C++這樣的巨型語言,是能躲就躲的,因此學的一知半解,導致現在工作時的無能為力。但是,指責所在,躲無可躲,只能做一些亡羊補牢之事。下麵的一系列文章,就是自己邊工作邊學習的記錄,算是學步時的點滴。內容會持續更 ...
  • 1 //漢字字元轉數組 2 function chStrToArray($str){ 3 $length = mb_strlen($str, 'utf-8'); 4 $array = array(); 5 for ($i=0; $i<$length; $i++) 6 $array[] = mb_su... ...
  • 郵箱是Actor模型的一個重要組成部分,負責接收發過來的消息,並保存起來,等待Actor處理。郵箱中維護著兩種隊列,一種是存系統消息,另一個是存用戶消息,系統省是指Started,Stoping,Stoped之類的,用戶當然指我們自定義的Actor。 另外,我們可以通過實現IMailboxStati... ...
  • 前言 萬事開頭難,很早之前就想寫博客記錄些東西,遲遲未行動,甚是遺憾。原因諸多,大體上無非都是懶、沒意志力等等。這次從自己的讀書筆記開始,興許能夠有所改變。 一、CLR概念 CLR(Common Language Runtime,譯為公共語言運行時)是一個可由多種編程語言使用的“運行時”。CLR的核 ...
  • 作者:依樂祝 原文地址:https://www.cnblogs.com/yilezhu/p/9557375.html 簡單的說Ocelot是一個用.NET Core實現並且開源的API網關技術。 可能你又要問了,什麼是API網關技術呢?Ocelot又有什麼特別呢?我們又該如何集成到我們的asp.ne ...
  • 軟體系統最大的價值在於用心去幫客戶解決各種痛點,需要做好方方面面的工作,數據的快捷搜索就是其中比較重要的一個環節。那麼,什麼樣的搜索方式才是最高效快捷的呢?目前能想到最好的辦法是用名稱聲母檢索,用聲母檢索輸入快、效率高,自然可能成為軟體的一大亮點。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...