为了保存与登录名相关的额外的用户信息,我认为实现自定义的身份认证标识(HttpContext.User实例)是个容易的解决方法。
理解这个方法也会让我们对Forms身份认证有着更清楚地认识。
这个方法的核心是(分为二个子过程):
1. 在登录时,创建自定义的FormsAuthenticationTicket对象,它包含了用户信息。
2. 加密FormsAuthenticationTicket对象。
3. 创建登录Cookie,它将包含FormsAuthenticationTicket对象加密后的结果。
4. 在管线的早期阶段,读取登录Cookie,如果有,则解密。
5. 从解密后的FormsAuthenticationTicket对象中还原我们保存的用户信息。
6. 设置HttpContext.User为我们自定义的对象。
现在,我们还是来看一下HttpContext.User这个属性的定义:
// 为当前 HTTP 请求获取或设置安全信息。 // // 返回结果: // 当前 HTTP 请求的安全信息。 public IPrincipal User { get; set; }
由于这个属性只是个接口类型,因此,我们也可以自己实现这个接口。
考虑到更好的通用性:不同的项目可能要求接受不同的用户信息类型。所以,我定义了一个泛型类。
public class MyFormsPrincipal<TUserData> : IPrincipal where TUserData : class, new() { private IIdentity _identity; private TUserData _userData; public MyFormsPrincipal(FormsAuthenticationTicket ticket, TUserData userData) { if( ticket == null ) throw new ArgumentNullException("ticket"); if( userData == null ) throw new ArgumentNullException("userData"); _identity = new FormsIdentity(ticket); _userData = userData; } public TUserData UserData { get { return _userData; } } public IIdentity Identity { get { return _identity; } } public bool IsInRole(string role) { // 把判断用户组的操作留给UserData去实现。 IPrincipal principal = _userData as IPrincipal; if( principal == null ) throw new NotImplementedException(); else return principal.IsInRole(role); }
与之配套使用的用户信息的类型定义如下(可以根据实际情况来定义):
public class UserInfo : IPrincipal { public int UserId; public int GroupId; public string UserName; // 如果还有其它的用户信息,可以继续添加。 public override string ToString() { return string.Format("UserId: {0}, GroupId: {1}, UserName: {2}, IsAdmin: {3}", UserId, GroupId, UserName, IsInRole("Admin")); } #region IPrincipal Members [ScriptIgnore] public IIdentity Identity { get { throw new NotImplementedException(); } } public bool IsInRole(string role) { if( string.Compare(role, "Admin", true) == 0 ) return GroupId == 1; else return GroupId > 0; } #endregion }
注意:表示用户信息的类型并不要求一定要实现IPrincipal接口,如果不需要用户组的判断,可以不实现这个接口。
登录时需要调用的方法(定义在MyFormsPrincipal类型中):
/// <summary> /// 执行用户登录操作 /// </summary> /// <param>登录名</param> /// <param>与登录名相关的用户信息</param> /// <param>登录Cookie的过期时间,单位:分钟。</param> public static void SignIn(string loginName, TUserData userData, int expiration) { if( string.IsNullOrEmpty(loginName) ) throw new ArgumentNullException("loginName"); if( userData == null ) throw new ArgumentNullException("userData"); // 1. 把需要保存的用户数据转成一个字符串。 string data = null; if( userData != null ) data = (new JavaScriptSerializer()).Serialize(userData); // 2. 创建一个FormsAuthenticationTicket,它包含登录名以及额外的用户数据。 FormsAuthenticationTicket ticket = new FormsAuthenticationTicket( 2, loginName, DateTime.Now, DateTime.Now.AddDays(1), true, data); // 3. 加密Ticket,变成一个加密的字符串。 string cookieValue = FormsAuthentication.Encrypt(ticket); // 4. 根据加密结果创建登录Cookie HttpCookie cookie = new HttpCookie(FormsAuthentication.FormsCookieName, cookieValue); cookie.HttpOnly = true; cookie.Secure = FormsAuthentication.RequireSSL; cookie.Domain = FormsAuthentication.CookieDomain; cookie.Path = FormsAuthentication.FormsCookiePath; if( expiration > 0 ) cookie.Expires = DateTime.Now.AddMinutes(expiration); HttpContext context = HttpContext.Current; if( context == null ) throw new InvalidOperationException(); // 5. 写登录Cookie context.Response.Cookies.Remove(cookie.Name); context.Response.Cookies.Add(cookie); }
这里有必要再补充一下:登录状态是有过期限制的。Cookie有 有效期,FormsAuthenticationTicket对象也有 有效期。这二者任何一个过期时,都将导致登录状态无效。按照默认设置,FormsAuthenticationModule将采用slidingExpiration=true的策略来处理FormsAuthenticationTicket过期问题。
登录页面代码: