今天的是我自己也比較感興趣的主題,就是token設計這部分,token不能讓別人太好猜到,因此不能直接使用簡單的編碼而已,而是應該進行加密設計。
加密的部分又涉及了資安和很多方面,這部分我也是個初學者,參考了很多文章後組成我現在的模式,大家可以多方參考各路大神的文章,這邊就以我使用的方法來介紹。
今天的文章也比較短~ 可以稍微休息一下吸收一下再出發!
由昨天 Day 21 文章可以知道整個token產生的流程,這邊再複習一次:
- 首先,先run
delExpiredToken()
,將資料表內所有超過有效期限的token都刪除。- 接著利用
createToken()
來進行token的建立。
- 這支呼叫了generateToken()
進行建立。
- 昨天的文章是用亂數,今天要進行token設計。
- 利用getClientIP()
去撈取ip資訊。
- 將建好的token寫入sys_tokens
資料表,並設計有效期限只有8小時 (可自行設定)。
- 回傳token。- 每次執行api都要帶token,建置
validatesToken()
來驗證其有效性,並回傳userid
。
所以今天的任務很簡單,就是設計token。
於是Google了一下,發現這篇文章寫得蠻完整的,可以參考:聽說不能用明文存密碼,那到底該怎麼存?
token的設計應該要隱含使用者的相關資訊,但又不包含特別機敏的資料;
由於token的權限是用來限制使用者存取API資訊,根據我微弱的資安常識,不應該只使用 BASE64編碼
或使用已知不安全的加密方法如 MD5
、SHA1
進行設計,這部分跟密碼的儲存有相似的情形。
不能用上述的方法,但可使用 AES
、RSA
、SHA256
等加密方式
之前有學過影像處理的人應該一看到加SALT就知道大概會跟 椒鹽雜訊(salt and papper noise)
有異曲同工之妙
影像的椒鹽雜訊(salt and papper noise):
因為我們現在還沒有對使用者進行角色的分類 (這系列到第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;
}
輸入:
輸出:
資料庫:
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
因為回傳的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;
}
將原本功能加入 [Authorize]
(反註解),以 getLayerResource()
為例,見下圖。
修正 'App_Start/SwaggerConfig.cs'
把下面那一段反註解,apiKey
為預設的 header name
c.EnableApiKeySupport("apiKey", "header");
並將 apiKey
修改為Authorization
c.EnableApiKeySupport("Authorization", "header");
利用swagger進行測試
測試若在沒有輸入token的情況下執行 getLayerResource()
,顯示 "Message": "已拒絕此要求的授權。"
測試若在輸入token的情況下執行 getLayerResource()
回傳正確的JSON檔資料,也可以在 header
那邊看到Authorization
的參數值,測試成功!
今天已經學會了如何產製token,本篇文章的產製方法可能不會是最好的,尚有許多沒考慮到的因素,像是驗證token時應該將token回復原始資訊並拆解後比對,達到雙重認證,而不是直接去比對資料庫的token是否一致;這邊只是說明基本的架構,有興趣的話可以再去深入了解。
既然今天已經將所有API都鎖上了,但現在都還僅只修改後端的部分,明天我們就要將新的API機制套用在前端,並進行帳號權限的控管,登入權限控管機制的部分也會在明天告一個段落了!