概述:
ASP.NET Web API 的好用使用过的都知道,没有复杂的配置文件,一个简单的ApiController加上需要的Action就能工作。但是在使用API的时候总会遇到跨域请求的问题, 特别各种APP万花齐放的今天,对API使用者身份角色验证是不能避免的(完全开发的API不需要对使用者身份角色进行管控,可以绕过),这篇文章就来谈谈基于令牌TOKEN身份验证的实现。
问题:
对于Web API的选择性的开放,使用者无论使用AJAX,还是HttpClient对接,总要对使用者的身份角色进行验证,然而使用API总有跨域使用情况的存在,这样就导致所有基于cookie验证方式都不再适用于API的验证。
原因:
比如,基于form表单验证的基础是登录验证成功后,用户的信息存在缓存或数据库或cookie,无论哪种方式存储用户信息,都不能绕过对cookie的使用,所以form表单验证方法对于禁用cookie的浏览器都不能正常使用,结论就是不能使用cookie 的环境就不能使用基本的form表单验证方式。因此WEB API 由于跨域的使用,导致cookie不能正常工作,所以不能再使用基于表单验证的方式来实现。
基于令牌TOKEN验证方法的实现:
方法一:
1. 实现对缓存TOKEN的管理,以防IIS服务器的宕机,可以对TOKEN进行持久化存储处理,每次IIS重启重新初始化已经登录成功TOKEN缓存。实现如下:
1 public class UserTokenManager2 {3 private static readonly IUserTokenRepository _tokenRep;4 private const string TOKENNAME = "PASSPORT.TOKEN";5 6 static UserTokenManager()7 {8 _tokenRep = ContainerManager.Resolve<IUserTokenRepository>();9 }10 /// <summary>11 /// 初始化缓存12 /// </summary>13 private static List<UserToken> InitCache()14 {15 if (HttpRuntime.Cache[TOKENNAME] == null)16 {17 var tokens = _tokenRep.GetAll();18 // cache 的过期时间, 令牌过期时间 *219 HttpRuntime.Cache.Insert(TOKENNAME, tokens, null, System.Web.Caching.Cache.NoAbsoluteExpiration, TimeSpan.FromDays(7 * 2));20 }21 var ts = (List<UserToken>)HttpRuntime.Cache[TOKENNAME];22 return ts;23 }24 25 26 public static int GetUId(string token)27 {28 var tokens = InitCache();29 var result = 0;30 if (tokens.Count > 0)31 {32 var id = tokens.Where(c => c.Token == token).Select(c => c.UId).FirstOrDefault();33 if (id != null)34 result = id.Value;35 }36 return result;37 }38 39 40 public static string GetPermission(string token)41 {42 var tokens = InitCache();43 if (tokens.Count == 0)44 return "NoAuthorize";45 else46 return tokens.Where(c => c.Token == token).Select(c => c.Permission).FirstOrDefault();47 }48 49 public static string GetUserType(string token)50 {51 var tokens = InitCache();52 if (tokens.Count == 0)53 return "";54 else55 return tokens.Where(c => c.Token == token).Select(c => c.UserType).FirstOrDefault();56 }57 58 /// <summary>59 /// 判断令牌是否存在60 /// </summary>61 /// <param name="token"></param>62 /// <returns></returns>63 public static bool IsExistToken(string token)64 {65 var tokens = InitCache();66 if (tokens.Count == 0) return false;67 else68 {69 var t = tokens.Where(c => c.Token == token).FirstOrDefault();70 if (t == null)71 return false;72 else if (t.Timeout < DateTime.Now)73 {74 RemoveToken(t);75 return false;76 }77 else78 {79 // 小于8小时 更新过期时间80 if ((t.Timeout - DateTime.Now).TotalMinutes < 1 * 60 - 1)81 {82 t.Timeout = DateTime.Now.AddHours(8);83 UpdateToken(t);84 }85 return true;86 }87 88 }89 }90 91 /// <summary>92 /// 添加令牌, 没有则添加,有则更新93 /// </summary>94 /// <param name="token"></param>95 public static void AddToken(UserToken token)96 {97 var tokens = InitCache();98 // 不存在 怎增加99 if (!IsExistToken(token.Token)) 100 { 101 token.ID = 0; 102 tokens.Add(token); 103 // 插入数据库 104 _tokenRep.Add(token); 105 } 106 else // 有则更新 107 { 108 UpdateToken(token); 109 } 110 } 111 112 public static bool UpdateToken(UserToken token) 113 { 114 var tokens = InitCache(); 115 if (tokens.Count == 0) return false; 116 else 117 { 118 var t = tokens.Where(c => c.Token == token.Token).FirstOrDefault(); 119 if (t == null) 120 return false; 121 t.Timeout = token.Timeout; 122 // 更新数据库 123 var tt = _tokenRep.FindByToken(token.Token); 124 if (tt != null) 125 { 126 tt.UserType = token.UserType; 127 tt.UId = token.UId; 128 tt.Permission = token.Permission; 129 tt.Timeout = token.Timeout; 130 _tokenRep.Update(tt); 131 } 132 return true; 133 } 134 } 135 /// <summary> 136 /// 移除指定令牌 137 /// </summary> 138 /// <param name="token"></param> 139 /// <returns></returns> 140 public static void RemoveToken(UserToken token) 141 { 142 var tokens = InitCache(); 143 if (tokens.Count == 0) return; 144 tokens.Remove(token); 145 _tokenRep.Remove(token); 146 } 147 148 public static void RemoveToken(string token) 149 { 150 var tokens = InitCache(); 151 if (tokens.Count == 0) return; 152 153 var ts = tokens.Where(c => c.Token == token).ToList(); 154 foreach (var t in ts) 155 { 156 tokens.Remove(t); 157 var tt = _tokenRep.FindByToken(t.Token); 158 if (tt != null) 159 _tokenRep.Remove(tt); 160 } 161 } 162 163 164 public static void RemoveToken(int uid) 165 { 166 var tokens = InitCache(); 167 if (tokens.Count == 0) return; 168 169 var ts = tokens.Where(c => c.UId == uid).ToList(); 170 foreach (var t in ts) 171 { 172 tokens.Remove(t); 173 var tt = _tokenRep.FindByToken(t.Token); 174 if (tt != null) 175 _tokenRep.Remove(tt); 176 } 177 } 178 }
2. 新建ApiAuthorizeAttribute类,继承AuthorizeAttribute,重写方法IsAuthorized,这样基于TOKEN验证方式就完成了。实现如下:
1 public class ApiAuthorizeAttribute : AuthorizeAttribute2 {3 protected override bool IsAuthorized(HttpActionContext actionContext)4 {5 // 验证token6 //var token = actionContext.Request.Headers.Authorization;7 var ts = actionContext.Request.Headers.Where(c => c.Key.ToLower() == "token").FirstOrDefault().Value;8 if (ts != null && ts.Count() > 0)9 { 10 var token = ts.First<string>(); 11 // 验证token 12 if (!UserTokenManager.IsExistToken(token)) 13 { 14 return false; 15 } 16 return true; 17 } 18 19 if (actionContext.Request.Method == HttpMethod.Options) 20 return true; 21 return false; 22 } 23 }
3. 登录实现
1 /// <summary>2 /// 账户3 /// </summary>4 public class AccountController : ApiController5 {6 /// <summary>7 /// 登录8 /// </summary>9 /// <param name="user">登录人员信息: 账号,密码 ,是否记住密码</param>10 /// <returns></returns>11 [HttpPost]12 [AllowAnonymous]13 public ResultData Login([FromBody]LoginUser user)14 {15 string mobile = user.Mobile;16 string password = user.Password;17 bool IsRememberMe = user.IsRememberMe;18 19 if (string.IsNullOrEmpty(mobile) || string.IsNullOrEmpty(password))20 return new ResultData(((int)LoginResultEnum.UserNameOrPasswordError), EnumExtension.GetEnumDescription(LoginResultEnum.UserNameOrPasswordError));21 22 User u=null;23 IMembershipService membershipSvc = ContainerManager.Container.Resolve<IMembershipService>();24 LoginResultEnum loginResult = membershipSvc.Login(mobile, password, out u);25 if (loginResult == LoginResultEnum.Success)26 {27 //SetAuthenticationTicket(u, IsRememberMe);28 29 // token 处理30 UserTokenManager.RemoveToken(u.ID);31 // 生成新Token32 var token = Utility.MD5Encrypt(string.Format("{0}{1}", Guid.NewGuid().ToString("D"), DateTime.Now.Ticks));33 // token过期时间34 int timeout = 8;35 if (!int.TryParse(ConfigurationManager.AppSettings["TokenTimeout"], out timeout))36 timeout = 8;37 // 创建新token38 var ut = new UserToken()39 {40 Token = token,41 Timeout = DateTime.Now.AddHours(timeout),42 UId = u.ID,43 UserType = (u.IsSaler.HasValue && u.IsSaler.Value) ? "Saler" : "Vip"44 };45 46 UserTokenManager.AddToken(ut);47 48 49 // 登录log50 var logRep = ContainerManager.Container.Resolve<ISysLogRepository>();51 var log = new Log()52 {53 Action = "Login",54 Detail = "会员登录:" + u.Mobile + "|" + u.Name,55 CreateDate = DateTime.Now,56 CreatorLoginName = u.Mobile,57 IpAddress = GetClientIp(this.Request)58 };59 60 logRep.Add(log);61 62 var data = new63 {64 id = u.ID,65 issaler = u.IsSaler.HasValue ? u.IsSaler.Value : false,66 mobile = u.Mobile,67 token = token68 };69 var result = new ResultData(data);70 result.desc = "登录成功";71 return result;72 }73 74 if (loginResult == LoginResultEnum.UserNameUnExists)75 {76 return new ResultData(((int)LoginResultEnum.UserNameUnExists), EnumExtension.GetEnumDescription(LoginResultEnum.UserNameUnExists));77 }78 if (loginResult == LoginResultEnum.VerifyCodeError)79 {80 return new ResultData(((int)LoginResultEnum.VerifyCodeError), EnumExtension.GetEnumDescription(LoginResultEnum.VerifyCodeError));81 }82 if (loginResult == LoginResultEnum.UserNameOrPasswordError)83 {84 return new ResultData(((int)LoginResultEnum.UserNameOrPasswordError), EnumExtension.GetEnumDescription(LoginResultEnum.UserNameOrPasswordError));85 }86 return new ResultData(ResultType.UnknowError, "登录失败,原因未知");87 }88 /// <summary>89 /// 退出当前账号90 /// </summary>91 /// <returns></returns>92 [HttpPost]93 public ResultData SignOut()94 {95 // 登录log96 var logRep = ContainerManager.Resolve<ISysLogRepository>();97 var log = new Log()98 {99 Action = "SignOut", 100 Detail = "会员退出:" + RISContext.Current.CurrentUserInfo.UserName, 101 CreateDate = DateTime.Now, 102 CreatorLoginName = RISContext.Current.CurrentUserInfo.UserName, 103 IpAddress = GetClientIp(this.Request) 104 }; 105 logRep.Add(log); 106 //System.Web.Security.FormsAuthentication.SignOut(); 107 UserTokenManager.RemoveToken(this.Token); 108 return new ResultData(ResultType.Success, "退出成功"); 109 } 110 }
4. 测试API
这样就可以配合.NET原有的 AllowAnonymousAttribute 属性使用, 使用方法如下:
不需要验证身份的 类或者Action 添加 [AllowAnonymous]属性,否则添加[ApiAuthorize]
1 /// <summary>2 /// 测试3 /// </summary>4 [ApiAuthorize]5 public class TestController : BaseApiController6 {7 /// <summary>8 /// 测试权限19 /// </summary> 10 [HttpGet] 11 public string TestAuthorize1() 12 { 13 return "TestAuthorize1"; 14 } 15 /// <summary> 16 /// 测试权限2 17 /// </summary> 18 [AllowAnonymous] 19 [HttpGet] 20 public string TestAuthorize2() 21 { 22 return "TestAuthorize2"; 23 } 24 }
测试一:
1 //TestAuthorize2 function TestAuthorize1() {3 $.ajax({4 type: "get",5 url: host + "/mobileapi/test/TestAuthorize1",6 dataType: "text",7 data: {},8 beforeSend: function (request) {9 request.setRequestHeader("token", $("#token").val()); // 请求发起前在头部附加token 10 }, 11 success: function (data) { 12 alert(data); 13 }, 14 error: function (x, y, z) { 15 alert("报错无语"); 16 } 17 }); 18 }
结果如下:
测试二:
1 //TestAuthorize2 function TestAuthorize2() {3 $.ajax({4 type: "get",5 url: host + "/mobileapi/test/TestAuthorize2",6 dataType: "text",7 data: {},8 beforeSend: function (request) {9 request.setRequestHeader("token", $("#token").val()); // 请求发起前在头部附加token 10 }, 11 success: function (data) { 12 alert(data); 13 }, 14 error: function (x, y, z) { 15 alert("报错无语"); 16 } 17 }); 18 }
结果如下:
测试三:
1 //TestAuthorize2 function TestAuthorize1() {3 $.ajax({4 type: "get",5 url: host + "/mobileapi/test/TestAuthorize1",6 dataType: "text",7 data: {},8 beforeSend: function (request) {9 //request.setRequestHeader("token", $("#token").val()); // 请求发起前在头部附加token 10 }, 11 success: function (data) { 12 alert(data); 13 }, 14 error: function (x, y, z) { 15 alert("报错无语"); 16 } 17 }); 18 }
结果如下:
测试四:
1 //TestAuthorize2 function TestAuthorize2() {3 $.ajax({4 type: "get",5 url: host + "/mobileapi/test/TestAuthorize2",6 dataType: "text",7 data: {},8 beforeSend: function (request) {9 //request.setRequestHeader("token", $("#token").val()); // 请求发起前在头部附加token 10 }, 11 success: function (data) { 12 alert(data); 13 }, 14 error: function (x, y, z) { 15 alert("报错无语"); 16 } 17 }); 18 }
结果如下:
方法二:
此方法缺点就是每次请求都需要附带token请求参数,这对于有强迫症的程序猿来说是一种折磨,不细说,实现代码如下,有需要的自己研究研究:
1 /// <summary>2 /// 用户令牌验证3 /// </summary>4 public class TokenAuthorizeAttribute : ActionFilterAttribute5 {6 private const string UserToken = "token";7 public override void OnActionExecuting(HttpActionContext actionContext)8 {9 // 匿名访问验证 10 var anonymousAction = actionContext.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>(); 11 if (!anonymousAction.Any()) 12 { 13 // 验证token 14 var token = TokenVerification(actionContext); 15 } 16 base.OnActionExecuting(actionContext); 17 } 18 19 /// <summary> 20 /// 身份令牌验证 21 /// </summary> 22 /// <param name="actionContext"></param> 23 protected virtual string TokenVerification(HttpActionContext actionContext) 24 { 25 string msg = ""; 26 // 获取token 27 var token = GetToken(actionContext, out msg); 28 if (!string.IsNullOrEmpty(msg)) 29 actionContext.Response = actionContext.Request.CreateResponse<NoAuthData>(System.Net.HttpStatusCode.OK, new NoAuthData() { code = "401", msg = msg }); 30 // 判断token是否有效 31 if (!UserTokenManager.IsExistToken(token)) 32 { 33 actionContext.Response = actionContext.Request.CreateResponse<NoAuthData>(System.Net.HttpStatusCode.OK, new NoAuthData() { code = "401", msg = "Token已失效,请重新登录!" }); 34 //actionContext.Response = actionContext.Request.CreateResponse<NoAuthData>(System.Net.HttpStatusCode.Unauthorized, new NoAuthData() { code = "401", msg = "Token已失效,请重新登录!" }); 35 // actionContext.Response = actionContext.Request.CreateErrorResponse(System.Net.HttpStatusCode.Unauthorized, "Token已失效,请重新登录!"); 36 } 37 38 return token; 39 } 40 41 private string GetToken(HttpActionContext actionContext, out string msg) 42 { 43 Dictionary<string, object> actionArguments = actionContext.ActionArguments; 44 HttpMethod type = actionContext.Request.Method; 45 msg = ""; 46 var token = ""; 47 if (type == HttpMethod.Post) 48 { 49 if (actionArguments.ContainsKey(UserToken)) 50 { 51 if (actionArguments[UserToken] != null) 52 token = actionArguments[UserToken].ToString(); 53 } 54 else 55 { 56 foreach (var value in actionArguments.Values) 57 { 58 if (value != null && value.GetType().GetProperty(UserToken) != null) 59 token = value.GetType().GetProperty(UserToken).GetValue(value, null).ToString(); 60 } 61 } 62 63 if (string.IsNullOrEmpty(token)) 64 msg = "登录超时,请重新登录!"; 65 } 66 else if (type == HttpMethod.Get) 67 { 68 if (!actionArguments.ContainsKey(UserToken)) 69 msg = "还未登录"; 70 // throw new HttpException(401, "还未登录"); 71 72 if (actionArguments[UserToken] != null) 73 token = actionArguments[UserToken].ToString(); 74 else 75 msg = "登录超时,请重新登录!"; 76 } 77 else 78 { 79 throw new HttpException(404, "暂未开放除POST,GET之外的访问方式!"); 80 } 81 return token; 82 } 83 } 84 85 public class NoAuthData 86 { 87 public string code { get; set; } 88 public string msg { get; set; } 89 }
此篇到此结束,欢迎大家讨论!