在 ASP.NET MVC 项目中实现单点登录,关键并不在于某个控制器如何校验账号密码,而在于把身份认证能力从多个业务系统中抽离出来,集中放到一个独立的认证中心。用户只需要在认证中心完成一次登录,后续访问多个互相信任的业务系统时,都可以凭借认证中心签发的票据完成身份确认。业务系统本身不保存用户密码,只负责判断当前会话是否存在、是否存在有效票据,并在必要时把用户重定向到认证中心。

单点登录的整体架构与交互流程
从架构角度看,单点登录体系通常由三类角色组成。第一类是认证中心,负责登录页展示、账号密码校验、票据生成、票据校验以及全局登录状态维护。第二类是业务系统,例如内部门户、报表平台、工单系统等,它们只消费认证结果,不直接处理用户凭据。第三类是用户浏览器,承担页面跳转和票据传递的载体职责。三者之间通过重定向与服务端校验接口协作,形成一条清晰的信任链。
首次访问业务系统时,如果用户尚未登录,业务系统不会直接展示受保护页面,而是把当前访问地址作为回跳参数带到认证中心。用户在认证中心输入账号密码,认证中心验证通过后生成一个票据,并把票据与用户身份的对应关系保存在服务端缓存中。随后,认证中心带着票据跳回业务系统。业务系统收到票据后,再通过服务端方式向认证中心确认票据是否有效。确认通过后,业务系统创建本地会话,用户即完成登录。
当用户访问第二个业务系统时,流程会有所不同。业务系统发现本地没有会话,仍然会把用户导向认证中心,但认证中心此时已经知道用户刚刚完成过登录,因此无需再次要求输入账号密码,而是直接生成新的票据并跳回目标系统。目标系统校验票据成功后,同样建立本地会话。对用户而言,整个过程几乎没有感知,达到了在多个系统之间通行的效果。
- 认证中心保存全局登录状态,是整个信任体系的核心。
- 业务系统保存本地会话,用于减少每次请求都向认证中心发起校验。
- 票据只作为短暂有效的凭证,不应承载过多敏感信息。
- 回跳地址需要统一编码,并在认证中心做必要校验,避免跳转到不可信地址。
认证中心的关键接口与票据设计
认证中心至少需要提供三类能力。第一类是登录入口,接收用户访问和回跳地址。第二类是票据校验接口,供业务系统在服务端确认票据是否合法。第三类是注销接口,用于清除全局登录状态。围绕这三类能力,认证中心还需要维护票据缓存、过期时间、用户身份映射等基础数据。票据设计可以采用不透明随机字符串,也可以采用加密后的自包含字符串。无论采用哪种方式,都应保证票据不可被随意伪造,并且能够在服务端被及时撤销。
在 ASP.NET MVC 的示例中,可以使用缓存保存票据与用户名的映射。票据本身可以由用户名、随机数和保护后的字节序列组成。这样即使票据被截获,在没有服务端缓存和密钥保护的情况下,也难以直接还原出可伪造的身份信息。票据校验接口返回简单的 JSON 结果即可,业务系统根据结果判断是否允许建立本地会话。
下面这段示例展示了认证中心如何完成登录、生成票据、校验票据以及注销票据。示例使用服务端缓存保存票据状态,适合演示单点登录的主干逻辑。在实际项目中,可以把缓存替换为分布式缓存,以便多个认证中心节点共享登录状态。
using System;
using System.Text;
using System.Web;
using System.Web.Mvc;
using System.Web.Security;
namespace Sso.AuthCenter.Controllers
{
public class AuthController : Controller
{
private const string CachePrefix = "SSO_TOKEN_";
public ActionResult Login(string returnUrl)
{
ViewBag.ReturnUrl = returnUrl;
return View();
}
[HttpPost]
public ActionResult Login(string userName, string password, string returnUrl)
{
if (string.Equals(userName, "test", StringComparison.OrdinalIgnoreCase) && password == "123456")
{
string token = CreateToken(userName);
StoreToken(token, userName);
string redirectUrl = BuildReturnUrl(returnUrl, token);
if (!string.IsNullOrEmpty(redirectUrl))
{
return Redirect(redirectUrl);
}
return Content("登录成功");
}
ViewBag.Error = "账号或密码错误";
return View();
}
public ActionResult ValidateToken(string token)
{
if (string.IsNullOrEmpty(token))
{
return Json(new { success = false, message = "票据为空" }, JsonRequestBehavior.AllowGet);
}
string userName = GetUserName(token);
if (string.IsNullOrEmpty(userName))
{
return Json(new { success = false, message = "票据无效或已过期" }, JsonRequestBehavior.AllowGet);
}
return Json(new { success = true, userName = userName }, JsonRequestBehavior.AllowGet);
}
public ActionResult Logout(string token)
{
if (!string.IsNullOrEmpty(token))
{
HttpRuntime.Cache.Remove(CachePrefix + token);
}
return Content("注销成功");
}
private string CreateToken(string userName)
{
string raw = userName + "_" + Guid.NewGuid().ToString("N");
byte[] protectedBytes = MachineKey.Protect(Encoding.UTF8.GetBytes(raw), "SSO_TOKEN");
return Convert.ToBase64String(protectedBytes);
}
private void StoreToken(string token, string userName)
{
HttpRuntime.Cache.Insert(
CachePrefix + token,
userName,
null,
DateTime.UtcNow.AddMinutes(30),
TimeSpan.Zero);
}
private string GetUserName(string token)
{
return HttpRuntime.Cache[CachePrefix + token] as string;
}
private string BuildReturnUrl(string returnUrl, string token)
{
if (string.IsNullOrEmpty(returnUrl))
{
return null;
}
string separator = returnUrl.Contains("?") ? "&" : "?";
return returnUrl + separator + "token=" + HttpUtility.UrlEncode(token);
}
}
}
业务系统接入与会话建立
业务系统接入单点登录时,最重要的是保持职责简单。业务系统不应该重复实现账号密码校验,也不应该直接读取认证中心的用户表。它只需要完成三件事:第一,在用户未登录时跳转到认证中心;第二,在认证中心回跳时接收票据;第三,在服务端校验票据并建立本地会话。只要这三步稳定可靠,业务系统就能以较低成本接入统一认证。
全局过滤器是 ASP.NET MVC 中实现登录拦截的常用方式。它可以在控制器动作执行前检查会话中是否存在用户标识。如果不存在,就把当前请求地址编码后作为回跳参数传给认证中心。这样可以保证用户完成登录后仍然回到最初想访问的页面,而不是丢失原始访问路径。需要注意的是,认证中心回跳回来的票据处理动作应当允许未登录访问,否则拦截器可能会把回调请求再次重定向到认证中心,形成不必要的循环。
当认证中心带着票据跳回业务系统时,业务系统不能仅凭 URL 中存在票据就认为用户合法,必须通过服务端请求调用认证中心的校验接口。校验成功后,可以把用户名写入本地会话,同时把票据暂时保存在会话中,便于后续注销使用。下面的示例分别展示了登录拦截器和票据回调处理逻辑。
using System.Web;
using System.Web.Mvc;
namespace Sso.BusinessApp.Filters
{
public class SsoLoginFilter : ActionFilterAttribute
{
private const string AuthLoginUrl = "http://sso.ipipp.com/Auth/Login";
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (filterContext.HttpContext.Session != null && filterContext.HttpContext.Session["UserName"] != null)
{
base.OnActionExecuting(filterContext);
return;
}
string currentUrl = filterContext.HttpContext.Request.Url.AbsoluteUri;
string redirectUrl = AuthLoginUrl + "?returnUrl=" + HttpUtility.UrlEncode(currentUrl);
filterContext.Result = new RedirectResult(redirectUrl);
base.OnActionExecuting(filterContext);
}
}
}
using System;
using System.Net;
using System.Web;
using System.Web.Mvc;
using Newtonsoft.Json;
namespace Sso.BusinessApp.Controllers
{
public class HomeController : Controller
{
private const string AuthLoginUrl = "http://sso.ipipp.com/Auth/Login";
private const string AuthValidateUrl = "http://sso.ipipp.com/Auth/ValidateToken";
public ActionResult Index(string token)
{
if (Session["UserName"] == null && !string.IsNullOrEmpty(token))
{
TokenValidateResult result = ValidateToken(token);
if (result != null && result.Success)
{
Session["UserName"] = result.UserName;
Session["Token"] = token;
}
}
if (Session["UserName"] != null)
{
return View();
}
string currentUrl = Request.Url.AbsoluteUri;
return Redirect(AuthLoginUrl + "?returnUrl=" + HttpUtility.UrlEncode(currentUrl));
}
private TokenValidateResult ValidateToken(string token)
{
string url = AuthValidateUrl + "?token=" + HttpUtility.UrlEncode(token);
using (var client = new WebClient())
{
string json = client.DownloadString(url);
return JsonConvert.DeserializeObject<TokenValidateResult>(json);
}
}
}
public class TokenValidateResult
{
[JsonProperty("success")]
public bool Success { get; set; }
[JsonProperty("userName")]
public string UserName { get; set; }
}
}
安全加固、单点注销与落地建议
单点登录带来便利的同时,也会放大认证中心的安全风险。因此,生产环境应优先使用 HTTPS,避免票据和登录凭据在传输过程中被读取。认证中心和业务系统之间的接口应限制来源,必要时为每个业务系统分配独立标识,并在生成票据时记录票据归属于哪个业务系统。票据有效期不宜过长,最好与业务会话过期时间保持协调,避免出现票据仍然有效但本地会话已经失效,或者本地会话长期存在而认证中心状态早已过期的情况。
单点注销是另一个容易被忽视的环节。用户在某个业务系统中点击退出时,如果只清除本地会话,认证中心仍然认为用户处于登录状态,那么下一次访问其他系统时又会自动完成登录。因此,注销动作至少需要通知认证中心清除全局状态。更完善的方案还会通过后台通道或前端跳转方式通知所有已登录业务系统清除本地会话。下面的示例展示了业务系统退出时如何清理本地会话,并调用认证中心注销接口。
using System.Net;
using System.Web;
using System.Web.Mvc;
namespace Sso.BusinessApp.Controllers
{
public class AccountController : Controller
{
private const string AuthLogoutUrl = "http://sso.ipipp.com/Auth/Logout";
public ActionResult Logout()
{
string token = Session["Token"] as string;
Session.Clear();
if (!string.IsNullOrEmpty(token))
{
string url = AuthLogoutUrl + "?token=" + HttpUtility.UrlEncode(token);
using (var client = new WebClient())
{
client.DownloadString(url);
}
}
return RedirectToAction("Index", "Home");
}
}
}
除了主流程之外,落地时还应关注运维和扩展问题。例如,票据缓存如果部署在单台服务器上,会限制认证中心横向扩展,建议替换为统一的分布式缓存。认证中心的校验接口应记录必要日志,便于排查票据过期、回跳地址错误、网络请求失败等问题。对于回跳地址,应做白名单校验,避免恶意构造跳转地址。对于票据,应尽量避免在地址栏中长期暴露,必要时可采用一次性票据或短时效授权码模式。
总体来看,在 ASP.NET MVC 中实现单点登录的关键,是把认证中心、业务系统和用户会话之间的边界划分清楚。认证中心负责统一登录和票据管理,业务系统负责拦截未登录请求并校验票据,浏览器负责完成必要的跳转。只要围绕这条主线设计接口、缓存和过期策略,就能构建出结构清晰、易于扩展的单点登录体系。
ASP.NET_MVCSSO单点登录身份认证修改时间:2026-07-10 19:30:15