從Client應用場景介紹IdentityServer4(五)

来源:https://www.cnblogs.com/FireworksEasyCool/archive/2018/12/27/10181681.html
-Advertisement-
Play Games

本節將在第四節基礎上介紹如何實現IdentityServer4從資料庫獲取User進行驗證,並對Claim進行許可權設置。 一、新建Web API資源服務,命名為ResourceAPI (1)新建API項目,用來進行user的身份驗證服務。 (2)配置埠為5001 安裝Microsoft.Entit ...


本節將在第四節基礎上介紹如何實現IdentityServer4從資料庫獲取User進行驗證,並對Claim進行許可權設置。


一、新建Web API資源服務,命名為ResourceAPI

(1)新建API項目,用來進行user的身份驗證服務。

(2)配置埠為5001

安裝Microsoft.EntityFrameworkCore

安裝Microsoft.EntityFrameworkCore.SqlServer

安裝Microsoft.EntityFrameworkCore.Tools

(3)我們在項目添加一個 Entities文件夾。

新建一個User類,存放用戶基本信息,其中Claims為一對多的關係。

其中UserId的值是唯一的。

 public class User
    {
        [Key]
        [MaxLength(32)]
        public string UserId { get; set; }

        [MaxLength(32)]
        public string UserName { get; set; }

        [MaxLength(50)]
        public string Password { get; set; }

        public bool IsActive { get; set; }//是否可用

        public virtual ICollection<Claims> Claims { get; set; }

}

新建Claims類

public class Claims
    {
        [MaxLength(32)]
        public int ClaimsId { get; set; }

        [MaxLength(32)]
        public string Type { get; set; }

        [MaxLength(32)]
        public string Value { get; set; }

        public virtual User User { get; set; }

    }

繼續新建 UserContext.cs

public class UserContext:DbContext
    {

        public UserContext(DbContextOptions<UserContext> options)
            : base(options)
        {
        }
        public DbSet<User> Users { get; set; }
        public DbSet<Claims> UserClaims { get; set; }
}

(4)修改startup.cs中的ConfigureServices方法,添加SQL Server配置。

public void ConfigureServices(IServiceCollection services)
        {
            var connection = "Data Source=localhost;Initial Catalog=UserAuth;User ID=sa;Password=Pwd";
            services.AddDbContext<UserContext>(options => options.UseSqlServer(connection));
            // Add framework services.
            services.AddMvc();
        }

完成後在程式包管理器控制台運行:Add-Migration InitUserAuth

生成遷移文件。

(5)添加Models文件夾,定義User的model類和Claims的model類。

在Models文件夾中新建User類:

public class User
    {
        public string UserId { get; set; }

        public string UserName { get; set; }

        public string Password { get; set; }

        public bool IsActive { get; set; }

        public ICollection<Claims> Claims { get; set; } = new HashSet<Claims>();
}

新建Claims類:

public class Claims
    {
        public Claims(string type,string value)
        {
            Type = type;
            Value = value;
        }
        public string Type { get; set; }
        public string Value { get; set; }
    }

做Model和Entity之前的映射。

添加類UserMappers:

public static class UserMappers
    {
        static UserMappers()
        {
            Mapper = new MapperConfiguration(cfg => cfg.AddProfile<UserContextProfile>())
                .CreateMapper();
        }
        internal static IMapper Mapper { get; }

        /// <summary>
        /// Maps an entity to a model.
        /// </summary>
        /// <param name="entity">The entity.</param>
        /// <returns></returns>
        public static Models.User ToModel(this User entity)
        {
            return Mapper.Map<Models.User>(entity);
        }

        /// <summary>
        /// Maps a model to an entity.
        /// </summary>
        /// <param name="model">The model.</param>
        /// <returns></returns>
        public static User ToEntity(this Models.User model)
        {
            return Mapper.Map<User>(model);
        }
    }

類UserContextProfile:

public class UserContextProfile: Profile
    {
        public UserContextProfile()
        {
            //entity to model
            CreateMap<User, Models.User>(MemberList.Destination)
                .ForMember(x => x.Claims, opt => opt.MapFrom(src => src.Claims.Select(x => new Models.Claims(x.Type, x.Value))));

            //model to entity
            CreateMap<Models.User, User>(MemberList.Source)
                .ForMember(x => x.Claims,
                    opt => opt.MapFrom(src => src.Claims.Select(x => new Claims { Type = x.Type, Value = x.Value })));
        }
    }

(6)在startup.cs中添加初始化資料庫的方法InitDataBase方法,對User和Claim做級聯插入。

 public void InitDataBase(IApplicationBuilder app)
        {

            using (var serviceScope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope())
            {
                serviceScope.ServiceProvider.GetRequiredService<Entities.UserContext>().Database.Migrate();

                var context = serviceScope.ServiceProvider.GetRequiredService<Entities.UserContext>();
                context.Database.Migrate();
                if (!context.Users.Any())
                {
                    User user = new User()
                    {
                        UserId = "1",
                        UserName = "zhubingjian",
                        Password = "123",
                        IsActive = true,
                        Claims = new List<Claims>
                        {
                            new Claims("role","admin")
                        }
                    };
                    context.Users.Add(user.ToEntity());
                    context.SaveChanges();
                }
            }
        }

(7)在startup.cs中添加InitDataBase方法的引用。

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            InitDataBase(app);
            app.UseMvc();
        }

運行程式,這時候數據生成資料庫UserAuth,表Users中有一條UserName=zhubingjian,Password=123的數據。


 

二、實現獲取User介面,進行身份驗證

(1)先對API進行保護,在Startup.cs的ConfigureServices方法中添加:

            //protect API
            services.AddMvcCore()
            .AddAuthorization()
            .AddJsonFormatters();

            services.AddAuthentication("Bearer")
                .AddIdentityServerAuthentication(options =>
                {
                    options.Authority = "http://localhost:5000";
                    options.RequireHttpsMetadata = false;

                    options.ApiName = "api1";
                });

併在Configure中,將UseAuthentication身份驗證中間件添加到管道中,以便在每次調用主機時自動執行身份驗證。

app.UseAuthentication();

(2)接著,實現獲取User的介面。

在ValuesController控制中,添加如下代碼:

UserContext context;
        public ValuesController(UserContext _context)
        {
            context = _context;
        }

//只接受role為AuthServer授權服務的請求
[Authorize(Roles = "AuthServer")]
        [HttpGet("{userName}/{password}")]
        public IActionResult AuthUser(string userName, string password)
        {
           var res = context.Users.Where(p => p.UserName == userName && p.Password == password)
                .Include(p=>p.Claims)
                .FirstOrDefault();
            return Ok(res.ToModel());
        }

好了,資源伺服器獲取User的介面完成了。

(3)接著回到AuthServer項目,把User改成從資料庫進行驗證。

找到AccountController控制器,把從記憶體驗證User部分修改成從資料庫驗證。

主要修改Login方法,代碼給出了簡要註釋:

 public async Task<IActionResult> Login(LoginInputModel model, string button)
        {
            // check if we are in the context of an authorization request
            AuthorizationRequest context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl);

            // the user clicked the "cancel" button
            if (button != "login")
            {
                if (context != null)
                {
                    // if the user cancels, send a result back into IdentityServer as if they 
                    // denied the consent (even if this client does not require consent).
                    // this will send back an access denied OIDC error response to the client.
                    await _interaction.GrantConsentAsync(context, ConsentResponse.Denied);

                    // we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
                    if (await _clientStore.IsPkceClientAsync(context.ClientId))
                    {
                        // if the client is PKCE then we assume it's native, so this change in how to
                        // return the response is for better UX for the end user.
                        return View("Redirect", new RedirectViewModel { RedirectUrl = model.ReturnUrl });
                    }

                    return Redirect(model.ReturnUrl);
                }
                else
                {
                    // since we don't have a valid context, then we just go back to the home page
                    return Redirect("~/");
                }
            }

            if (ModelState.IsValid)
            {
                //從資料庫獲取User併進行驗證
                var client = _httpClientFactory.CreateClient();
                //已過時
                DiscoveryResponse disco = await DiscoveryClient.GetAsync("http://localhost:5000");
                TokenClient tokenClient = new TokenClient(disco.TokenEndpoint, "AuthServer", "secret");
                var tokenResponse = await tokenClient.RequestClientCredentialsAsync("api1");

                //var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
                //{
                //    Address = "http://localhost:5000",
                //    ClientId = "AuthServer",
                //    ClientSecret = "secret",
                //    Scope = "api1"
                //});
                //if (tokenResponse.IsError) throw new Exception(tokenResponse.Error);
                client.SetBearerToken(tokenResponse.AccessToken);

                try
                {
                    var response = await client.GetAsync("http://localhost:5001/api/values/" + model.Username + "/" + model.Password);
                    if (!response.IsSuccessStatusCode)
                    {
                        throw new Exception("Resource server is not working!");
                    }
                    else
                    {
                        var content = await response.Content.ReadAsStringAsync();
                        User user = JsonConvert.DeserializeObject<User>(content);
                        if (user != null)
                        {
                            await _events.RaiseAsync(new UserLoginSuccessEvent(user.UserName, user.UserId, user.UserName));

                            // only set explicit expiration here if user chooses "remember me". 
                            // otherwise we rely upon expiration configured in cookie middleware.
                            AuthenticationProperties props = null;
                            if (AccountOptions.AllowRememberLogin && model.RememberLogin)
                            {
                                props = new AuthenticationProperties
                                {
                                    IsPersistent = true,
                                    ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration)
                                };
                            };

                            //             context.Result = new GrantValidationResult(
                            //user.SubjectId ?? throw new ArgumentException("Subject ID not set", nameof(user.SubjectId)),
                            //OidcConstants.AuthenticationMethods.Password, _clock.UtcNow.UtcDateTime,
                            //user.Claims);

                            // issue authentication cookie with subject ID and username
                            await HttpContext.SignInAsync(user.UserId, user.UserName, props);

                            if (context != null)
                            {
                                if (await _clientStore.IsPkceClientAsync(context.ClientId))
                                {
                                    // if the client is PKCE then we assume it's native, so this change in how to
                                    // return the response is for better UX for the end user.
                                    return View("Redirect", new RedirectViewModel { RedirectUrl = model.ReturnUrl });
                                }

                                // we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
                                return Redirect(model.ReturnUrl);
                            }

                            // request for a local page
                            if (Url.IsLocalUrl(model.ReturnUrl))
                            {
                                return Redirect(model.ReturnUrl);
                            }
                            else if (string.IsNullOrEmpty(model.ReturnUrl))
                            {
                                return Redirect("~/");
                            }
                            else
                            {
                                // user might have clicked on a malicious link - should be logged
                                throw new Exception("invalid return URL");
                            }
                        }

                        await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials"));
                        ModelState.AddModelError("", AccountOptions.InvalidCredentialsErrorMessage);
                    }
                }
                catch (Exception ex)
                {
                    await _events.RaiseAsync(new UserLoginFailureEvent("Resource server", "is not working!"));
                    ModelState.AddModelError("", "Resource server is not working");
                }

            }

            // something went wrong, show form with error
            var vm = await BuildLoginViewModelAsync(model);
            return View(vm);
        }

可以看到,在IdentityServer4更新後,舊版獲取tokenResponse的方法已過時,但我按官網文檔的說明,使用新方法(註釋的代碼),獲取不到信息,還望大家指點。

官網鏈接:https://identitymodel.readthedocs.io/en/latest/client/token.html

所以這裡還是按老方法來獲取tokenResponse。

(4)到這步後,可以把Startup中ConfigureServices方法裡面的AddTestUsers去掉了。

運行程式,已經可以從數據進行User驗證了。

點擊進入About頁面時候,出現沒有許可權提示,我們會發現從資料庫獲取的User中的Claims不起作用了。


 

三、使用數據數據自定義Claim

為了讓獲取的Claims起作用,我們來實現IresourceOwnerPasswordValidator介面和IprofileService介面。

(1)在AuthServer中添加類ResourceOwnerPasswordValidator,繼承IresourceOwnerPasswordValidator介面。

 public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
    {
        private readonly IHttpClientFactory _httpClientFactory;
        public ResourceOwnerPasswordValidator(IHttpClientFactory httpClientFactory)
        {
            _httpClientFactory = httpClientFactory;
        }
        public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
        {
            try
            {
                var client = _httpClientFactory.CreateClient();
                //已過時
                DiscoveryResponse disco = await DiscoveryClient.GetAsync("http://localhost:5000");
                TokenClient tokenClient = new TokenClient(disco.TokenEndpoint, "AuthServer", "secret");
                var tokenResponse = await tokenClient.RequestClientCredentialsAsync("api1");

                //var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
                //{
                //    Address = "http://localhost:5000",
                //    ClientId = "AuthServer",
                //    ClientSecret = "secret",
                //    Scope = "api1"
                //});
                //if (TokenResponse.IsError) throw new Exception(TokenResponse.Error);
                client.SetBearerToken(tokenResponse.AccessToken);

                var response = await client.GetAsync("http://localhost:5001/api/values/" + context.UserName + "/" + context.Password);
                if (!response.IsSuccessStatusCode)
                {
                    throw new Exception("Resource server is not working!");
                }
                else
                {
                    var content = await response.Content.ReadAsStringAsync();
                    User user = JsonConvert.DeserializeObject<User>(content);
                    //get your user model from db (by username - in my case its email)
                    //var user = await _userRepository.FindAsync(context.UserName);
                    if (user != null)
                    {
                        //check if password match - remember to hash password if stored as hash in db
                        if (user.Password == context.Password)
                        {
                            //set the result
                            context.Result = new GrantValidationResult(
                                subject: user.UserId.ToString(),
                                authenticationMethod: "custom",
                                claims: GetUserClaims(user));

                            return;
                        }
                        context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Incorrect password");
                        return;
                    }
                    context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "User does not exist.");
                    return;
                }
            }
            catch (Exception ex)
            {

            }

        }
        public static Claim[] GetUserClaims(User user)
        {
            List<Claim> claims = new List<Claim>();
            Claim claim;
            foreach (var itemClaim in user.Claims)
            {
                claim = new Claim(itemClaim.Type, itemClaim.Value);
                claims.Add(claim);
            }
            return claims.ToArray();
        }
}

(2)ProfileService類實現IprofileService介面:

 public class ProfileService : IProfileService
    {
        private readonly IHttpClientFactory _httpClientFactory;
        public ProfileService(IHttpClientFactory httpClientFactory)
        {
            _httpClientFactory = httpClientFactory;
        }
        ////services
        //private readonly IUserRepository _userRepository;

        //public ProfileService(IUserRepository userRepository)
        //{
        //    _userRepository = userRepository;
        //}

        //Get user profile date in terms of claims when calling /connect/userinfo
        public async Task GetProfileDataAsync(ProfileDataRequestContext context)
        {
            try
            {
                //depending on the scope accessing the user data.
                           var userId = context.Subject.Claims.FirstOrDefault(x => x.Type == "sub");
                    //獲取User_Id
                    if (!string.IsNullOrEmpty(userId?.Value) && long.Parse(userId.Value) > 0)
                    {
                        var client = _httpClientFactory.CreateClient();
                        //已過時
                        DiscoveryResponse disco = await DiscoveryClient.GetAsync("http://localhost:5000");
                        TokenClient tokenClient = new TokenClient(disco.TokenEndpoint, "AuthServer", "secret");
                        var tokenResponse = await tokenClient.RequestClientCredentialsAsync("api1");

                        //var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
                        //{
                        //    Address = "http://localhost:5000",
                        //    ClientId = "AuthServer",
                        //    ClientSecret = "secret",
                        //    Scope = "api1"
                        //});
                        //if (TokenResponse.IsError) throw new Exception(TokenResponse.Error);
                        client.SetBearerToken(tokenResponse.AccessToken);

                        //根據User_Id獲取user
                        var response = await client.GetAsync("http://localhost:5001/api/values/" + long.Parse(userId.Value));
                        //get user from db (find user by user id)
                        //var user = await _userRepository.FindAsync(long.Parse(userId.Value));
                        var content = await response.Content.ReadAsStringAsync();
                        User user = JsonConvert.DeserializeObject<User>(content);
                        // issue the claims for the user
                        if (user != null)
                        {
                            //獲取user中的Claims
                            var claims = GetUserClaims(user);
                            //context.IssuedClaims = claims.Where(x => context.RequestedClaimTypes.Contains(x.Type)).ToList();
                            context.IssuedClaims = claims.ToList();
                        }
                  }
            }
            catch (Exception ex)
            {
                //log your error
            }
        }

        //check if user account is active.
        public async Task IsActiveAsync(IsActiveContext context)
        {
            try
            {
                var userId = context.Subject.Claims.FirstOrDefault(x => x.Type == "sub");

                        if (!string.IsNullOrEmpty(userId?.Value) && long.Parse(userId.Value) > 0)
                        {
                            //var user = await _userRepository.FindAsync(long.Parse(userId.Value));
                            var client = _httpClientFactory.CreateClient();
                            //已過時
                            DiscoveryResponse disco = await DiscoveryClient.GetAsync("http://localhost:5000");
                            TokenClient tokenClient = new TokenClient(disco.TokenEndpoint, "AuthServer", "secret");
                            var tokenResponse = await tokenClient.RequestClientCredentialsAsync("api1");

                            //var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
                            //{
                            //    Address = "http://localhost:5000",
                            //    ClientId = "AuthServer",
                            //    ClientSecret = "secret",
                            //    Scope = "api1"
                            //});
                            //if (TokenResponse.IsError) throw new Exception(TokenResponse.Error);
                            client.SetBearerToken(tokenResponse.AccessToken);

                            //根據User_Id獲取user
                            var response = await client.GetAsync("http://localhost:5001/api/values/" + long.Parse(userId.Value));
                            //get user from db (find user by user id)
                            //var user = await _userRepository.FindAsync(long.Parse(userId.Value));
                            var content = await response.Content.ReadAsStringAsync();
                            User user = JsonConvert.DeserializeObject<User>(content);
                            if (user != null)
                            {
                                if (user.IsActive)
                                {
                                    context.IsActive = user.IsActive;
                                }
                            }                
                        }
            }
            catch (Exception ex)
            {
                //handle error logging
            }
        }
        public static Claim[] GetUserClaims(User user)
        {
            List<Claim> claims = new List<Claim>();
            Claim claim;
            foreach (var itemClaim in user.Claims)
            {
                claim = new Claim(itemClaim.Type, itemClaim.Value);
                claims.Add(claim);
            }
            return claims.ToArray();
        }
    }

(3)發現代碼裡面需要在ResourceAPI項目的ValuesController控制器中

添加根據UserId獲取User的Claims的介面。

        Authorize(Roles = "AuthServer")]
        [HttpGet("{userId}")]
        public ActionResult<string> Get(string userId)
        {
            var user = context.Users.Where(p => p.UserId == userId)
           .Include(p => p.Claims)
           .FirstOrDefault();
            return Ok(user.ToModel());
        }

(4)修改AuthServer中的Config中GetIdentityResources方法,定義從數據獲取的Claims為role的信息。

 public static IEnumerable<IdentityResource> GetIdentityResources()
        {
            var customProfile = new IdentityResource(
                name: "mvc.profile",
                displayName: "Mvc profile",
                claimTypes: new[] { "role" });
            return new List<IdentityResource>
            {
                new IdentityResources.OpenId(),
                new IdentityResources.Profile(),
                //new IdentityResource("roles","role",new List<string>{ "role"}),
                customProfile
            };
        }

(5)在GetClients中把定義的mvc.profile加到Scope配置

(6)最後記得在Startup的ConfigureServices方法加上

.AddResourceOwnerValidator<ResourceOwnerPasswordValidator>()

.AddProfileService<ProfileService>();

 

運行後,出現熟悉的About頁面(Access Token後面加上去的,源碼上有添加方法)


 本節介紹的IdentityServer4通過訪問介面的形式驗證從資料庫獲取的User信息。當然,也可以寫成AuthServer授權服務通過連接資料庫進行驗證。

另外,授權服務訪問資源服務API,用的是ClientCredentials模式(服務與服務之間訪問)。

參考博客:https://stackoverflow.com/questions/35304038/identityserver4-register-userservice-and-get-users-from-database-in-asp-net-core

源碼地址:https://github.com/Bingjian-Zhu/Mvc-HybridFlow.git

 


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

-Advertisement-
Play Games
更多相關文章
  • 設計模式(Design Patterns) ——可復用面向對象軟體的基礎 設計模式(Design pattern)是一套被反覆使用、多數人知曉的、經過分類編目的、代碼設計經驗的總結。使用設計模式是為了可重用代碼、讓代碼更容易被他人理解、保證代碼可靠性。 毫無疑問,設計模式於己於他人於系統都是多贏的, ...
  • 前言 介紹java的常用集合+各個集合使用用例 歡迎轉載,請註明作者和出處哦☺ 參考: 1,《Java核心編程技術(第二版)》 2, "http://www.cnblogs.com/LittleHann/p/3690187.html" java 集合基本概念​​​​ 在《Java核心編程技術(第二版 ...
  • 總結 從畢業到現在工作差不多快半年了,其中做了公司官網的改版,工作彙報,分享樓盤的模塊,和網頁端安卓端對接,也認識到了交流的重要性,公司雖然小,但是包住宿這個還是很方便的,給剛來北京的我減輕了許多壓力. 我其實不想寫總結的,但是今天發生了一件事情,讓我寫了總結,以此來記錄下自己的心情感受,希望十年後 ...
  • ■ NPE 原因:thrift服務端可能停了 ■ org.apache.thrift.transport.TTransportException: Cannot write to null outputstream 原因:客戶端未調用transport的open()方法 ...
  • 1. 簡單瞭解模塊 寫的每一個py文件都是一個模塊. 還有一些我們一直在使用的模塊 buildins 內置模塊. print, input random 主要是和隨機相關的內容 random() 隨機小數 uninform(a,b) 隨機小數 randint(a,b) 隨機整數 choice() 隨 ...
  • 2018-12-26 今天是我正式學習Python的第二天,也是我準備用博客來記錄我學習歷程的第一天。希望可以堅持下去,並且真正學到一些東西。 對字元串進行操作的方法: 1.對輸入字元是否是數字的判斷{str.isdecimal(),str.isdigit(),str.isnumeric()} 由上 ...
  • 利用線性基的合併,(直接暴力合併,複雜度62^2),當樹上路徑和來做,仿造java風格寫個類就好。。。(然後跑的巨慢,但是可以優化哈哈) cpp include using namespace std; const int N=2e4+7; / 我突然覺得,應該寫一個類。。 / struct Sha ...
  • 運行結果 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...