C#中實(shí)現(xiàn)SSO單點(diǎn)登錄的常用方案及實(shí)際案例
一、為什么我們需要SSO?
想象這樣一個(gè)場(chǎng)景:你每天上班需要登錄公司的OA系統(tǒng)審批文件,登錄CRM系統(tǒng)查看客戶(hù)信息,登錄財(cái)務(wù)系統(tǒng)報(bào)銷(xiāo)費(fèi)用,登錄人力資源系統(tǒng)提交休假申請(qǐng)...每個(gè)系統(tǒng)都有不同的賬號(hào)密碼,每天光是輸入密碼都要花費(fèi)不少時(shí)間,更不用說(shuō)時(shí)不時(shí)還要應(yīng)對(duì)密碼過(guò)期、忘記密碼的困擾。
這就是單點(diǎn)登錄(Single Sign-On,簡(jiǎn)稱(chēng)SSO)技術(shù)要解決的核心問(wèn)題。SSO讓用戶(hù)只需登錄一次,就能訪(fǎng)問(wèn)多個(gè)相互信任的應(yīng)用系統(tǒng),極大提升了用戶(hù)體驗(yàn)和工作效率。在企業(yè)級(jí)應(yīng)用架構(gòu)中,SSO已經(jīng)成為不可或缺的基礎(chǔ)設(shè)施。
二、SSO單點(diǎn)登錄的基本原理
SSO的實(shí)現(xiàn)基于一個(gè)關(guān)鍵概念:認(rèn)證中心。所有應(yīng)用系統(tǒng)不再各自為政地管理用戶(hù)認(rèn)證,而是將認(rèn)證請(qǐng)求委托給統(tǒng)一的認(rèn)證中心處理。
核心流程
用戶(hù)訪(fǎng)問(wèn)應(yīng)用系統(tǒng)A,發(fā)現(xiàn)未登錄
應(yīng)用系統(tǒng)A將用戶(hù)重定向到認(rèn)證中心
用戶(hù)在認(rèn)證中心輸入憑證進(jìn)行登錄
認(rèn)證中心驗(yàn)證通過(guò)后,生成一個(gè)全局會(huì)話(huà),并頒發(fā)一個(gè)令牌(Token)
用戶(hù)帶著令牌重定向回應(yīng)用系統(tǒng)A
應(yīng)用系統(tǒng)A驗(yàn)證令牌的有效性,確認(rèn)后建立本地會(huì)話(huà)
當(dāng)用戶(hù)訪(fǎng)問(wèn)應(yīng)用系統(tǒng)B時(shí),同樣重定向到認(rèn)證中心
認(rèn)證中心發(fā)現(xiàn)用戶(hù)已有全局會(huì)話(huà),直接頒發(fā)令牌并重定向回應(yīng)用系統(tǒng)B
整個(gè)過(guò)程中,用戶(hù)只需輸入一次賬號(hào)密碼,就能無(wú)縫訪(fǎng)問(wèn)多個(gè)系統(tǒng)。
三、C#中實(shí)現(xiàn)SSO的常用方案
在C#生態(tài)系統(tǒng)中,我們有多種實(shí)現(xiàn)SSO的方案,下面介紹幾種主流的實(shí)現(xiàn)方式。
3.1 基于ASP.NET Identity + OAuth 2.0
ASP.NET Identity是微軟官方提供的身份驗(yàn)證框架,結(jié)合OAuth 2.0協(xié)議可以輕松實(shí)現(xiàn)SSO。
實(shí)現(xiàn)步驟
創(chuàng)建認(rèn)證服務(wù)器
首先,我們需要?jiǎng)?chuàng)建一個(gè)專(zhuān)門(mén)的認(rèn)證服務(wù)器項(xiàng)目:
// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// 配置身份驗(yàn)證
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<IdentityUser>(options =>
options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
// 配置認(rèn)證服務(wù)器
services.AddIdentityServer()
.AddApiAuthorization<IdentityUser, ApplicationDbContext>();
services.AddAuthentication()
.AddIdentityServerJwt();
}
?
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// ...
app.UseAuthentication();
app.UseIdentityServer();
app.UseAuthorization();
// ...
}配置客戶(hù)端應(yīng)用
在需要接入SSO的客戶(hù)端應(yīng)用中,添加如下配置:
// appsettings.json
"IdentityServer": {
"Authority": "https://your-auth-server.com",
"ClientId": "your-client-id",
"ClientSecret": "your-client-secret",
"ResponseType": "code"
}
?
// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "https://your-auth-server.com";
options.ClientId = "your-client-id";
options.ClientSecret = "your-client-secret";
options.ResponseType = "code";
options.Scope.Add("profile");
options.Scope.Add("email");
options.SaveTokens = true;
});
}3.2 基于JWT令牌的自定義實(shí)現(xiàn)
如果需要更靈活的SSO實(shí)現(xiàn),可以基于JWT(JSON Web Token)自定義開(kāi)發(fā)。
核心代碼實(shí)現(xiàn)
JWT工具類(lèi)
public class JwtHelper
{
private readonly string _secretKey;
private readonly string _issuer;
private readonly string _audience;
public JwtHelper(IConfiguration configuration)
{
_secretKey = configuration["Jwt:SecretKey"];
_issuer = configuration["Jwt:Issuer"];
_audience = configuration["Jwt:Audience"];
}
public string GenerateToken(string userId, string username, IEnumerable<string> roles)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, userId),
new Claim(ClaimTypes.Name, username)
};
foreach (var role in roles)
{
claims.Add(new Claim(ClaimTypes.Role, role));
}
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_secretKey));
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: _issuer,
audience: _audience,
claims: claims,
expires: DateTime.Now.AddHours(1),
signingCredentials: credentials
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
public bool ValidateToken(string token, out ClaimsPrincipal principal)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.UTF8.GetBytes(_secretKey);
try
{
principal = tokenHandler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = true,
ValidateAudience = true,
ValidIssuer = _issuer,
ValidAudience = _audience,
ClockSkew = TimeSpan.Zero
}, out _);
return true;
}
catch
{
principal = null;
return false;
}
}
}認(rèn)證中心控制器
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
private readonly JwtHelper _jwtHelper;
private readonly IUserService _userService;
public AuthController(JwtHelper jwtHelper, IUserService userService)
{
_jwtHelper = jwtHelper;
_userService = userService;
}
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginRequest request)
{
var user = await _userService.ValidateCredentials(request.Username, request.Password);
if (user == null)
{
return Unauthorized("用戶(hù)名或密碼錯(cuò)誤");
}
var roles = await _userService.GetUserRoles(user.Id);
var token = _jwtHelper.GenerateToken(user.Id.ToString(), user.UserName, roles);
// 設(shè)置SSO Cookie
Response.Cookies.Append("SSO_TOKEN", token, new CookieOptions
{
HttpOnly = true,
Secure = true,
SameSite = SameSiteMode.None,
Domain = ".yourcompany.com", // 注意設(shè)置為父域名
Expires = DateTime.Now.AddHours(1)
});
return Ok(new { Token = token, UserId = user.Id, Username = user.UserName });
}
[HttpGet("check")]
public IActionResult CheckLogin()
{
if (Request.Cookies.TryGetValue("SSO_TOKEN", out var token))
{
if (_jwtHelper.ValidateToken(token, out var principal))
{
return Ok(new { IsLoggedIn = true, UserInfo = principal.Identity.Name });
}
}
return Ok(new { IsLoggedIn = false });
}
[HttpPost("logout")]
public IActionResult Logout()
{
Response.Cookies.Delete("SSO_TOKEN", new CookieOptions
{
Domain = ".yourcompany.com",
SameSite = SameSiteMode.None,
Secure = true
});
return Ok("退出成功");
}
}3.3 使用第三方SSO解決方案
對(duì)于企業(yè)級(jí)應(yīng)用,我們也可以考慮使用成熟的第三方SSO解決方案,如:
Microsoft Azure AD:提供完整的身份和訪(fǎng)問(wèn)管理解決方案
Okta:功能強(qiáng)大的身份管理平臺(tái)
Auth0:易于集成的認(rèn)證服務(wù)
這些解決方案通常提供豐富的API和SDK,大大簡(jiǎn)化了SSO的實(shí)現(xiàn)難度。
四、SSO實(shí)現(xiàn)中的關(guān)鍵考量
在實(shí)現(xiàn)SSO時(shí),有幾個(gè)關(guān)鍵因素需要特別注意:
4.1 安全性
使用HTTPS:確保所有認(rèn)證請(qǐng)求都通過(guò)加密通道傳輸
令牌保護(hù):使用HttpOnly和Secure標(biāo)志保護(hù)SSO令牌
令牌有效期:設(shè)置合理的令牌有效期,平衡安全性和用戶(hù)體驗(yàn)
簽名驗(yàn)證:確保令牌的完整性和真實(shí)性
4.2 跨域問(wèn)題
在Web應(yīng)用中實(shí)現(xiàn)SSO,跨域問(wèn)題是繞不開(kāi)的挑戰(zhàn)。解決方法包括:
設(shè)置共享的父域名Cookie
使用CORS(跨域資源共享)策略
通過(guò)iframe或postMessage進(jìn)行跨域通信
4.3 會(huì)話(huà)管理
全局會(huì)話(huà):認(rèn)證中心需要維護(hù)全局會(huì)話(huà)狀態(tài)
單點(diǎn)登出:實(shí)現(xiàn)一處登出,處處登出的功能
會(huì)話(huà)超時(shí):合理設(shè)置會(huì)話(huà)超時(shí)時(shí)間和自動(dòng)續(xù)期機(jī)制
4.4 高可用性
認(rèn)證中心作為所有系統(tǒng)的單點(diǎn),其高可用性至關(guān)重要??梢酝ㄟ^(guò):
部署多個(gè)認(rèn)證中心實(shí)例
使用負(fù)載均衡
實(shí)現(xiàn)故障轉(zhuǎn)移機(jī)制
五、實(shí)際案例:企業(yè)應(yīng)用SSO集成
讓我們看一個(gè)實(shí)際的企業(yè)應(yīng)用場(chǎng)景,如何將多個(gè)不同的應(yīng)用系統(tǒng)集成到SSO中。
場(chǎng)景描述
某企業(yè)有三個(gè)主要應(yīng)用系統(tǒng):
內(nèi)部OA系統(tǒng)(ASP.NET MVC)
CRM客戶(hù)管理系統(tǒng)(ASP.NET Core Web API)
人力資源管理系統(tǒng)(Blazor WebAssembly)
現(xiàn)在需要為這三個(gè)系統(tǒng)實(shí)現(xiàn)SSO,讓用戶(hù)只需登錄一次就能訪(fǎng)問(wèn)所有系統(tǒng)。
實(shí)現(xiàn)架構(gòu)
認(rèn)證中心:基于ASP.NET Core + IdentityServer4構(gòu)建
OA系統(tǒng):集成OpenID Connect客戶(hù)端
CRM系統(tǒng):使用JWT Bearer認(rèn)證
HR系統(tǒng):集成OIDC客戶(hù)端
核心配置
在認(rèn)證中心配置三個(gè)客戶(hù)端:
public static IEnumerable<Client> Clients =>
new Client[]
{
new Client
{
ClientId = "oa-client",
ClientName = "內(nèi)部OA系統(tǒng)",
AllowedGrantTypes = GrantTypes.Code,
ClientSecrets = { new Secret("oa-secret".Sha256()) },
RedirectUris = { "https://oa.yourcompany.com/signin-oidc" },
PostLogoutRedirectUris = { "https://oa.yourcompany.com/signout-callback-oidc" },
AllowedScopes = { "openid", "profile", "email" }
},
new Client
{
ClientId = "crm-client",
ClientName = "CRM系統(tǒng)",
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets = { new Secret("crm-secret".Sha256()) },
AllowedScopes = { "crm-api" }
},
new Client
{
ClientId = "hr-client",
ClientName = "人力資源系統(tǒng)",
AllowedGrantTypes = GrantTypes.Code,
ClientSecrets = { new Secret("hr-secret".Sha256()) },
RedirectUris = { "https://hr.yourcompany.com/authentication/login-callback" },
PostLogoutRedirectUris = { "https://hr.yourcompany.com/authentication/logout-callback" },
AllowedScopes = { "openid", "profile", "email", "hr-api" }
}
};六、總結(jié)與展望
SSO單點(diǎn)登錄技術(shù)極大地改善了用戶(hù)在多系統(tǒng)環(huán)境下的使用體驗(yàn),同時(shí)也簡(jiǎn)化了系統(tǒng)管理。在C#開(kāi)發(fā)中,我們可以通過(guò)多種方式實(shí)現(xiàn)SSO,從自定義開(kāi)發(fā)到使用成熟的第三方解決方案,選擇最適合自己項(xiàng)目需求的方式。
隨著微服務(wù)架構(gòu)的普及和身份管理技術(shù)的發(fā)展,SSO也在不斷演進(jìn)。未來(lái),我們可以期待看到更多基于云原生、支持多平臺(tái)、更加安全便捷的SSO解決方案出現(xiàn)。
對(duì)于C#開(kāi)發(fā)者來(lái)說(shuō),掌握SSO的實(shí)現(xiàn)原理和技術(shù)方案,不僅能夠提升項(xiàng)目的用戶(hù)體驗(yàn),也是構(gòu)建現(xiàn)代化企業(yè)應(yīng)用的必備技能
到此這篇關(guān)于C#中實(shí)現(xiàn)SSO單點(diǎn)登錄的常用方案及實(shí)際案例的文章就介紹到這了,更多相關(guān)C#實(shí)現(xiàn)SSO單點(diǎn)登錄內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C#實(shí)現(xiàn)文件篩選讀取并翻譯的自動(dòng)化工具
這篇文章主要為大家詳細(xì)介紹了如何利用C#實(shí)現(xiàn)文件篩選及讀取內(nèi)容,并翻譯的自動(dòng)化工具,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解一下2023-03-03
C#?崩潰異常中研究頁(yè)堆布局的詳細(xì)過(guò)程
最近遇到一位朋友的程序崩潰,發(fā)現(xiàn)崩潰點(diǎn)在富編輯器 msftedit 上,這個(gè)不是重點(diǎn),重點(diǎn)在于發(fā)現(xiàn)他已經(jīng)開(kāi)啟了 頁(yè)堆,由于 頁(yè)堆 和 NT堆 的內(nèi)存布局完全不一樣,這一篇結(jié)合我的了解以及 windbg 驗(yàn)證來(lái)系統(tǒng)的介紹下 頁(yè)堆,需要的朋友可以參考下2022-10-10
C#多線(xiàn)程開(kāi)發(fā)之任務(wù)并行庫(kù)詳解
最近在學(xué)習(xí)C#的并行編程,在每本書(shū)上的看到的知識(shí)點(diǎn)都不全面,所以先參考多本書(shū)書(shū)籍的講解,將并行編程,多線(xiàn)程編程的知識(shí)點(diǎn)整理一下,這篇文章主要給大家介紹了關(guān)于C#多線(xiàn)程開(kāi)發(fā)之任務(wù)并行庫(kù)的相關(guān)資料,需要的朋友可以參考下2021-09-09
C#調(diào)用WebService的實(shí)現(xiàn)方法
這篇文章主要介紹了C#調(diào)用WebService的實(shí)現(xiàn)方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2025-06-06
C#使用NPOI對(duì)word進(jìn)行讀寫(xiě)
這篇文章介紹了C#使用NPOI對(duì)word進(jìn)行讀寫(xiě)的方法,文中通過(guò)示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-06-06
C#學(xué)習(xí)基礎(chǔ)概念二十五問(wèn) 11-15
C#學(xué)習(xí)基礎(chǔ)概念二十五問(wèn) 11-15...2007-04-04

