基於Token認證的多點登錄和WebApi保護


  在文章中有錯誤的地方,或是有建議或意見的地方,請大家多多指正,郵箱: 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     }

 

 

看了自己寫寫的東西,現在想起來,為什么小學語文是體育老師教的啊

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM