API用戶體系設(shè)計和身份認(rèn)證實踐

數(shù)據(jù)表

表 user

id(主鍵) username(唯一) password(唯一) salt token(唯一) token_expire_time token_invalid_time
1 username1 password1 OAu8d4 token1 1558021467 1,558,028,667
2 username2 password2 85Dfg2 token2 1558021468 1,558,028,668

表 nonce
user_id + nonce 唯一

id(主鍵) user_id nonce nonce_expire_time
1 1 nonce1 1558021467
2 2 nonce2 1558021468

前端公共代碼

//對象按鍵名排序
function objectSortByKey(object)
{
    let keys = Object.keys(object);
    let len = keys.length;
    let i, k;
    let new_object = {};
    keys.sort();
    for (i = 0; i < len; i++) {
        k = keys[i];
        new_object[k] = object[k];
    }
    return new_object;
}

//生成指定長度的隨機(jī)字符串
function randStr(length)
{
    //細(xì)節(jié)請自行實現(xiàn)
}

//獲取當(dāng)前時間戳
function time()
{
    return Math.floor(microtime()/100); 
}

//獲取當(dāng)前毫秒時間戳
function microtime()
{
    return (new Date()).getTime();
}

后端公共代碼

<?php
    //生成指定長度的隨機(jī)字符串    
    function rand_str(length)
    {
        //細(xì)節(jié)請自行實現(xiàn)
    }
    
    /**
     * 密碼對稱加密函數(shù)
     * @param  string  $password
     * @param  string  $salt     
     * @param  string  $type //EN 加密、DE解密
     * @return string
     */
    function password_crypt($password, $salt, $type)
    {
        //細(xì)節(jié)請自行實現(xiàn)翠胰,算法請勿外泄
    }

獲得賬號

a. 服務(wù)商自己生成賬號密碼提供給請求方

b. 用戶自行注冊賬號

客戶端JS代碼

import md5 from 'js-md5';

let username = 'test';   //用戶輸入的用戶名
let password = '123456'; //用戶輸入的密碼
password = md5(username + md5(password));

let post_data = {username, password};
//發(fā)送數(shù)據(jù) post_data 至賬號注冊接口

后端接口PHP代碼

<?php
    function password_encrypt($password)
    {
        $salt = rand_str(6);
        $password = password_crypt($password, $salt, 'EN');
        if(Db::table('user')->where('password', $password)->count() > 0){
            return password_encrypt($password);
        }
        return compact('password', 'salt');
    }

    $username = $_POST['username'];
    $password = $_POST['password'];
    $time = time();
    $token = md5(md5($username) . $time . rand_str(6);//初始化token
    $token_expire_time = 0; //設(shè)置為立即過期们童,初始化token不需被使用
    $token_invalid_time = 0;
    if(Db::table('user')->where('username', $username)->count() > 0){
        echo '用戶名已存在';
        exit;
    }
    
    $encrypt_res = password_encrypt($password); 
    extract($encrypt_res);
        
    $user_data = compact(
        'username', 
        'password', 
        'salt',
        'token',
        'token_expire_time',
        'token_invalid_time'
    );
    //數(shù)據(jù) $user_data 寫入user表

獲取token信息

客戶端JS代碼

import md5 from 'js-md5';

let username = 'test';   //用戶輸入的用戶名
let password = '123456'; //用戶輸入的密碼
let nonce = md5(microtime() + randStr(6)); 
let timestamp = time() ; 
let sign_key = md5(username + md5(password));

let post_data = {
    "username" : username,
    "nonce" : nonce,
    "timestamp" : timestamp
};

post_data['sign'] = md5(JSON.stringify(objectSortByKey(post_data)) + sign_key);
//發(fā)送數(shù)據(jù) post_data 至獲取token接口

后端接口PHP代碼

<?php
    $post_data = $_POST;
    $req_timeout = 60; //設(shè)置請求超時時間
    $token_timeout = 7200; //設(shè)置token失效時間
    $token_invalid_timeout = 14400; //設(shè)置token作廢時間
    // token作廢后無法刷新token;
    $time = time();
    if($post_data['timestamp'] - $time > $req_timeout){
        echo '請求超時';
        exit;
    }
    
    $user_info = Db::table('user')->where('username', $post_data['username'])->find();
    if(empty($user_info){
        echo '賬號或密碼錯誤';
        exit;
    }
    
    $replay = Db::table('nonce')
    ->where('user_id', $user_info['id'])
    ->where('nonce', $post_data['nonce'])
    ->count();
    if($replay > 0){
        echo '請勿重復(fù)請求';
        exit;
    }
    
    $password = password_crypt($user_info['password'], $user_info['salt'], 'DE');
    
    $post_sign = $post_data['sign'];
    unset($post_data['sign']);
    ksort($post_data);
    $sign = md5(json_encode($post_data).$password);
    if($post_sign !== $sign){
        echo '非法請求';
        exit;
    }
    
    Db::table('nonce')->insert([
        'user_id' => $user_info['id'],
        'nonce' => $post_data['nonce'],
        'nonce_expire_time' => $time + $req_timeout*2
    ]);
    
    $token =md5(md5($post_data['username']) . $time . rand_str(6));
    $token_expire_time = $token_timeout  + $time;
    $token_invalid_time = $token_invalid_timeout  + $time;
    
    $token_data = compact('token', 'token_expire_time', 'token_invalid_time');
    Db::table('user')->where('id', $user_info['id'])->update($token_data);
    
    //輸出 $token_data 數(shù)據(jù)至客戶端

接口身份認(rèn)證

usernamepassword 通過獲取token接口 獲取到 token和 token_expire_time token_invalid_time
username token token_expire_time token_invalid_time 進(jìn)行本地持久化保存,可以是cookie或 LocalStorage 的方式

客戶端JS代碼

import md5 from 'js-md5';

let user_info = localStorage.getItem('user_info');
user_info = JSON.parse(user_info);

if(time() > user_info.token_invalid_time){
    alert('登陸超時');
    return ;
}
if(time() > user_info.token_expire_time){
    //走刷新token流程
    user_info = localStorage.getItem('user_info');
    user_info = JSON.parse(user_info);
}

let nonce = md5(microtime() + randStr(6)); 
let timestamp = time() ; 

let post_data = {
    "username" : user_info.username,
    "nonce" : nonce,
    "timestamp" : timestamp
    //更多請求請求參數(shù)根據(jù)實際業(yè)務(wù)添加
};

post_data['sign'] = md5(JSON.stringify(objectSortByKey(post_data)) + user_info.token);
//發(fā)送數(shù)據(jù) post_data 至業(yè)務(wù)接口

后端接口PHP代碼

<?php
    $post_data = $_POST;
    $req_timeout = 60; //設(shè)置請求超時時間
    $time = time();
    if($post_data['timestamp'] - $time > $req_timeout){
        echo '請求超時';
        exit;
    }

    $user_info = Db::table('user')->where('username', $post_data['username'])->find();
    if(empty($user_info){
        echo '賬號不存在';
        exit;
    } 

    $replay = Db::table('nonce')
    ->where('user_id', $user_info['id'])
    ->where('nonce', $post_data['nonce'])
    ->count();
    if($replay > 0){
        echo '請勿重復(fù)請求';
        exit;
    }
    
    if($time > $user_info['token_invalid_time']){
        echo 'token無效';
        exit;
    }   
    
    //判斷當(dāng)前請求的接口是否為刷新token接口剩檀,開發(fā)時根據(jù)實際進(jìn)行調(diào)整
    if(strtolower($_SERVER['REQUEST_URI'] ) !== strtolower('/api/freshToken')){
        if($time > $user_info['token_expire_time']){
            echo 'token過期';
            exit;
        }   
    }
    
    $post_sign = $post_data['sign'];
    unset($post_data['sign']);
    ksort($post_data);
    $sign = md5(json_encode($post_data).$user_info['token']);
    if($post_sign !== $sign){
        echo '非法請求';
        exit;
    }

    Db::table('nonce')->insert([
        'user_id' => $user_info['id'],
        'nonce' => $post_data['nonce'],
        'nonce_expire_time' => $time + $req_timeout*2
    ]);    
    
    //身份認(rèn)證通過,繼續(xù)處理實際業(yè)務(wù)邏輯

注意事項

  • 后端示例代碼中Db類來自ThinkPHP5
  • 由于nonce表數(shù)據(jù)會越來越大請定期根據(jù) nonce_expire_time 刪除
  • 考慮到性能問題 nonce 可使用redis方式實現(xiàn)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末旺芽,一起剝皮案震驚了整個濱河市沪猴,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌采章,老刑警劉巖运嗜,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異悯舟,居然都是意外死亡担租,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進(jìn)店門抵怎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來奋救,“玉大人,你說我怎么就攤上這事反惕〕⑺遥” “怎么了?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵承璃,是天一觀的道長利耍。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么隘梨? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任程癌,我火速辦了婚禮,結(jié)果婚禮上轴猎,老公的妹妹穿的比我還像新娘嵌莉。我一直安慰自己,他們只是感情好捻脖,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布锐峭。 她就那樣靜靜地躺著,像睡著了一般可婶。 火紅的嫁衣襯著肌膚如雪沿癞。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天矛渴,我揣著相機(jī)與錄音椎扬,去河邊找鬼。 笑死具温,一個胖子當(dāng)著我的面吹牛蚕涤,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播铣猩,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼揖铜,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了达皿?” 一聲冷哼從身側(cè)響起天吓,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎鳞绕,沒想到半個月后失仁,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體尸曼,經(jīng)...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡们何,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了控轿。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片冤竹。...
    茶點(diǎn)故事閱讀 39,841評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖茬射,靈堂內(nèi)的尸體忽然破棺而出鹦蠕,到底是詐尸還是另有隱情,我是刑警寧澤在抛,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布钟病,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏肠阱。R本人自食惡果不足惜票唆,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望屹徘。 院中可真熱鬧走趋,春花似錦、人聲如沸噪伊。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鉴吹。三九已至姨伟,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間豆励,已是汗流浹背授滓。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留肆糕,地道東北人般堆。 一個月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像诚啃,于是被迫代替她去往敵國和親淮摔。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評論 2 354

推薦閱讀更多精彩內(nèi)容