你的位置:首页 > ASP.net教程

[ASP.net教程]基于.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   }

View Code

     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   }

View Code

     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 = new 63         { 64           id = u.ID, 65           issaler = u.IsSaler.HasValue ? u.IsSaler.Value : false, 66           mobile = u.Mobile, 67           token = token 68         }; 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       // 登录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   }

View Code

    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   }

View Code


此篇到此结束,欢迎大家讨论!