首页 > 代码库 > 基于.Net Framework 4.0 Web API开发(4):ASP.NET Web APIs 基于令牌TOKEN验证的实现
基于.Net Framework 4.0 Web API开发(4):ASP.NET Web APIs 基于令牌TOKEN验证的实现
概述:
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 UserTokenManager 2 { 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 的过期时间, 令牌过期时间 *2 19 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 else 46 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 else 55 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 else 68 { 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 else 78 { 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 else117 {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 : AuthorizeAttribute 2 { 3 protected override bool IsAuthorized(HttpActionContext actionContext) 4 { 5 // 验证token 6 //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 // 验证token12 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 : ApiController 5 { 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 // 生成新Token 32 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 // 创建新token 38 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 // 登录log 50 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 = http://www.mamicode.com/new"登录成功"; 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 // 登录log 96 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 : BaseApiController 6 { 7 /// <summary> 8 /// 测试权限1 9 /// </summary>10 [HttpGet]11 public string TestAuthorize1()12 {13 return "TestAuthorize1";14 }15 /// <summary>16 /// 测试权限217 /// </summary>18 [AllowAnonymous]19 [HttpGet]20 public string TestAuthorize2()21 {22 return "TestAuthorize2";23 }24 }
测试一:
1 //TestAuthorize 2 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()); // 请求发起前在头部附加token10 },11 success: function (data) {12 alert(data);13 },14 error: function (x, y, z) {15 alert("报错无语");16 }17 });18 }
结果如下:
测试二:
1 //TestAuthorize 2 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()); // 请求发起前在头部附加token10 },11 success: function (data) {12 alert(data);13 },14 error: function (x, y, z) {15 alert("报错无语");16 }17 });18 }
结果如下:
测试三:
1 //TestAuthorize 2 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()); // 请求发起前在头部附加token10 },11 success: function (data) {12 alert(data);13 },14 error: function (x, y, z) {15 alert("报错无语");16 }17 });18 }
结果如下:
测试四:
1 //TestAuthorize 2 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()); // 请求发起前在头部附加token10 },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 : ActionFilterAttribute 5 { 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 // 验证token14 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 // 获取token27 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 else55 {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 else75 msg = "登录超时,请重新登录!";76 }77 else78 {79 throw new HttpException(404, "暂未开放除POST,GET之外的访问方式!");80 }81 return token;82 }83 }84 85 public class NoAuthData86 {87 public string code { get; set; }88 public string msg { get; set; }89 }
基于.Net Framework 4.0 Web API开发(4):ASP.NET Web APIs 基于令牌TOKEN验证的实现