數(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)證
username
和password
通過獲取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)