数据表
表 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();
}
后端公共代码
<?php
//生成指定长度的随机字符串
function rand_str(length)
{
//细节请自行实现
}
/**
* 密码对称加密函数
* @param string $password
* @param string $salt
* @param string $type //EN 加密、DE解密
* @return string
*/
function password_crypt($password, $salt, $type)
{
//细节请自行实现,算法请勿外泄
}
获得账号
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代码
<?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; //设置为立即过期,初始化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代码
<?php
$post_data = $_POST;
$req_timeout = 60; //设置请求超时时间
$token_timeout = 7200; //设置token失效时间
$token_invalid_timeout = 14400; //设置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 '请勿重复请求';
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 数据至客户端
接口身份认证
username
和password
通过获取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代码
<?php
$post_data = $_POST;
$req_timeout = 60; //设置请求超时时间
$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 '请勿重复请求';
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方式实现