使用Token來做身份認證在目前的移動客戶端上非常流行重罪,Token這個概念來源于OAuth認證樱哼,主要是在服務(wù)端實現(xiàn)。關(guān)于相關(guān)的原理剿配,同學(xué)們自行百度搅幅。在這里,我簡單介紹一下我是怎么具體實現(xiàn)的呼胚,重點描述token生成茄唐、token識別及token緩存。
注:SpringBoot版本請點擊這里蝇更,商用代碼比較復(fù)雜沪编,但是核心思想與本文一樣
生成Token
服務(wù)端接收客戶端傳遞的username和password等請求,在數(shù)據(jù)庫中檢查年扩,如果用戶名密碼匹配的話蚁廓,表示登錄成功,服務(wù)端生成并返回一個token訪問令牌常遂。
public function login()
{
$data = array_merge($this->request->post(), []);
// login with password
$user = Db::name($this->table)->field($this->field_except, true)->where('name', $data['name'])->find();
if ($user) {
if ($user['pwd'] === EncryptService::password($data['pwd'])) {
$this->onLoginSuccess($user);
} else {
parent::logic_failure('密碼不正確', 'user_wrong_pwd');
}
} else {
parent::logic_failure('用戶不存在', 'user_not_exists');
}
}
private function onLoginSuccess($user)
{
unset($user['pwd']);
$token = Token::generateToken($this->request, $user);
$ret = [
'token' => $token['token'],
'user' => $user
];
$this->logic_success(null, $ret);
}
在上面的代碼中纳令,登錄成功,服務(wù)端向客戶端返回token和user信息克胳。token是通過Token類的靜態(tài)方法generateToken
來生成的平绩。
public static function generateToken($request, $user)
{
// 1,生成包含用戶及設(shè)備信息的新token
$data = [
'uuid' => $request->param('uuid', ''),
'uid' => $user['id'],
'ip' => $request->ip(),
'client' => $request->param('client', ''),
'update_time' => ApiBase::_timestamp(),
];
// json序列化
$json = json_encode($data);
// 加密token信息
$key = md5(EncryptService::encrypt($json));
$data['token'] = $key;
// 2漠另,從數(shù)據(jù)庫查詢用戶的token捏雌,決定是update還是insert
$token = Token::get($user['id']);
// 如果數(shù)據(jù)庫中已存在token,更新
if ($token) {
// 刪除舊的緩存
cache($token['token'], null);
// 執(zhí)行更新
$token->isUpdate(true)->save($data);
}
// 不存在笆搓,插入
else {
$token = new Token();
$data['create_time'] = ApiBase::_timestamp();
$token->data($data)->save();
}
// 3性湿,將token寫入緩存
Token::cacheToken($token->getData());
return $token->getData();
}
這里主要有3個步驟
- 根據(jù)用戶及設(shè)備等信息生成一個唯一的token纬傲,uid用于識別用戶,uuid用于識別設(shè)備肤频,time用于保證唯一叹括。將這些信息序列化后加密并生成md5
- 根據(jù)用戶id查詢用戶登錄表,不管用戶原來是否已經(jīng)存在token宵荒,新的token將替換舊的token
- 將token寫入緩存汁雷,因為token是每個請求都會解析,如果不使用緩存的話报咳,會導(dǎo)致數(shù)據(jù)庫訪問瓶頸侠讯。
客戶端應(yīng)該保存token,然后在訪問一些需要身份認證的API暑刃,比如修改昵稱厢漩,就需要帶上這個token了,而不需要顯示帶上用戶信息岩臣,比如user_id溜嗜。
解析token
服務(wù)端接收客戶端傳遞的token,需要從中解析出相關(guān)的用戶及設(shè)備信息婿脸。
public static function getToken($key)
{
if (!isset($key)) {
return null;
}
$token = cache($key);
if ($token) {
return $token;
}
else {
return Token::findByToken($key);
}
}
過程很簡單粱胜,先從緩存中取,如果不存在再從數(shù)據(jù)庫中查詢狐树。所有的請求,服務(wù)端都會執(zhí)行解析token鸿脓。
驗證token
服務(wù)端簡單地把API分為三類
- 公共API抑钟,客戶端可以任意訪問,不需要驗證身份野哭,此類API多以GET請求為多在塔。比如查詢產(chǎn)品列表
- 受保護的API,也即需要強制認證的API拨黔,這類API需要驗證客戶端身份才能訪問蛔溃,比如修改頭像,客戶端未傳token或token無效篱蝇,服務(wù)端返回一個錯誤碼贺待。
- 可選認證的API,介于1和2之間零截,不要求強制驗證客戶端身份麸塞,比如分享獎勵,如果客戶端傳遞了token并認證通過了涧衙,則給相應(yīng)的獎勵哪工,否則不做任何的獎勵奥此。
基于上面的分析,驗證token附帶一個是否強制驗證的參數(shù)雁比,用于中斷稚虎。驗證的規(guī)則也相對簡單,只要用戶請求的設(shè)備ID與token中的設(shè)備ID相同就算驗證通過了偎捎。
public function checkToken($exit = true)
{
if ($this->token) {
if ($this->token['uuid'] === $this->request->param('uuid', '')) {
return true;
} else {
if ($exit) {
$this->logic_failure(null, 'user_offline');
} else {
$this->token = null;
}
return false;
}
} else {
if ($exit) {
$this->logic_failure(null, 'token_invalid');
}
return false;
}
}
緩存
緩存相對簡單蠢终,緩存的key為加密token之后的md5值,也即登錄成功后鸭限,服務(wù)端向客戶端發(fā)送的token值蜕径。緩存未設(shè)置有效期,默認永不過期败京。
public static function cacheToken($token)
{
if ($token) {
if (is_object($token)) {
$token = $token->getData();
}
cache($token['token'], ($token));
}
}
?>
進階
為了讓您的應(yīng)該更加安全兜喻,還可以在以下方面強化
- token有效期
- token緩存加密
- token高級校驗
不過,我這里已經(jīng)對token做了對稱加密赡麦,相信他人偽造token的可能性也不大朴皆。