在文章中有錯誤的地方,或是有建議或意見的地方,請大家多多指正,郵箱: linjie.rd@gmail.com
一天張三,李四,王五,趙六去動物園,張三沒買票,李四制作了個假票,王五買了票,趙六要直接翻牆進動物園
到了門口,驗票的時候,張三沒有買票被拒絕進入動物園,李四因為買假票而被補,趙六被執勤人員抓獲,只有張三進去了動物園
后來大家才知道,當一個用戶帶着自己的信息去買票的時候,驗證自己的信息是否正確,那真實的身份證(正確的用戶名和密碼),驗證通過以后通過身份證信息和票據打印時間(用戶登錄時間)生成一個新的動物園參觀票(Token令牌),給了用戶一個,在動物園門口也保存了票據信息(相當與客戶端和服務端都保存一份),在進動物園的時候兩個票據信息對比,正確的就可以進動物園玩了
這就是我理解的Token認證.當然可能我的比喻不太正確,望大家多多諒解
下面是我們在服務端定義的授權過濾器
思路是根據切面編程的思想,相當於二戰時期城樓門口設立的卡,當用戶想api發起請求的時候,授權過濾器在api執行動作之前執行,獲取到用戶信息
如果發現用戶沒有登錄,我們會判斷用戶要訪問的頁面是否允許匿名訪問
用戶沒有登錄但是允許匿名訪問,放行客戶端的請求
用戶沒有登錄且不允許匿名訪問,不允許通過,告訴客戶端,狀態碼403或401,請求被拒絕了
如果發現用戶登錄,判斷用戶的良民證(Token令牌)是真的還是假的
用戶登錄,且良民證是真的,放行
發現良民證造價,抓起來,不允許訪問
當然,這里可以加權限,驗證是否有某個操作的權限
1 public class UserTokenAttribute : AuthorizeAttribute 2 { 3 protected override void HandleUnauthorizedRequest(HttpActionContext filterContext) 4 { 5 6 base.HandleUnauthorizedRequest(filterContext); 7 var response = filterContext.Response = filterContext.Response ?? new HttpResponseMessage(); 8 response.StatusCode = HttpStatusCode.Forbidden; 9 ResultModel model = new ResultModel(); 10 11 model.result_code = ResultCode.Error; 12 model.result_data = null; 13 model.result_mess = "您沒有授權!"; 14 15 response.Content = new StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(model), Encoding.UTF8, "application/json"); 16 } 17 public override void OnAuthorization(HttpActionContext actionContext) 18 { 19 //從http請求的頭里面獲取身份驗證信息,驗證token值是否有效 20 string token = GetToken(actionContext); 21 22 if (!string.IsNullOrEmpty(token)) 23 { 24 if (UserService.Instance.ValidateToken(token)) 25 { 26 base.IsAuthorized(actionContext); 27 } 28 else 29 { 30 HandleUnauthorizedRequest(actionContext); 31 } 32 } 33 34 else//如果取不到token值,並且不允許匿名訪問,則返回未驗證401 35 { 36 var attributes = actionContext.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().OfType<AllowAnonymousAttribute>(); 37 bool isAnonymous = attributes.Any(a => a is AllowAnonymousAttribute);//驗證當前動作是否允許匿名訪問 38 if (isAnonymous) 39 { 40 base.OnAuthorization(actionContext); 41 } 42 else 43 { 44 HandleUnauthorizedRequest(actionContext); 45 } 46 } 47 } 48 public string GetToken(HttpActionContext actionContext) 49 { 50 if (!string.IsNullOrEmpty(System.Web.HttpContext.Current.Request["token"])) 51 return System.Web.HttpContext.Current.Request["token"].ToString(); 52 if (System.Web.HttpContext.Current.Request.InputStream.Length > 0) 53 { 54 string json = new System.IO.StreamReader(System.Web.HttpContext.Current.Request.InputStream).ReadToEnd(); 55 Dictionary<string, object> di = Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<string, object>>(json); 56 if (di.Keys.Contains("token")) return di["token"].ToString(); 57 } 58 return ""; 59 } 60 61 62 }
好了,服務端有驗證了,客戶端也不能拉下啊,客戶端使用了動作過濾器,在用戶操作之前或用戶操作之后驗證登錄信息(這里可以加權限,驗證是否有某個操作的權限)
客戶端驗證思路和服務端驗證差不多
下面是客戶端驗證代碼:
1 public class LoginVerificationAttribute : ActionFilterAttribute 2 { 3 public override void OnActionExecuting(System.Web.Mvc.ActionExecutingContext filterContext) 4 { 5 #region 是否登錄 6 var userInfo = UserManager.GetLoginUser(); 7 if (userInfo != null) 8 { 9 base.OnActionExecuting(filterContext); 10 } 11 else 12 { 13 if (filterContext.ActionDescriptor.IsDefined(typeof(AllowAnonymousAttribute), true) 14 || filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof(AllowAnonymousAttribute), true)) 15 { 16 base.OnActionExecuting(filterContext); 17 } 18 else 19 { 20 System.Web.HttpContext.Current.Session["token"] = null; 21 filterContext.HttpContext.Response.Redirect("/Common/Login", true); 22 filterContext.HttpContext.Response.End(); 23 } 24 } 25 #endregion 26 } 27 }
但是有良民證也不能也不能無限制的待在城里啊,我們做了一個時效性,在城市里什么時也不做到達一定的時長后得驅逐出城啊(類似與游戲中的掛機超過一定時間后T出本局游戲)
在這里使用的Redis記錄良民證(Token),思路是用戶登錄之后生成的新的Token保存在Redis上,設定保存時間20分鍾,當有用戶有動作之后更新Redis保存有效期
下面是服務端驗證token的,token有效,從新寫入到Redis
1 public UserInfoDetail GetUserIdInCachen(string token) 2 { 3 SimpleTokenInfo tokenInfo = JwtUtil.Decode<SimpleTokenInfo>(token); 4 if (tokenInfo == null) return null; 5 RedisHelper redisHelper = new RedisHelper(); 6 string userJson = redisHelper.Get<string>(tokenInfo.UserName); 7 if (!string.IsNullOrEmpty(userJson)) 8 { 9 10 UserAndToken userAndToken = JsonConvert.DeserializeObject<UserAndToken>(userJson); 11 if (userAndToken.Token.Equals(token)) 12 { 13 redisHelper.Set(tokenInfo.UserName, userJson, DateTime.Now.AddSeconds(expireSecond)); 14 return userAndToken; 15 } 16 17 return null; 18 } 19 return null; 20 }
以上就是Token認證
現在說說單點登錄的思路
張三登錄了qq:123456,生成了一個Token以鍵值對的方式保存在了數據庫,鍵就是qq號,值就是qq信息和登錄時間生成的一個Token
李四也登錄了qq123456,qq信息是一致的,但是qq登錄時間不同,生成了一個新的Token,在保存的時候發現Redis里已經存在這個qq的鍵了,說明這是已經有人登錄了,在這里可以判斷是否繼續登錄,登錄后新的Token信息覆蓋了張三登錄QQ生成的Token,張三的Token失效了,當他再次請求的時候發現Token對應不上,被T下線了
多點登錄也是,可以通過qq號加客戶端類型作為鍵,這樣手機qq登錄的鍵是 123456_手機,電腦登錄的鍵是123456_電腦,這樣在保存到Redis的時候就不會發生沖突,可以保持手機和電腦同時在線
但是有一個人用手機登錄qq 123456了,就會覆蓋redis中鍵為123456_手機的Token信息,導致原先登錄那個人的信息失效,被強制下線
來展示代碼
判斷是否可以登錄
1 public string GetToken(string userName, string passWord, ClientType clientType) 2 { 3 UserInfoDetail user = GetModel(userName); 4 if (user != null) 5 { 6 if (user.Pwd.ToUpper() == StringMd5.Md5Hash32Salt(passWord).ToUpper()) 7 { 8 return GetToken(user, clientType); 9 } 10 } 11 return ""; 12 }
客戶端類型實體
這是我們的客戶端類型:
1 public enum ClientType 2 { 3 /// <summary> 4 /// 主站 5 /// </summary> 6 MasetSize, 7 /// <summary> 8 /// 微信小程序 9 /// </summary> 10 WeChatSmallProgram, 11 /// <summary> 12 /// WinForm端 13 /// </summary> 14 WinForm, 15 /// <summary> 16 /// App端 17 /// </summary> 18 App 19 }
獲取token需要的用戶信息和登錄時間的實體Model
這是我們的用戶Model
1 public partial class UserInfoDetail 2 { 3 public UserInfoDetail() 4 { } 5 public string Admin{ get; set; } 6 public string Pwd { get; set; } 7 }
生成Token用的實體
1 public class SimpleTokenInfo 2 { 3 public SimpleTokenInfo() 4 { 5 CreateTimeStr = DateTime.Now.ToString("yyyyMMddhhmmss"); 6 7 } 8 /// <summary> 9 /// 創建日期 10 /// </summary> 11 public string CreateTimeStr { get; set; } 12 /// <summary> 13 /// 用戶賬戶 14 /// </summary> 15 public string UserName { get; set; } 16 }
登錄成功,通過JWT非對稱加密生成Token
1 /// <summary> 2 /// 獲取token 3 /// </summary> 4 /// <param name="user">用戶</param> 5 /// <returns></returns> 6 public string GetToken(UserInfoDetail user, ClientType clientType) 7 { 8 RedisHelper redisHelper = new RedisHelper(); 9 SimpleTokenInfo tokenInfo = new SimpleTokenInfo(); 10 tokenInfo.UserName = user.Admin + "_" + ((int)clientType).ToString(); 11 string token = JwtUtil.Encode(tokenInfo); 12 UserAndToken userAndToken = new UserAndToken(); 13 userAndToken.Token = token; 14 ModelCopier.CopyModel(user, userAndToken); 15 string userJson = JsonConvert.SerializeObject(userAndToken); 16 redisHelper.Set(tokenInfo.UserName, userJson, DateTime.Now.AddSeconds(expireSecond)); 17 return token; 18 }
下面是JWT加密和解密的代碼
1 public class JwtUtil 2 { 3 private static string secret = "***********";//這個服務端加密秘鑰 屬於私鑰 4 5 public static string Encode(object obj) 6 { 7 IJwtAlgorithm algorithm = new HMACSHA256Algorithm(); 8 IJsonSerializer serializer = new JsonNetSerializer(); 9 IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder(); 10 IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder); 11 var token = encoder.Encode(obj, secret); 12 return token; 13 } 14 public static T Decode<T>(string token) 15 { 16 string json; 17 try 18 { 19 IJsonSerializer serializer = new JsonNetSerializer(); 20 IDateTimeProvider provider = new UtcDateTimeProvider(); 21 IJwtValidator validator = new JwtValidator(serializer, provider); 22 IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder(); 23 IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder); 24 json = decoder.Decode(token, secret, verify: false);//token為之前生成的字符串 25 T model = JsonConvert.DeserializeObject<T>(json); 26 //對時間和用戶賬戶密碼進行認證 27 return model; 28 } 29 catch (Exception) 30 { 31 return default(T); 32 } 33 34 } 35 }
將獲取到的Token保存到Redis
1 /// <summary> 2 /// 存儲數據到hash表 3 /// </summary> 4 public bool Set(string key, string value, DateTime tmpExpire) 5 { 6 try 7 { 8 9 Redis.Set(key, value, tmpExpire); 10 Save(); 11 } 12 catch 13 { 14 return false; 15 } 16 return true; 17 }
展示一下,在服務端,繼承控制器實現登錄驗證:
1 /// <summary> 2 /// 驗證登錄,繼承此控制器限制登錄才可以訪問 3 /// </summary> 4 [UserToken] 5 public class LoginVerificationController : BaseApiController 6 { 7 8 }
在服務端的驗證方法,在方法前加[AllowAnonymous]是允許匿名訪問,也就是不登錄訪問,
下面以用戶中心控制器為例
1 public class UserController : LoginVerificationController 2 { 3 4 /// <summary> 5 /// 用戶登錄,獲取token 6 /// </summary> 7 /// <param name="userName">用戶名</param> 8 /// <param name="passWord">密碼</param> 9 /// <param name="clientType">客戶端類型</param> 10 /// <returns></returns> 11 [HttpGet] 12 [AllowAnonymous] 13 public ResultModel GetToken(string userName, string passWord, int clientType) 14 { 15 ResultModel model = new ResultModel(); 16 17 if (!Enum.IsDefined(typeof(ClientType), clientType)) 18 { 19 model.result_code = ResultCode.Error; 20 model.result_data = ""; 21 model.result_mess = "客戶端類型不正確!"; 22 return model; 23 } 24 string token = UserService.Instance.GetToken(userName, passWord, (ClientType)clientType); 25 if (string.IsNullOrEmpty(token)) 26 { 27 model.result_code = ResultCode.Fail; 28 model.result_data = ""; 29 model.result_mess = "獲取token失敗!"; 30 } 31 else 32 { 33 model.result_code = ResultCode.Ok; 34 model.result_data = token; 35 model.result_mess = "獲取token成功!"; 36 } 37 38 return model; 39 } 40 /// <summary> 41 /// 獲取用戶 42 /// </summary> 43 /// <param name="token">令牌</param> 44 /// <returns></returns> 45 public ResultModel GetUserInfo(string token) 46 { 47 ResultModel model = new ResultModel(); 48 try 49 { 50 UserInfoDetail user = UserService.Instance.GetUser(token); 51 if (user != null) 52 { 53 model.result_code = ResultCode.Ok; 54 model.result_data = user; 55 model.result_mess = "獲取用戶成功!"; 56 } 57 else 58 { 59 model.result_code = ResultCode.Fail; 60 model.result_data = ""; 61 model.result_mess = "獲取用戶失敗!"; 62 63 } 64 } 65 catch (Exception ex) 66 { 67 model.result_code = ResultCode.Error; 68 model.result_data = null; 69 model.result_mess = ex.ToString(); 70 } 71 return model; 72 } 73 74 /// <summary> 75 /// 驗證用戶名有效性 76 /// </summary> 77 /// <param name="user">用戶model 包含用戶名和token</param> 78 /// <returns></returns> 79 [HttpPost] 80 public ResultModel VUserName([FromBody]User user) 81 { 82 ResultModel model = new ResultModel(); 83 try 84 { 85 86 if (string.IsNullOrEmpty(user.username)) 87 { 88 model.result_code = ResultCode.Ok; 89 model.result_data = ""; 90 model.result_mess = "用戶名可以使用!"; 91 } 92 else 93 { 94 model.result_code = ResultCode.Fail; 95 model.result_data = ""; 96 model.result_mess = "用戶名已經存在!"; 97 } 98 } 99 catch (Exception ex) 100 { 101 model.result_code = ResultCode.Error; 102 model.result_data = null; 103 model.result_mess = ex.ToString(); 104 } 105 return model; 106 } 107 }
1 public class User 2 { 3 public string username { get; set; } 4 public string token { get; set; } 5 }
客戶端使用也類似,在方法前加[AllowAnonymous]是該方法允許匿名訪問,下面是客戶端我封裝的Base控制器
1 public class BaseController : Controller 2 { 3 public BaseController() 4 { 5 UserInfoDetail userinfo= GetUserInfo(); 6 ViewBag.User=userinfo?.Admin??"請登陸"; 7 ViewBag.UserId = GetToken(); 8 } 9 public string GetAPI(string method, string uri,object o=null) 10 { 11 12 string str = string.Empty; 13 HttpContent content = new StringContent(JsonConvert.SerializeObject(o)); 14 using (HttpClient client = new HttpClient()) 15 { 16 client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 17 content.Headers.ContentType =new MediaTypeHeaderValue("application/json"); 18 HttpResponseMessage response=null; 19 if(method.ToLower()=="get") 20 { 21 response = client.GetAsync(uri).Result; 22 } 23 else if(method.ToLower()=="post") 24 { 25 response = client.PostAsync(uri, content).Result; 26 } 27 else if(method.ToLower()=="put") 28 { 29 response = client.PutAsync(uri, content).Result; 30 } 31 else if(method.ToLower()=="delete") 32 { 33 response = client.DeleteAsync(uri).Result; 34 } 35 if(response.IsSuccessStatusCode) 36 { 37 str = response.Content.ReadAsStringAsync().Result; 38 } 39 } 40 return str; 41 } 42 43 /// <summary> 44 /// 獲取用戶信息 45 /// </summary> 46 /// <returns></returns> 47 public UserInfoDetail GetUserInfo() 48 { 49 return UserManager.GetLoginUser(); 50 } 51 52 /// <summary> 53 /// 獲取Token 54 /// </summary> 55 /// <returns></returns> 56 public string GetToken() 57 { 58 return UserManager.GetLoginToken(); 59 } 60 }
使用方法如下
1 /// <summary> 2 /// 繼承此控制器表示登錄驗證 3 /// </summary> 4 [LoginVerification] 5 public class LoginVerificationController : BaseController 6 { 7 8 //[AllowAnonymous]//該特性表示允許匿名訪問該方法 9 //public void Test() 10 //{ 11 12 //} 13 }
看了自己寫寫的東西,現在想起來,為什么小學語文是體育老師教的啊