主頁 > .NET開發 > Angular SPA基于Ocelot API網關與IdentityServer4的身份認證與授權(一)

Angular SPA基于Ocelot API網關與IdentityServer4的身份認證與授權(一)

2020-09-16 20:34:25 .NET開發

好吧,這個題目我也想了很久,不知道如何用最簡單的幾個字來概括這篇文章,原本打算取名《Angular單頁面應用基于Ocelot API網關與IdentityServer4+ASP.NET Identity實作身份認證與授權》,然而如你所見,這樣的名字實在是太長了,所以,我不得不縮寫“單頁面應用”幾個字,然后去掉ASP.NET Identity的描述,最后形成目前的標題,

不過,這也就意味著這篇文章會涵蓋很多內容和技術,我會利用這些技術來走通一個完整的流程,這個流程也代表著在微服務架構中單點登錄的一種實作模式,在此程序中,我們會使用到如下技識訓框架:

  • Angular 8
  • Ocelot API Gateway
  • IdentityServer4
  • ASP.NET Identity
  • Entity Framework Core
  • SQL Server

本文假設讀者具有上述技術框架的基礎知識,由于內容比較多,我還是將這篇文章分幾個部分進行講解和討論,

場景描述

在微服務架構下的一種比較流行的設計,就是基于前后端分離,前端只做呈現和用戶操作流的管理,后端服務由API網關同一協調,以從業務層面為前端提供各種服務,大致可以用下圖表示:

image

在這個結構中,我沒有將Identity Service放在API Gateway后端,因為考慮到Identity Service本身并沒有承擔任何業務功能,從它所能提供的端點(Endpoint)的角度,它也需要做負載均衡、熔斷等保護,但我們暫時不討論這些內容,

流程上其實也比較簡單,在上圖的數字標識中:

  1. Client向Identity Service發送認證請求,通常可以是用戶名密碼
  2. 如果驗證通過,Identity Service會向Client回傳認證的Token
  3. Client使用Token向API Gateway發送API呼叫請求
  4. API Gateway將Client發送過來的Token發送給Identity Service,以驗證Token的有效性
  5. 如果驗證成功,Identity Service會告知API Gateway認證成功
  6. API Gateway轉發Client的請求到后端API Service
  7. API Service將結果回傳給API Gateway
  8. API Gateway將API Service回傳的結果轉發到Client

只是在這些步驟中,我們有很多技術選擇,比如Identity Service的實作方式、認證方式等等,接下來,我就在ASP.NET Core的基礎上使用IdentityServer4、Entity Framework Core和Ocelot來完成這一流程,在完成整個流程的演練之前,需要確保機器滿足以下條件:

  • 安裝Visual Studio 2019 Community Edition,使用Visual Studio Code也是可以的,根據自己的需要選擇
  • 安裝Visual Studio Code
  • 安裝Angular 8

IdentityServer4結合ASP.NET Identity實作Identity Service

創建新專案

首先第一步就是實作Identity Service,在Visual Studio 2019 Community Edition中,新建一個ASP.NET Core Web Application,模板選擇Web Application (Model-View-Controller),然后點擊Authentication下的Change按鈕,再選擇Individual User Accounts選項,以便將ASP.NET Identity的依賴包都加入專案,并且自動完成基礎代碼的搭建,

image

然后,通過NuGet添加IdentityServer4.AspNetIdentity以及IdentityServer4.EntityFramework的參考,IdentityServer4也隨之會被添加進來,接下來,在該專案的目錄下,執行以下命令安裝IdentityServer4的模板,并將IdentityServer4的GUI加入到當前專案:

dotnet new -i identityserver4.templates
dotnet new is4ui --force

然后調整一下專案結構,將原本的Controllers目錄洗掉,同時洗掉Models目錄下的ErrorViewModel類,然后將Quickstart目錄重命名為Controllers,編譯代碼,代碼應該可以編譯通過,接下來就是實作我們自己的Identity,

定制Identity Service

為了能夠展現一個標準的應用場景,我自己定義了User和Role物件,它們分別繼承于IdentityUser和IdentityRole類:

public class AppUser : IdentityUser
{
    public string DisplayName { get; set; }
}

public class AppRole : IdentityRole
{
    public string Description { get; set; }
}


當然,Data目錄下的ApplicationDbContext也要做相應調整,它應該繼承于IdentityDbContext<AppUser, AppRole, string>類,這是因為我們使用了自定義的IdentityUser和IdentityRole的實作:

public class ApplicationDbContext : IdentityDbContext<AppUser, AppRole, string>
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }
}


之后修改Startup.cs里的ConfigureServices方法,通過呼叫AddIdentity、AddIdentityServer以及AddDbContext,將ASP.NET Identity、IdentityServer4以及存盤認證資料所使用的Entity Framework Core的依賴全部注冊進來,為了測驗方便,目前我們還是使用Developer Signing Credential,對于Identity Resource、API Resource以及Clients,我們也是暫時先寫死(hard code):

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddIdentity<AppUser, AppRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();
    services.AddIdentityServer().AddDeveloperSigningCredential()
      .AddOperationalStore(options =>
      {
          options.ConfigureDbContext = builder => builder.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"),
              sqlServerDbContextOptionsBuilder =>
              sqlServerDbContextOptionsBuilder.MigrationsAssembly(typeof(Startup).Assembly.GetName().Name));
          options.EnableTokenCleanup = true;
          options.TokenCleanupInterval = 30; // interval in seconds
      })
      .AddInMemoryIdentityResources(Config.GetIdentityResources())
      .AddInMemoryApiResources(Config.GetApiResources())
      .AddInMemoryClients(Config.GetClients())
      .AddAspNetIdentity<AppUser>();

    services.AddCors(options => options.AddPolicy("AllowAll", p => p.AllowAnyOrigin()
       .AllowAnyMethod()
       .AllowAnyHeader()));

    services.AddControllersWithViews();
    services.AddRazorPages();
    services.AddControllers();
}


然后,調整Configure方法的實作,將IdentityServer加入進來,同時配置CORS使得站點能夠被跨域訪問:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseDatabaseErrorPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
        app.UseHsts();
    }

    app.UseCors("AllowAll");
    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();
    app.UseIdentityServer();

    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller=Home}/{action=Index}/{id?}");
        endpoints.MapRazorPages();
    });
}


完成這部分代碼調整后,編譯是通不過的,因為我們還沒有定義IdentityServer4的IdentityResource、API Resource和Clients,在專案中新建一個Config類,代碼如下:

public static class Config
{
    public static IEnumerable<IdentityResource> GetIdentityResources() => 
        new IdentityResource[]
        {
            new IdentityResources.OpenId(),
            new IdentityResources.Email(),
            new IdentityResources.Profile()
        };

    public static IEnumerable<ApiResource> GetApiResources() =>
        new[]
        {
            new ApiResource("api.weather", "Weather API")
            {
                Scopes =
                {
                    new Scope("api.weather.full_access", "Full access to Weather API")
                },
                UserClaims =
                {
                    ClaimTypes.NameIdentifier,
                    ClaimTypes.Name,
                    ClaimTypes.Email,
                    ClaimTypes.Role
                }
            }
        };

    public static IEnumerable<Client> GetClients() =>
        new[]
        {
            new Client
            {
                RequireConsent = false,
                ClientId = "angular",
                ClientName = "Angular SPA",
                AllowedGrantTypes = GrantTypes.Implicit,
                AllowedScopes = { "openid", "profile", "email", "api.weather.full_access" },
                RedirectUris = {"http://localhost:4200/auth-callback"},
                PostLogoutRedirectUris = {"http://localhost:4200/"},
                AllowedCorsOrigins = {"http://localhost:4200"},
                AllowAccessTokensViaBrowser = true,
                AccessTokenLifetime = 3600
            },
            new Client
            {
                ClientId = "webapi",
                AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
                ClientSecrets =
                {
                    new Secret("mysecret".Sha256())
                },
                AlwaysSendClientClaims = true,
                AllowedScopes = { "api.weather.full_access" }
            }
        };
}

大致說明一下上面的代碼,通俗地講,IdentityResource是指允許應用程式訪問用戶的哪些身份認證資源,比如,用戶的電子郵件或者其它用戶賬戶資訊,在Open ID Connect規范中,這些資訊會被轉換成Claims,保存在User Identity的物件里;ApiResource用來指定被IdentityServer4所保護的資源,比如這里新建了一個ApiResource,用來保護Weather API,它定義了自己的Scope和UserClaims,Scope其實是一種關聯關系,它關聯著Client與ApiResource,用來表示什么樣的Client對于什么樣的ApiResource具有怎樣的訪問權限,比如在這里,我定義了兩個Client:angular和webapi,它們對Weather API都可以訪問;UserClaims定義了當認證通過之后,IdentityServer4應該向請求方回傳哪些Claim,至于Client,就比較容易理解了,它定義了客戶端能夠以哪幾種方式來向IdentityServer4提交請求,

至此,我們的源代碼就可以編譯通過了,成功編譯之后,還需要使用Entity Framework Core所提供的命令列工具或者Powershell Cmdlet來初始化資料庫,我這里選擇使用Visual Studio 2019 Community中的Package Manager Console,在執行資料庫更新之前,確保appsettings.json檔案里設定了正確的SQL Server連接字串,當然,你也可以選擇使用其它型別的資料庫,只要對ConfigureServices方法做些相應的修改即可,在Package Manager Console中,依次執行下面的命令:

Add-Migration ModifiedUserAndRole -Context ApplicationDbContext
Add-Migration ModifiedUserAndRole –Context PersistedGrantDbContext
Update-Database -Context ApplicationDbContext
Update-Database -Context PersistedGrantDbContext

效果如下:

image

打開SQL Server Management Studio,看到資料表都已成功創建:

image

由于IdentityServer4的模板所產生的代碼使用的是mock user,也就是IdentityServer4里默認的TestUser,因此,相關部分的代碼需要被替換掉,最主要的部分就是AccountController的Login方法,將該方法中的相關代碼替換為:

if (ModelState.IsValid)
{
    var user = await _userManager.FindByNameAsync(model.Username);

    if (user != null && await _userManager.CheckPasswordAsync(user, model.Password))
    {
        await _events.RaiseAsync(new UserLoginSuccessEvent(user.UserName, user.Id, user.DisplayName));

        // 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)
            };
        };

        // issue authentication cookie with subject ID and username
        await HttpContext.SignInAsync(user.Id, 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", clientId: context?.ClientId));
    ModelState.AddModelError(string.Empty, AccountOptions.InvalidCredentialsErrorMessage);
}

這樣才能通過注入的userManager和EntityFramework Core來訪問SQL Server,以完成登錄邏輯,

新用戶注冊API

由IdentityServer4所提供的默認UI模板中沒有包括新用戶注冊的頁面,開發者可以根據自己的需要向Identity Service中增加View來提供注冊界面,不過為了快速演示,我打算先增加兩個API,然后使用curl來新建一些用于測驗的角色(Role)和用戶(User),下面的代碼為客戶端提供了注冊角色和注冊用戶的API:

public class RegisterRoleRequestViewModel
{
    [Required]
    public string Name { get; set; }

    public string Description { get; set; }
}

public class RegisterRoleResponseViewModel
{
    public RegisterRoleResponseViewModel(AppRole role)
    {
        Id = role.Id;
        Name = role.Name;
        Description = role.Description;
    }

    public string Id { get; }

    public string Name { get; }

    public string Description { get; }
}

public class RegisterUserRequestViewModel
{
    [Required]
    [StringLength(50, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 2)]
    [Display(Name = "DisplayName")]
    public string DisplayName { get; set; }

    public string Email { get; set; }

    [Required]
    [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
    [DataType(DataType.Password)]
    [Display(Name = "Password")]
    public string Password { get; set; }

    [Required]
    [StringLength(20)]
    [Display(Name = "UserName")]
    public string UserName { get; set; }

    public List<string> RoleNames { get; set; }
}

public class RegisterUserResponseViewModel
{
    public string Id { get; set; }
    public string UserName { get; set; }
    public string DisplayName { get; set; }
    public string Email { get; set; }

    public RegisterUserResponseViewModel(AppUser user)
    {
        Id = user.Id;
        UserName = user.UserName;
        DisplayName = user.DisplayName;
        Email = user.Email;
    }
}

// Controllers\Account\AccountController.cs
[HttpPost]
[Route("api/[controller]/register-account")]
public async Task<IActionResult> RegisterAccount([FromBody] RegisterUserRequestViewModel model)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    var user = new AppUser { UserName = model.UserName, DisplayName = model.DisplayName, Email = model.Email };
    

    var result = await _userManager.CreateAsync(user, model.Password);

    if (!result.Succeeded) return BadRequest(result.Errors);

    await _userManager.AddClaimAsync(user, new Claim(ClaimTypes.NameIdentifier, user.UserName));
    await _userManager.AddClaimAsync(user, new Claim(ClaimTypes.Name, user.DisplayName));
    await _userManager.AddClaimAsync(user, new Claim(ClaimTypes.Email, user.Email));

    if (model.RoleNames?.Count > 0)
    {
        var validRoleNames = new List<string>();
        foreach(var roleName in model.RoleNames)
        {
            var trimmedRoleName = roleName.Trim();
            if (await _roleManager.RoleExistsAsync(trimmedRoleName))
            {
                validRoleNames.Add(trimmedRoleName);
                await _userManager.AddToRoleAsync(user, trimmedRoleName);
            }
        }

        await _userManager.AddClaimAsync(user, new Claim(ClaimTypes.Role, string.Join(',', validRoleNames)));
    }

    return Ok(new RegisterUserResponseViewModel(user));
}

// Controllers\Account\AccountController.cs
[HttpPost]
[Route("api/[controller]/register-role")]
public async Task<IActionResult> RegisterRole([FromBody] RegisterRoleRequestViewModel model)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    var appRole = new AppRole { Name = model.Name, Description = model.Description };
    var result = await _roleManager.CreateAsync(appRole);
    if (!result.Succeeded) return BadRequest(result.Errors);

    return Ok(new RegisterRoleResponseViewModel(appRole));
}

在上面的代碼中,值得關注的就是register-account API中的幾行AddClaimAsync呼叫,我們將一些用戶資訊資料加入到User Identity的Claims中,比如,將用戶的角色資訊,通過逗號分隔的字串保存為Claim,在后續進行用戶授權的時候,會用到這些資料,

創建一些基礎資料

運行我們已經搭建好的Identity Service,然后使用下面的curl命令創建一些基礎資料:

curl -X POST https://localhost:7890/api/account/register-role \
  -d '{"name":"admin","description":"Administrator"}' \
  -H 'Content-Type:application/json' --insecure
curl -X POST https://localhost:7890/api/account/register-account \
  -d '{"userName":"daxnet","password":"P@ssw0rd123","displayName":"Sunny Chen","email":"[email protected]","roleNames":["admin"]}' \
  -H 'Content-Type:application/json' --insecure
curl -X POST https://localhost:7890/api/account/register-account \
  -d '{"userName":"acqy","password":"P@ssw0rd123","displayName":"Qingyang Chen","email":"[email protected]"}' \
  -H 'Content-Type:application/json' --insecure

完成這些命令后,系統中會創建一個admin的角色,并且會創建daxnet和acqy兩個用戶,daxnet具有admin角色,而acqy則沒有該角色,

使用瀏覽器訪問https://localhost:7890,點擊主頁的鏈接進入登錄界面,用已創建的用戶名和密碼登錄,可以看到如下的界面,表示Identity Service的開發基本完成:

image

小結

一篇文章實在是寫不完,今天就暫且告一段落吧,下一講我將介紹Weather API和基于Ocelot的API網關,整合Identity Service進行身份認證,

源代碼

訪問以下Github地址以獲取源代碼:

https://github.com/daxnet/identity-demo

轉載請註明出處,本文鏈接:https://www.uj5u.com/net/59638.html

標籤:.NET Core

上一篇:使用Lucene.Net做一個簡單的搜索引擎-全文索引

下一篇:ASP.NET CORE 啟動程序及原始碼解讀

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • WebAPI簡介

    Web體系結構: 有三個核心:資源(resource),URL(統一資源識別符號)和表示 他們的關系是這樣的:一個資源由一個URL進行標識,HTTP客戶端使用URL定位資源,表示是從資源回傳資料,媒體型別是資源回傳的資料格式。 接下來我們說下HTTP. HTTP協議的系統是一種無狀態的方式,使用請求/ ......

    uj5u.com 2020-09-09 22:07:47 more
  • asp.net core 3.1 入口:Program.cs中的Main函式

    本文分析Program.cs 中Main()函式中代碼的運行順序分析asp.net core程式的啟動,重點不是剖析原始碼,而是理清程式開始時執行的順序。到呼叫了哪些實體,哪些法方。asp.net core 3.1 的程式入口在專案Program.cs檔案里,如下。ususing System; us ......

    uj5u.com 2020-09-09 22:07:49 more
  • asp.net網站作為websocket服務端的應用該如何寫

    最近被websocket的一個問題困擾了很久,有一個需求是在web網站中搭建websocket服務。客戶端通過網頁與服務器建立連接,然后服務器根據ip給客戶端網頁發送資訊。 其實,這個需求并不難,只是剛開始對websocket的內容不太了解。上網搜索了一下,有通過asp.net core 實作的、有 ......

    uj5u.com 2020-09-09 22:08:02 more
  • ASP.NET 開源匯入匯出庫Magicodes.IE Docker中使用

    Magicodes.IE在Docker中使用 更新歷史 2019.02.13 【Nuget】版本更新到2.0.2 【匯入】修復單列匯入的Bug,單元測驗“OneColumnImporter_Test”。問題見(https://github.com/dotnetcore/Magicodes.IE/is ......

    uj5u.com 2020-09-09 22:08:05 more
  • 在webform中使用ajax

    如果你用過Asp.net webform, 說明你也算是.NET 開發的老兵了。WEBform應該是2011 2013左右,當時還用visual studio 2005、 visual studio 2008。后來基本都用的是MVC。 如果是新開發的專案,估計沒人會用webform技術。但是有些舊版 ......

    uj5u.com 2020-09-09 22:08:50 more
  • iis添加asp.net網站,訪問提示:由于擴展配置問題而無法提供您請求的

    今天在iis服務器配置asp.net網站,遇到一個問題,記錄一下: 問題:由于擴展配置問題而無法提供您請求的頁面。如果該頁面是腳本,請添加處理程式。如果應下載檔案,請添加 MIME 映射。 WindowServer2012服務器,添加角色安裝完.netframework和iis之后,運行aspx頁面 ......

    uj5u.com 2020-09-09 22:10:00 more
  • WebAPI-處理架構

    帶著問題去思考,大家好! 問題1:HTTP請求和回傳相應的HTTP回應資訊之間發生了什么? 1:首先是最底層,托管層,位于WebAPI和底層HTTP堆疊之間 2:其次是 訊息處理程式管道層,這里比如日志和快取。OWIN的參考是將訊息處理程式管道的一些功能下移到堆疊下端的OWIN中間件了。 3:控制器處理 ......

    uj5u.com 2020-09-09 22:11:13 more
  • 微信門戶開發框架-使用指導說明書

    微信門戶應用管理系統,采用基于 MVC + Bootstrap + Ajax + Enterprise Library的技術路線,界面層采用Boostrap + Metronic組合的前端框架,資料訪問層支持Oracle、SQLServer、MySQL、PostgreSQL等資料庫。框架以MVC5,... ......

    uj5u.com 2020-09-09 22:15:18 more
  • WebAPI-HTTP編程模型

    帶著問題去思考,大家好!它是什么?它包含什么?它能干什么? 訊息 HTTP編程模型的核心就是訊息抽象,表示為:HttPRequestMessage,HttpResponseMessage.用于客戶端和服務端之間交換請求和回應訊息。 HttpMethod類包含了一組靜態屬性: private stat ......

    uj5u.com 2020-09-09 22:15:23 more
  • 部署WebApi隨筆

    一、跨域 NuGet參考Microsoft.AspNet.WebApi.Cors WebApiConfig.cs中配置: // Web API 配置和服務 config.EnableCors(new EnableCorsAttribute("*", "*", "*")); 二、清除默認回傳XML格式 ......

    uj5u.com 2020-09-09 22:15:48 more
最新发布
  • C#多執行緒學習(二) 如何操縱一個執行緒

    <a href="https://www.cnblogs.com/x-zhi/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2943582/20220801082530.png" alt="" /></...

    uj5u.com 2023-04-19 09:17:20 more
  • C#多執行緒學習(二) 如何操縱一個執行緒

    C#多執行緒學習(二) 如何操縱一個執行緒 執行緒學習第一篇:C#多執行緒學習(一) 多執行緒的相關概念 下面我們就動手來創建一個執行緒,使用Thread類創建執行緒時,只需提供執行緒入口即可。(執行緒入口使程式知道該讓這個執行緒干什么事) 在C#中,執行緒入口是通過ThreadStart代理(delegate)來提供的 ......

    uj5u.com 2023-04-19 09:16:49 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    <a href="https://www.cnblogs.com/huangxincheng/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/214741/20200614104537.png" alt="" /&g...

    uj5u.com 2023-04-18 08:39:04 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    一:背景 1. 講故事 前段時間協助訓練營里的一位朋友分析了一個程式卡死的問題,回過頭來看這個案例比較經典,這篇稍微整理一下供后來者少踩坑吧。 二:WinDbg 分析 1. 為什么會卡死 因為是表單程式,理所當然就是看主執行緒此時正在做什么? 可以用 ~0s ; k 看一下便知。 0:000> k # ......

    uj5u.com 2023-04-18 08:33:10 more
  • SignalR, No Connection with that ID,IIS

    <a href="https://www.cnblogs.com/smartstar/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/u36196.jpg" alt="" /></a>...

    uj5u.com 2023-03-30 17:21:52 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:15:33 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:13:31 more
  • C#遍歷指定檔案夾中所有檔案的3種方法

    <a href="https://www.cnblogs.com/xbhp/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/957602/20230310105611.png" alt="" /></a&...

    uj5u.com 2023-03-27 14:46:55 more
  • C#/VB.NET:如何將PDF轉為PDF/A

    <a href="https://www.cnblogs.com/Carina-baby/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2859233/20220427162558.png" alt="" />...

    uj5u.com 2023-03-27 14:46:35 more
  • 武裝你的WEBAPI-OData聚合查詢

    <a href="https://www.cnblogs.com/podolski/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/616093/20140323000327.png" alt="" /><...

    uj5u.com 2023-03-27 14:46:16 more