最近想要作一個小項目,因爲先後都是一我的,在登陸和註冊的接口上就被卡住了,所以想登陸、註冊、口令之間的關係,使用 PHP 實現登陸註冊模塊,和訪問口令。javascript
出於安全的考慮,首先定下三項原則:php
在網絡來講,咱們知道不論 POST 請求和 GET 請求都會被抓包,在沒有使用 HTTPS 的狀況下,抓包咱們是防不住的,若是明文傳輸用戶隱私,那後果就不說了。java
本地和服務器也是如此,好比 iOS 設備,若是存儲在本地,越獄以後經過設備 Finder 之類的功能,也能輕易找到咱們存儲在本地的用戶隱私。python
使用 Keychain 在本地也有保存,但不在沙盒,暫且忽略。mysql
上面講到,用戶隱私數據總歸能夠被拿到的,如何保證被拿到以後不會被用來作壞事?算法
將用戶的隱私數據加密,那麼就算被拿到,也沒法被拿來使用。在這裏呢,咱們先不談加密,而是先糾正一個誤區,有些朋友會認爲 Base64 能夠加密,甚至有 Base64 加密的說法。sql
Base64 主要不是加密,它主要的用途是把二進制數據序列轉化爲 ASCII 字符序列,用以數據傳輸。二進制數據是什麼呢?計算機上存儲的全部數據,都是二進制數據。數據庫
Base64 最多見的應用場景是 URL,由於 URL 只能是特定的一些 ASCII 字符。這時須要用到 Base64 編碼,固然這也只是對二進制數據自己的編碼,編碼後的數據裏面可能包含 +/=
等符號,真正放到 URL 裏面時候,還須要URL-Encoding,變成 %XX
模式,以消除這些符號的歧義。其次就是將圖片轉爲 Base64 的字符串。json
所以,Base64 只是一種編碼方式,而不是加密方式。swift
好了,如今回到咱們的主題,先說登陸和註冊之間的關係,這 3 個模塊須要作什麼事情呢?
咱們把用戶輸入的隱私數據再具象一些,好比帳號和密碼,結合咱們上面提到的安全原則,那麼分解開來,實際咱們要作如下幾件事:
上面邏輯理清楚後,相信對於你們來講並不難實現,如下是服務器註冊接口作的事情:
/*獲取 get 請求傳遞的參數*/
$account = $_GET['account'];
$password = $_GET['password'];
/*建立數據鏈接*/
$db = new DataBase();
/*檢查用戶名是否存在*/
$is_exist = $db->check_user_exist($account);
if ($is_exist) {
echo return_value(10001, false);
}
else {
/*檢查用戶名是否添加成功*/
$result = $db->add_user($account, $password);
if ($result) {
echo return_value(0, true);
}
else {
echo return_value(20001, false);
}
}複製代碼
如今是服務器登陸接口作的事情:
/*獲取 get 請求傳遞的參數*/
$account = $_GET['account'];
$password = $_GET['password'];
/*建立數據鏈接*/
$db = new DataBase();
/*是否命中用戶名和密碼*/
$should_login = $db->should_login($account, $password);
if ($should_login) {
/*更新 token*/
$token = $db->insert_token($account);
if ($token == '') {
echo response(40001, false);
}
else {
$data = ['token' => $token];
echo response(0, $data);
}
}
else {
echo response(30001, false);
}複製代碼
剩下的無非是加密算法的不一樣,我最經常使用的是 md5,那麼咱們通過 md5 加密之後,其實仍是不太安全,爲何呢?由於 md5 自己就不安全。雖然 md5 是不可逆的 hash 算法,反向算出來雖然困難,可是若是反向查詢,密碼設置的簡單,也很容易被攻破。
好比咱們使用 md5 加密一個密碼 123456
,對應的 md5 是 e10adc3949ba59abbe56e057f20f883e
,找到一個 md5 解密的網站,好比 cmd5.com/,很容易就被破解了密碼…
工做一段時間的同窗對這個名詞應該不會陌生,這種方式算是給用戶的隱私數據加上密了,其實就是一段隱私數據加一段亂碼再進行 md5,用代碼表示大體是這樣:
// 僞代碼
salt = '#^&%**(^&(&*)_)_(*&^&#$%GVHKBJ(*^&*%^%&^&'
password = '123456'
post_body = salt + password
print post_body.md5()
// ffb34d898f6573a1cf14fdc34d3343c0複製代碼
如今,密碼看起來挺靠譜的了,可是,咱們知道加鹽這種方式是比較早期的處理方式了,既然如今沒有在大範圍使用了,就說明單純加鹽仍是存在缺陷的。
如今咱們在客戶端對密碼作了 md5 加鹽,服務器保存的也是加密後的內容,可是,鹽是寫在了客戶端的源代碼中,一旦對源代碼進行反編譯,找到 salt
這個字符串,那麼加鹽的作法也就形同虛設了。
反編譯源代碼的代價也很高,通常對於安全性能要求不高的話,也夠用了,可是,對於一些涉及資金之類的 App 來講,僅僅加鹽仍是不夠的。
好比離職的技術同窗不是很開心,又或者有人想花錢買這串字符等等,鹽一旦被泄露,就是一場災難,這也是鹽最大的缺陷。
鹽一旦被設定,那麼再作修改的話就很是困難了,由於服務器存儲的所有是加鹽後的數據,若是換鹽,那麼這些數據所有都須要改動。可是可怕的不在於此,若是將服務器的數據改動後,舊版本的用戶再訪問又都不能夠了,由於他們用的是以前的鹽。
目前最多見的方式,應該就是 HMAC 了,HMAC 算法主要應用於身份驗證,與加鹽的不一樣點在於,鹽被移到了服務器,服務器返回什麼,就用什麼做爲鹽。
這麼作有什麼好處呢? 若是咱們在登陸的過程當中,黑客截獲了咱們發送的數據,他也只能獲得 HMAC 加密事後的結果,因爲不知道密鑰,根本不可能獲取到用戶密碼,從而保證了安全性。
可是還有一個問題,前面咱們講到,鹽被獲取之後很危險,若是從服務器獲取鹽,也會被抓包,那還不如寫在源代碼裏面呢,至少被反編譯還困難點,那若是解決這個隱患呢?
那就是,在用戶註冊時就生成和獲取這個祕鑰,以代碼示例:
如今咱們發送一個請求:
GET http://localhost:8888/capsule/register.php?account=joy&password=789複製代碼
服務器收到請求後,作了下面的事情:
/*獲取 get 請求傳遞的參數*/
$account = $_GET['account'];
$password = $_GET['password']; //123456
/*建立數據鏈接*/
$db = new DataBase();
/*製做一個隨機的鹽*/
$salt = salt();
/*檢查用戶名是否存在*/
$is_exist = $db->check_user_exist($account);
if ($is_exist) {
echo response(10001, false);
}
else {
/*將密碼進行 hmac 加密*/
$password = str_hmac($password, $salt);
/*檢查用戶名是否添加成功*/
$result = $db->add_user($account, $password);
if ($result) {
$data = ['salt'=>$salt];
echo response(0, $data);
//echo response(0, true);
}
else {
echo response(20001, false);
}
}複製代碼
服務器如今保存的是:
account: joy
password: 05575c24576複製代碼
客戶端拿到的結果是:
{
"rc": 0,
"data": {
"salt": "5633905fdc65b6c57be8698b1f0e884948c05d7f"
},
"errorInfo": ""
}複製代碼
那麼客戶端接下來應該作什麼呢?把 salt
作本地的持久化,登陸時將用戶輸入的密碼作一次一樣的 hmac,那麼就能經過服務器的 password: 05575c24576
校驗了,發起登陸請求:
GET http://localhost:8888/capsule/login.php?account=joy&password=789
// fail
GET http://localhost:8888/capsule/login.php?account=joy&password=05575c24576
// success複製代碼
如今咱們解決了依賴性太強的問題,鹽咱們能夠隨意的更改,甚至能夠是隨機的,每一個用戶都不同。這樣單個用戶的安全性雖然沒有增強,可是整個平臺的安全性缺大大提高了,不多有人會針對一個用戶搞事情。可是細心的同窗應該能夠想到,如今的鹽,也就是祕鑰是保存在本地的,若是用戶的祕鑰丟失,好比換手機了,那麼豈不是有正確的密碼,也沒法登錄了嗎?
針對這個問題,核心就是用戶沒有了祕鑰,那麼在用戶登錄的時候,邏輯就須要變一下。
// 僞代碼
func login(account, password) {
//若是有鹽
if let salt = getSalt() {
//將密碼進行 hmac,請求登錄接口
network.login(account, password.hmac(salt))
}
else {
//請求 getSalt 接口,請求參數爲帳戶+應用標識
network.getSalt(account + bundleId, { salt in
//將鹽保存在本地,再次調用自身。
savaSalt(salt)
login(account, password)
})
}
}複製代碼
那麼可想而知,咱們的註冊接口如今也須要新加一個 bundleId
的請求參數,而後用 account + bundleId
做爲 key,來保存 salt
:
/*獲取 get 請求傳遞的參數*/
$account = $_GET['account'];
$password = $_GET['password']; //123456
$bundle_id = $_GET['bundleId'];
/*建立數據鏈接*/
$db = new DataBase();
/*製做一個隨機的鹽*/
$salt = salt();
/*檢查用戶名是否存在*/
$is_exist = $db->check_user_exist($account);
if ($is_exist) {
echo response(10001, false);
}
else {
/*將密碼進行 hmac 加密*/
$password = str_hmac($password, $salt);
/*檢查用戶名是否添加成功*/
$result = $db->add_user($account, $password);
if ($result) {
/*檢查祕鑰是否保存成功*/
$save_salt = $db->save_salt($salt, $account, $bundle_id);
if ($save_salt) {
$data = ['salt'=>$salt];
echo response(0, $data);
}
else {
echo response(20001, false);
}
}
else {
echo response(20001, false);
}
}複製代碼
同時咱們須要建立一個獲取 salt
的接口:
/*獲取 get 請求傳遞的參數*/
$account = $_GET['account'];
$bundle_id = $_GET['bundleId'];
/*建立數據鏈接*/
$db = new DataBase();
/*獲取祕鑰*/
$salt = $db->get_salt($account, $bundle_id);
if ($salt == '') {
echo response(40001, false);
}
else {
$data = ['salt'=>$salt];
echo response(0, $data);
}複製代碼
寫到這裏,就能夠給你們介紹一個比較好玩的東西了。
一些 App 具備設備鎖的功能,好比 QQ,這個功能是將帳號與設備進行綁定,若是其餘人知道了用戶的帳號和密碼,可是設備不符,一樣沒法登陸,怎樣實現呢?
就是用戶開啓設備鎖以後,若是設備中沒有 salt
,那麼就再也不請求 getSalt
接口,而是轉爲其餘驗證方式,經過以後,才能夠請求 getSalt
。
如今這個 App 相對來講比較安全了,上面說到,由於每一個用戶的 salt
都不同,破解單個用戶的利益不大,因此,對於平臺來講安全性已經比較高了,但凡是都有例外,若是這個破壞者就是鐵了心要搞事情,就針對一個用戶,如今這個方案,還有哪些問題存在呢?
salt
被抓包時有可能會泄露;salt
被抓包時有可能會泄露;salt
,有可能經過文件路徑獲取到;抓包的人就算不知道密碼,經過 hmac 加密後的字符,也能夠進行登陸;
怎麼處理呢?首先咱們須要清楚的是,之因此會被破解,是拿到了咱們加密時的因子,或者叫種子,這個種子服務器和客戶端都必需要有,若是沒有的話,二者就沒法進行通訊了,可是咱們也不能在客戶端將種子寫死,在服務器給客戶端種子時,總會有可能被獲取。
咱們要設計一種思路,須要有一個種子,服務器和客戶端之間無需通信,可是均可以被理解的種子。
同時咱們須要這個種子是動態的,每次加密的結果都不同,那麼就算抓到了加密後的密碼,這個密碼也隨之失效了。
因此,咱們須要一個無需服務器和客戶端通信的,動態的種子,時間。
這個動態的種子是如何使用的呢?
salt
,保存 hmac 後的密碼;salt
;客戶端代碼:
// 祕鑰
const salt = ''
// 當前時間,精確到分鐘
const currentTime = '201709171204'
// 用戶輸入的密碼
let password = '123456'
// (hmac+currentTime).md5
password = (password.hmac(salt) + currentTime).md5()
network('login', {method: 'GET', params: {password:password}})複製代碼
服務器代碼:
function should_login($account, $password) {
$account = mysqli_real_escape_string($this->connection ,$account);
$password = mysqli_real_escape_string($this->connection, $password);
$user = $this->get_user($account);
if ($user == null) {
return false;
}
$password_local = $user['password'];
if ($password_local == '') {
return false;
}
$password_local = md5($password_local.current_time());
if ($password_local == $password) {
return true;
}
else {
return false;
}
}複製代碼
可是如今還有一點問題,那就是對時間的容錯上,若是客戶端發送的時候是 201709171204
,服務器響應時卻已經到了 201709171205
了,那麼這樣勢必是不能經過的,這種狀況,只須要服務器把當前的時間減去一分鐘,再校驗一次,符合其中之一就能夠。
聰明的你應該能夠想到,這也就是驗證碼 5 分鐘內有效期的實現。
如今這個 App,就算註冊時拿到了 salt
,也很難在 1 分鐘內反推出密碼,同時,抓包的密碼一分鐘後也就失效了,對於單個用戶的安全性,也有了進一步的提高。