如何将使用FormsAuthentication的网站迁移至OWIN Cookie Authentication?
无数据库场景下ASP.NET MVC FormsAuthentication的实践方案
嘿,我刚好处理过类似的场景——完全不碰数据库,全靠外部Web服务做身份验证,用FormsAuth来管理站点内的登录状态。结合你描述的流程(Login调用Web服务拿令牌,成功后SetAuthCookie),我给你几个关键的优化点和实践建议:
1. 别只存用户名,把外部会话令牌塞进Forms票据里
默认的FormsAuthentication.SetAuthCookie只会把用户名存在Cookie里,但你需要那个Web服务返回的会话令牌来调用其他依赖身份的接口对吧?所以得自定义FormsAuthenticationTicket,把令牌存到UserData字段里:
[HttpPost] public ActionResult Login(LoginViewModel model) { // 调用外部认证服务 var authResult = _externalAuthService.ValidateCredentials(model.Username, model.Password); if (authResult.IsAuthenticated) { // 构建自定义认证票据 var authTicket = new FormsAuthenticationTicket( 1, model.Username, DateTime.Now, DateTime.Now.AddMinutes(FormsAuthentication.Timeout.TotalMinutes), model.RememberMe, authResult.SessionToken // 把外部服务的令牌存在这里 ); // 加密票据并写入Cookie string encryptedTicket = FormsAuthentication.Encrypt(authTicket); var authCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket) { HttpOnly = true, Secure = FormsAuthentication.RequireSSL, Expires = authTicket.Expiration }; Response.Cookies.Add(authCookie); return RedirectToAction("Index", "Dashboard"); } ModelState.AddModelError("", "用户名或密码不正确"); return View(model); }
之后在任何需要令牌的地方,就可以解密票据拿出来用:
private string GetCurrentSessionToken() { var authCookie = Request.Cookies[FormsAuthentication.FormsCookieName]; if (authCookie == null) return null; var authTicket = FormsAuthentication.Decrypt(authCookie.Value); return authTicket?.UserData; }
2. 自定义授权过滤器,额外验证令牌有效性
FormsAuth默认只会验证Cookie的签名和过期时间,但你的令牌可能在外部服务那边已经失效了(比如用户在其他地方登出)。所以得做个自定义的AuthorizeAttribute,每次请求都去校验令牌是否有效:
public class ValidSessionTokenAttribute : AuthorizeAttribute { protected override bool AuthorizeCore(HttpContextBase httpContext) { // 先过一遍默认的FormsAuth验证 if (!base.AuthorizeCore(httpContext)) return false; // 取出令牌 var authCookie = httpContext.Request.Cookies[FormsAuthentication.FormsCookieName]; var authTicket = FormsAuthentication.Decrypt(authCookie.Value); var sessionToken = authTicket.UserData; // 调用外部服务校验令牌有效性 bool isTokenValid = _externalAuthService.VerifySessionToken(sessionToken); if (!isTokenValid) { // 令牌失效,清掉Cookie跳回登录页 FormsAuthentication.SignOut(); httpContext.Response.Redirect(FormsAuthentication.LoginUrl); return false; } return true; } }
然后把这个过滤器套在需要授权的控制器/Action上:
[ValidSessionToken] public class DashboardController : Controller { // 这里的Action都会先校验令牌有效性 }
3. 处理令牌过期的自动刷新
如果外部服务的令牌有自己的过期时间,和FormsAuth的Cookie过期时间不同步,那得加个自动刷新的逻辑。比如在每次请求前检查令牌剩余时间,快过期时调用外部服务的刷新接口拿新令牌,再更新Forms票据:
public void RefreshSessionTokenIfNecessary() { var authCookie = Request.Cookies[FormsAuthentication.FormsCookieName]; if (authCookie == null) return; var authTicket = FormsAuthentication.Decrypt(authCookie.Value); var sessionToken = authTicket.UserData; // 调用外部服务获取令牌过期时间 DateTime tokenExpiry = _externalAuthService.GetTokenExpiration(sessionToken); // 剩余5分钟以内就刷新 if (tokenExpiry - DateTime.Now < TimeSpan.FromMinutes(5)) { string newToken = _externalAuthService.RefreshSessionToken(sessionToken); if (!string.IsNullOrEmpty(newToken)) { // 生成新的票据 var newAuthTicket = new FormsAuthenticationTicket( authTicket.Version, authTicket.Name, DateTime.Now, DateTime.Now.AddMinutes(FormsAuthentication.Timeout.TotalMinutes), authTicket.IsPersistent, newToken ); // 更新Cookie string encryptedNewTicket = FormsAuthentication.Encrypt(newAuthTicket); authCookie.Value = encryptedNewTicket; authCookie.Expires = newAuthTicket.Expiration; Response.Cookies.Set(authCookie); } } }
可以把这个方法放在全局的ActionFilter里,每次请求前执行。
4. 安全细节不能忘
- 一定要在web.config里配置固定的machineKey,不然多服务器部署时,A服务器加密的Cookie到B服务器就解密不了了
- 开启Cookie的
HttpOnly和Secure属性,防止XSS攻击,强制HTTPS传输 UserData里别存敏感信息,虽然票据是加密的,但尽量只放必要的令牌- 如果外部服务支持,请求令牌时指定仅限当前站点的scope,降低令牌泄露的风险
内容的提问来源于stack exchange,提问作者Jeff Dege
相关产品推荐
相关产品推荐

