前言
想象一下這樣的場(chǎng)景:
每天早上,你打開電腦,先登錄OA系統(tǒng)查看郵件,再登錄CRM系統(tǒng)查看客戶信息,接著登錄財(cái)務(wù)系統(tǒng)審批報(bào)銷單...每個(gè)系統(tǒng)都要輸入用戶名密碼,而且為了防止安全風(fēng)險(xiǎn),你還得為每個(gè)系統(tǒng)設(shè)置不同的復(fù)雜密碼。
這簡直是現(xiàn)代職場(chǎng)人的噩夢(mèng)!
作為一名程序猿,我們不僅要解決自己的痛點(diǎn),更要為用戶創(chuàng)造流暢的體驗(yàn)。這就是單點(diǎn)登錄(Single Sign-On, SSO)誕生的意義——讓用戶只需登錄一次,就能訪問所有相互信任的系統(tǒng),告別密碼記憶的煩惱。
下面我們一起探索如何在 C# 中實(shí)現(xiàn)單點(diǎn)登錄(SSO)。
什么是單點(diǎn)登錄?
**單點(diǎn)登錄(SSO)**,顧名思義,就是 "一次登錄,處處通行"。
它是一種身份驗(yàn)證機(jī)制,允許用戶使用一組憑據(jù)(如用戶名和密碼)登錄到多個(gè)應(yīng)用程序或網(wǎng)站,而無需為每個(gè)應(yīng)用程序單獨(dú)登錄。
SSO的核心思想可以用一個(gè)生活中的例子來理解:
假設(shè)你是一家大型公司的員工,公司有辦公大樓、健身房、餐廳和停車場(chǎng)。傳統(tǒng)方式下,你進(jìn)入每個(gè)區(qū)域都需要出示不同的證件或刷卡。而有了SSO,就像公司給你發(fā)了一張萬能門禁卡,刷一次卡就能暢通無阻地進(jìn)入所有區(qū)域。
從技術(shù)角度看,SSO主要解決了以下問題:
- 用戶體驗(yàn):減少用戶需要記憶的密碼數(shù)量,提高操作效率
- 安全管理:集中管理用戶身份,降低密碼泄露風(fēng)險(xiǎn)
- 系統(tǒng)集成:簡化不同系統(tǒng)間的身份驗(yàn)證流程
SSO的實(shí)現(xiàn)方式
SSO的實(shí)現(xiàn)方式多種多樣,但核心思想都是集中認(rèn)證,分散使用。
下面是一些常見的實(shí)現(xiàn)方式:
基于Cookie的SSO(同域/子域場(chǎng)景)
這是最簡單的SSO實(shí)現(xiàn)方式,適用于同一主域名下的不同子域名系統(tǒng)(如a.baidu.com和b.baidu.com)。
原理是將認(rèn)證信息存儲(chǔ)在父域名的 Cookie 中,子域名可以自動(dòng)繼承父域名的 Cookie。
基于認(rèn)證中心的SSO(跨域場(chǎng)景)
對(duì)于完全不同的域名(如a.com和b.com),Cookie無法共享,這時(shí)就需要引入獨(dú)立的認(rèn)證中心。
常見的實(shí)現(xiàn)方案有CAS、OAuth2.0、OIDC和SAML等。
以 CAS(Central Authentication Service) 為例,其流程大概如下:
- 用戶訪問a.com,a.com發(fā)現(xiàn)用戶未登錄,重定向到認(rèn)證中心(sso.com)
- 用戶在sso.com完成認(rèn)證,認(rèn)證中心生成臨時(shí)票據(jù)(ST)
- a.com后臺(tái)向認(rèn)證中心驗(yàn)證ST的有效性
- 驗(yàn)證通過后,a.com建立本地會(huì)話
基于Token的SSO(現(xiàn)代Web應(yīng)用)
對(duì)于前后端分離的現(xiàn)代Web應(yīng)用,常用JWT(JSON Web Token)實(shí)現(xiàn)SSO。
認(rèn)證中心頒發(fā) Token 后,前端將 Token 存儲(chǔ)在 LocalStorage 中,每次請(qǐng)求攜帶 Token,各子系統(tǒng)通過驗(yàn)證 Token 確認(rèn)用戶身份。
單點(diǎn)登錄實(shí)現(xiàn)例子
1. 創(chuàng)建解決方案和項(xiàng)目
打開VS2022,新建空白解決方案"SSODemo",然后添加三個(gè)ASP.NET Core Web應(yīng)用項(xiàng)目:
2. 認(rèn)證中心實(shí)現(xiàn)
在SSO.Auth項(xiàng)目中,添加LoginController:
[Route("auth")]
public class LoginController : Controller
{
private static Dictionary<string, string> _tickets = new Dictionary<string, string>();
[HttpGet("login")]
public IActionResult Login(string returnUrl)
{
ViewBag.ReturnUrl = returnUrl;
return View();
}
[HttpPost("login")]
public IActionResult Login(string username, string password, string returnUrl)
{
// 簡單模擬用戶驗(yàn)證
if(username == "admin" && password == "123456")
{
// 生成臨時(shí)票據(jù)(ST)
var st = Guid.NewGuid().ToString();
_tickets[st] = username;
// 重定向回子系統(tǒng),攜帶ST
return Redirect($"{returnUrl}?st={st}");
}
ViewBag.Error = "用戶名或密碼錯(cuò)誤";
return View();
}
[HttpGet("validate")]
public IActionResult Validate(string st, string service)
{
if(_tickets.TryGetValue(st, out var username))
{
_tickets.Remove(st); // ST一次性使用
return Ok(new { username });
}
return Unauthorized();
}
}
3. 子系統(tǒng)實(shí)現(xiàn)
在SSO.AppA和SSO.AppB中,添加HomeController:
public class HomeController : Controller
{
[HttpGet]
public IActionResult Index()
{
// 檢查本地會(huì)話
if(HttpContext.Session.GetString("username") != null)
{
ViewBag.Username = HttpContext.Session.GetString("username");
return View();
}
// 未登錄,跳轉(zhuǎn)到認(rèn)證中心
var returnUrl = $"{Request.Scheme}://{Request.Host}";
var ssoUrl = $"https://localhost:5001/auth/login?returnUrl={returnUrl}";
return Redirect(ssoUrl);
}
[HttpGet("callback")]
public async Task<IActionResult> Callback(string st)
{
// 驗(yàn)證ST
using(var client = new HttpClient())
{
var validateUrl = $"https://localhost:5001/auth/validate?st={st}&service=appA";
var response = await client.GetAsync(validateUrl);
if(response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<dynamic>(content);
string username = result.GetProperty("username").GetString();
// 建立本地會(huì)話
HttpContext.Session.SetString("username", username);
return RedirectToAction("Index");
}
}
return Unauthorized();
}
[HttpGet("logout")]
public IActionResult Logout()
{
HttpContext.Session.Clear();
return RedirectToAction("Index");
}
}
4. 配置和運(yùn)行
為每個(gè)項(xiàng)目設(shè)置不同的端口:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddSession(options => {
options.IdleTimeout = TimeSpan.FromMinutes(30);
options.Cookie.HttpOnly = true;
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// ...
app.UseSession();
// ...
}
- 同時(shí)啟動(dòng)三個(gè)項(xiàng)目,訪問AppA或AppB,都會(huì)被重定向到認(rèn)證中心登錄。登錄成功后,再訪問另一個(gè)App,無需再次登錄。
總結(jié)
單點(diǎn)登錄在各種場(chǎng)景下都能大顯身手,我們經(jīng)??吹降囊恍┙尤胛⑿拧⒅Ц秾毜鹊谌降卿浀膽?yīng)用,本質(zhì)上是利用這些平臺(tái)作為認(rèn)證中心的SSO流程。
作為 C# 程序員,我們可以利用 ASP.NET Core 強(qiáng)大的功能,相對(duì)輕松地實(shí)現(xiàn)各種 SSO 方案,無論是簡單的同域Cookie共享,還是復(fù)雜的跨域認(rèn)證中心,.NET 都提供了完善的支持。
不過,在實(shí)現(xiàn) SSO 的時(shí)候,安全問題也是需要密切注意的,這里有一些建議:
- 通信時(shí)最好使用 HTTPS,防止憑證被竊取
- 票據(jù)尤其是臨時(shí)的票據(jù)要設(shè)置較短的有效期,一般不要超過5分鐘
- 使用 state 參數(shù)防止跨站請(qǐng)求偽造(CSRF攻擊)
- 有可能的情況下,進(jìn)行敏感操作時(shí)最好進(jìn)行二次驗(yàn)證
- 實(shí)現(xiàn)全局注銷,確保所有系統(tǒng)會(huì)話都被清除
該文章在 2025/7/28 12:39:17 編輯過