iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 22
0
Modern Web

WebGIS入門學習 - 以Openlayers實作系列 第 22

Day 22. API登入權限控管機制 #3:Token編碼與加密設計

前言

今天的是我自己也比較感興趣的主題,就是token設計這部分,token不能讓別人太好猜到,因此不能直接使用簡單的編碼而已,而是應該進行加密設計。
加密的部分又涉及了資安和很多方面,這部分我也是個初學者,參考了很多文章後組成我現在的模式,大家可以多方參考各路大神的文章,這邊就以我使用的方法來介紹。

今天的文章也比較短~ 可以稍微休息一下吸收一下再出發!

今天的大綱

  1. Token編碼與加密設計
  2. 帳號密碼獲取token的swagger測試畫面
  3. 修正validatesToken
  4. 將其他API鎖上,需header帶token驗證才可取用

1. Token編碼與加密設計

由昨天 Day 21 文章可以知道整個token產生的流程,這邊再複習一次:

  1. 首先,先run delExpiredToken(),將資料表內所有超過有效期限的token都刪除。
  2. 接著利用 createToken() 來進行token的建立。
    - 這支呼叫了 generateToken() 進行建立。
    - 昨天的文章是用亂數,今天要進行token設計。
    - 利用 getClientIP() 去撈取ip資訊。
    - 將建好的token寫入 sys_tokens 資料表,並設計有效期限只有8小時 (可自行設定)。
    - 回傳token。
  3. 每次執行api都要帶token,建置 validatesToken() 來驗證其有效性,並回傳 userid

所以今天的任務很簡單,就是設計token。

於是Google了一下,發現這篇文章寫得蠻完整的,可以參考:聽說不能用明文存密碼,那到底該怎麼存?

token的設計應該要隱含使用者的相關資訊,但又不包含特別機敏的資料;
由於token的權限是用來限制使用者存取API資訊,根據我微弱的資安常識,不應該只使用 BASE64編碼 或使用已知不安全的加密方法如 MD5SHA1 進行設計,這部分跟密碼的儲存有相似的情形。
不能用上述的方法,但可使用 AESRSASHA256等加密方式

之前有學過影像處理的人應該一看到加SALT就知道大概會跟 椒鹽雜訊(salt and papper noise) 有異曲同工之妙

影像的椒鹽雜訊(salt and papper noise):
https://ithelp.ithome.com.tw/upload/images/20201001/20108631EK6cWovRPU.png

因為我們現在還沒有對使用者進行角色的分類 (這系列到第30天都不會有,有興趣的人可以自己設計)
如果有登入角色的資訊,token的設計可使用:角色+帳號+相關資訊

但今天沒有角色資訊,generateToken() 就改為:現在日期、帳號、SALT(隨機產生),利用Encoding.UTF8.GetBytes()Cryptography.SHA256Managed().ComputeHash(),再轉為Base64 Convert.ToBase64String() 進行token的設計。

採加SALT的方式, 將原本的 GetRandomString() 改為下面的程式碼 ,執行後隨機產生一串包含特殊字符的string同時存入資料庫,並加進去編碼。

public static string GetRandomString(int length)
{
    byte[] b = new byte[4];
    new System.Security.Cryptography.RNGCryptoServiceProvider().GetBytes(b);
    Random r = new Random(BitConverter.ToInt32(b, 0));
    string s = null, str = "";
    str += "0123456789";
    str += "abcdefghijklmnopqrstuvwxyz";
    str += "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    str += "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
    for (int i = 0; i < length; i++)
    {
        s += str.Substring(r.Next(0, str.Length - 1), 1);
    }
    return s;
}

dbo.sys_token 新增一個欄位:salt nvarchar(10) 紀錄隨機產生的salt資訊
createToken() 中,利用 GetRandomString(10) 取得10碼包含特殊字元的salt,並執行 generateToken()

回傳token時,於前方帶入 "OLMapAPI "字串作為識別。

private string createToken(string userid, string type)
{
    string salt = GetRandomString(10);
    string token = generateToken(userid, salt);
    delExpiredToken();
    string sqlstr = @"INSERT INTO sys_tokens (userid,type,token,ip,issuedOn,expiredOn,salt) Values (@userid,@type,@token,@ip,@issuedOn,@expiredOn,@salt)";
    SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["DefaultConnection"].ConnectionString);
    SqlCommand cmd = new SqlCommand(sqlstr, conn);
    conn.Open();
    cmd.Parameters.AddWithValue("@userid", userid);
    cmd.Parameters.AddWithValue("@type", type);
    cmd.Parameters.AddWithValue("@token", token);
    cmd.Parameters.AddWithValue("@ip", getClientIP());
    cmd.Parameters.AddWithValue("@issuedOn", DateTime.Now);
    cmd.Parameters.AddWithValue("@expiredOn", DateTime.Now.AddHours(8)); //8小時後刪除
    cmd.Parameters.AddWithValue("@salt", salt);
    SqlDataReader dr = cmd.ExecuteReader();
    dr.Close(); dr.Dispose(); conn.Close(); conn.Dispose();
    return "OLMapAPI " + token;
}

protected string generateToken(string userid, string salt)
{
    DateTime dt = DateTime.Now;
    byte[] useridAndSaltBytes = System.Text.Encoding.UTF8.GetBytes(dt.ToFileTime().ToString() + "-" + userid + salt);
    byte[] hashBytes = new System.Security.Cryptography.SHA256Managed().ComputeHash(useridAndSaltBytes);
    string hashString = Convert.ToBase64String(hashBytes);
    return hashString;
}

2. 帳號密碼獲取token的swagger測試畫面

輸入:
https://ithelp.ithome.com.tw/upload/images/20201001/201086315X9i7iA50m.png

輸出:
https://ithelp.ithome.com.tw/upload/images/20201001/20108631Sd9PEBpeDK.png

資料庫:
id|userid|type|token|ip|issuedOn|expiredOn|salt
------|------
18|apiUser_admin|apiuser|3C8iq3bF8Rnexisw5mkyJt2gyo1ipJ70nNq/Uk4h7Ug=|::1|2020-10-01 16:46:37.190|2020-10-02 00:46:37.190|Z{ceFfL#lb

3. 修正validatesToken

因為回傳的Token前面多了 "OLMapAPI ",在進行比對時須先將"OLMapAPI " 移除。

/// <summary>
/// 驗證token
/// </summary>
public string validatesToken(string token)
{
    SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["DefaultConnection"].ConnectionString);
    SqlCommand cmd = new SqlCommand("select userid from sys_tokens where token=@token and  GETDATE()<=expiredOn", conn);
    conn.Open();
    // 移除 "OLMapAPI "
    cmd.Parameters.AddWithValue("@token", token.Replace("OLMapAPI ", ""));
    SqlDataReader dr = cmd.ExecuteReader();
    string userid = "";
    while (dr.Read())
    {
        userid = dr["userid"].ToString();
    }
    dr.Close(); dr.Dispose(); conn.Close(); conn.Dispose();
    return userid;
}

4. 將其他API鎖上,需header帶token驗證才可取用

將原本功能加入 [Authorize] (反註解),以 getLayerResource() 為例,見下圖。
https://ithelp.ithome.com.tw/upload/images/20201001/20108631Oc6BJYFACR.png

修正 'App_Start/SwaggerConfig.cs'

把下面那一段反註解,apiKey 為預設的 header name

c.EnableApiKeySupport("apiKey", "header");

並將 apiKey 修改為Authorization

c.EnableApiKeySupport("Authorization", "header");

利用swagger進行測試

  1. 測試若在沒有輸入token的情況下執行 getLayerResource(),顯示 "Message": "已拒絕此要求的授權。"
    https://ithelp.ithome.com.tw/upload/images/20201001/20108631SUlBEAbQvY.png

  2. 測試若在輸入token的情況下執行 getLayerResource()
    https://ithelp.ithome.com.tw/upload/images/20201001/20108631B7RjwCsYgM.png
    回傳正確的JSON檔資料,也可以在 header 那邊看到Authorization 的參數值,測試成功!
    https://ithelp.ithome.com.tw/upload/images/20201001/20108631GsBV2HYZzi.png


小結

今天已經學會了如何產製token,本篇文章的產製方法可能不會是最好的,尚有許多沒考慮到的因素,像是驗證token時應該將token回復原始資訊並拆解後比對,達到雙重認證,而不是直接去比對資料庫的token是否一致;這邊只是說明基本的架構,有興趣的話可以再去深入了解。

既然今天已經將所有API都鎖上了,但現在都還僅只修改後端的部分,明天我們就要將新的API機制套用在前端,並進行帳號權限的控管,登入權限控管機制的部分也會在明天告一個段落了!


上一篇
Day 21. API登入權限控管機制 #2:帳號驗證與token回傳流程建立
下一篇
Day 23. API登入權限控管機制 #4:前端套用帳號驗證與API權限管控
系列文
WebGIS入門學習 - 以Openlayers實作30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言