最近项目第一次尝试使用web api,照搬了一般mvc的Forms登录方式,在和前端对接的时候出现一个问题:
前端使用ajax调用登录接口完成登录后,再调用别的接口,被判断为未登录。
如果直接在浏览器中先后访问登录接口和别的接口,则能识别为已登录。
对于asp.net的这些机制其实我了解不多,所以我猜测为跨域导致了两次调用的http上下文不一致造成的,当时我们解决跨域问题的方式是在服务端配置文件的system.webServer节点下加入:
<httpProtocol> <customHeaders> <add name="Access-Control-Allow-Origin" value="*" /> <add name="Access-Control-Allow-Headers" value="Content-Type" /> <add name="Access-Control-Allow-Methods" value="GET, POST, PUT, DELETE, OPTIONS" /> </customHeaders> </httpProtocol>
我以为是这种解决方式不够完善,于是我开始在网上寻找并尝试各种解决跨域的方案,最重型的尝试就是把我的.net4.0版本的web api升级到.net4.5的web api2来使用Microsoft ASP.NET Web API 2 Cross-Origin Suppor支持跨域,结果发现这种方法只是控制更精细,和我们最开始用的那种简单粗暴的方式没有本质上的差别,别的方法也都是大同小异,于是此路不通。
此时我想到自己定义一个简单的登录,具体实现就是:
1.登录时创建一个票据,并在服务端保存一组票据和用户信息的键值对,将票据返回给客户端,客户端在访问需要登录的接口时必须带上此票据
2.给需要登录验证的接口添加一个自定义的AuthorizeAttribute,在此属性中获取客户端传递的票据,来验证票据是否存在或者过期,如果票据合法,将对应的用户信息添加到http上下文,如果票据不合法,返回用户未登录的提示
由于本人功力有限,并且因为项目涉及到充值提现等资金操作,已经定了要使用SSL,所以关于篡改,复用等传输安全方面的问题没有纳入考虑。
以下是代码:
用户信息模型(登录时将返回此信息至客户端):
public class MemberTicket { public string ID { get; set; } public string LoginName { get; set; } public string Token { get; set; } public DateTime LoginDate { get; set; } }
登录处理类:
/// <summary> /// 自定义登录 /// </summary> public class LoginHelper { /// <summary> /// 用户信息集合 /// </summary> private static Dictionary<string, MemberTicket> Members = new Dictionary<string, MemberTicket>(); /// <summary> /// 登录 /// </summary> /// <param name="ticket">用户信息</param> public static void Login(MemberTicket ticket) { if (Members.Keys.Contains(ticket.Token)) Members[ticket.Token] = ticket; else Members.Add(ticket.Token, ticket); } /// <summary> /// 退出登录 /// </summary> /// <param name="Token">票据</param> public static void SignOut(string Token) { if (Members.Keys.Contains(Token)) Members.Remove(Token); } /// <summary> /// 根据票据检查票据是否合法 /// </summary> /// <param name="Token">票据</param> /// <returns></returns> public static MemberTicket Check(string Token) { if (!string.IsNullOrEmpty(Token) && Members.Keys.Contains(Token)) { MemberTicket ticket = Members[Token]; if (ticket != null && ticket.LoginDate.AddMinutes(CommonData.TimeOut) > CommonData.TimeNow()) return ticket; } return null; } }
定义用户对象(能保存于http上下文的结构):
public class MemberPrincipal : IPrincipal { private string loginname; public string Loginname { get { return loginname; } set { loginname = value; } } private IIdentity _Identity; public IIdentity Identity { get { return _Identity; } set { _Identity = value; } } public bool IsInRole(string role) { return false; } public MemberPrincipal(string Name) { loginname = Name; _Identity = new GenericIdentity(loginname, "Forums"); bool isok = _Identity.IsAuthenticated; } }
保存用户信息到http上下文:
public class PrincipalHelper { public static void SetPrincipal(IPrincipal principal) { Thread.CurrentPrincipal = principal; if (HttpContext.Current != null) { HttpContext.Current.User = principal; } } }
关键部分,自定义登录策略:
public class LoginAuthorize : AuthorizeAttribute { public override void OnAuthorization(HttpActionContext httpContext) { MemberTicket ticket = LoginHelper.Check(GetToken(httpContext.Request.RequestUri.Query)); if (ticket == null) { HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.OK); ResponseMessage<string> result = new ResponseMessage<string>(); result.Header = new ResponseHeader(); result.Header.State = (int)ResponseHeaderState.SignOut; result.Header.Message = "用户未登录"; response.Content = new StringContent(JsonConvert.SerializeObject(result)); httpContext.Response = response; } else PrincipalHelper.SetPrincipal(new MemberPrincipal(ticket.LoginName)); } public string GetToken(string Query) { if (!Query.Contains("Token")) return null; string[] Param = Query.Split('&'); if (Param.Length == 0) return null; foreach (var item in Param) { if (!item.Contains("Token")) continue; string[] value = item.Split('='); if (value.Length == 0) return null; return value[1]; } return null; } }
返回消息结构:
public class ResponseMessage<T> { /// <summary> /// 消息头 /// </summary> public ResponseHeader Header { get; set; } /// <summary> /// 消息本体 /// </summary> public T Body { get; set; } } public class ResponseHeader { /// <summary> /// 执行状态 /// </summary> public int State { get; set; } /// <summary> /// 消息 /// </summary> public string Message { get; set; } }
最后将此属性加在需要登录验证的控制器或方法上即可。
做完之后,目前是达到了我的预期,但是我的预期太低,所以自己也感觉弄得很low……希望各位大神小神大牛小牛不吝赐教,指点指点我应该往哪个方向优化登录机制