API用户体系设计和身份认证实践

数据表

表 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;
}

//生成指定长度的随机字符串
function randStr(length)
{
    //细节请自行实现
}

//获取当前时间戳
function time()
{
    return Math.floor(microtime()/100); 
}

//获取当前毫秒时间戳
function microtime()
{
    return (new Date()).getTime();
}

后端公共代码

获得账号

a. 服务商自己生成账号密码提供给请求方

b. 用户自行注册账号

客户端JS代码

import md5 from 'js-md5';

let username = 'test';   //用户输入的用户名
let password = '123456'; //用户输入的密码
password = md5(username + md5(password));

let post_data = {username, password};
//发送数据 post_data 至账号注册接口

后端接口PHP代码

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; //设置为立即过期,初始化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'
    );
    //数据 $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);
//发送数据 post_data 至获取token接口

后端接口PHP代码

 $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 '请勿重复请求';
        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 数据至客户端

接口身份认证

usernamepassword 通过获取token接口 获取到 token和 token_expire_time token_invalid_time
username token token_expire_time token_invalid_time 进行本地持久化保存,可以是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
    //更多请求请求参数根据实际业务添加
};

post_data['sign'] = md5(JSON.stringify(objectSortByKey(post_data)) + user_info.token);
//发送数据 post_data 至业务接口

后端接口PHP代码

 $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 '请勿重复请求';
        exit;
    }
    
    if($time > $user_info['token_invalid_time']){
        echo 'token无效';
        exit;
    }   
    
    //判断当前请求的接口是否为刷新token接口,开发时根据实际进行调整
    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
    ]);    
    
    //身份认证通过,继续处理实际业务逻辑

注意事项

  • 后端示例代码中Db类来自ThinkPHP5
  • 由于nonce表数据会越来越大请定期根据 nonce_expire_time 删除
  • 考虑到性能问题 nonce 可使用redis方式实现

你可能感兴趣的:(API用户体系设计和身份认证实践)