简要:
我觉得我们项目中用的注册流程好麻烦啊,可能是我是第一次给APP端写接口,token设计,存储,处理以及传输 都有好多啊,下面我将分步介绍.......
注意点: 两个token access_token 1 天 refresh_token 30 天 ,存储到redis, 采用string类型
流程:
- 注册用户时,判断手机号格式是否正确
- 在没发送验证码之前, 判断手机号是否已经被注册过
- 密码和确认密码判断是否一致
- 新创建两个token
setex access_token 1天
setex refresh_token 30天
- 注册完成后, 将两个token返回给用户, 并且默认用户是登录的
设计格式:
- 生成的两个token组合方式:
$accessToken = md5('at' . $mobile . $userId .time());
$refreshToken = md5('rt' . $mobile . $userId . time());
- 将两个token和user_id之间创建联系,然后存储到redis上,以后可以通过token获取对应的user_id
user:at:{$access_token} => {$user_id}
user:rt:{$refresh_token} => {$user_id}
说明: 当生成好两个token后, 将他们与user_id建立起来,然后我将两个token返回给APP端.
注册:代码如下:
/**
* Action Register
* 用户注册
*/
public function actionRegister()
{
$params = Yii::$app->request->post();
$mAccount = new Account();
$util = Yii::$app->util;
//规格: 由8-16位的数字,字母组成的密码,其中不能有空格,不能全是数字,不能全是字母
$regular = "/((?=.*\d)(?=.*\D)|(?=.*[a-zA-Z])(?=.*[^a-zA-Z]))^.{8,16}$/";
if (!$params) {
return $util->formatResData(1101, '无参数提供', (object)[], false);
}
if (!isset($params['mobile']) || !$params['mobile']) {
return $util->formatResData(1102, '手机号不能为空', (object)[], false);
}
if (!$util->regularMobile($params['mobile'])) {
return $util->formatResData(1103, '手机号码格式不正确', (object)[], false);
}
if (!isset($params['vcode']) || !$params['vcode']) {
return yii::$app->util->formatResData(1104, '验证码不能为空', (object)[], false);
}
if (!isset($params['password']) || !$params['password']) {
return yii::$app->util->formatResData(1105, '密码不能为空', (object)[], false);
}
if (!isset($params['confirm_password']) || !$params['confirm_password']) {
return yii::$app->util->formatResData(1106, '确认密码不能为空', (object)[], false);
}
$users = $mAccount->isExistMobile($params['mobile'], 'user_id, salt, status');
$status = 0;
if ($users) {
$status = $users['status'];
//如果该用户存在,且状态为可用|禁用,都提示已注册
if (($status == Account::STATUS_ENABLE) || ($status == Account::STATUS_DISABLE)){
return $util->formatResData(1107, '该手机号已被注册', (object)[], false);
}
}
//TODO 判断验证码是否正确
$mobie_white_list = array('手机号', '手机号1');
if(in_array($params['mobile'], $mobie_white_list)) {
} else if (($result = $this->checkSmsVcode('reg', $params['mobile'], $params['vcode'])) !== true) {
return $result;
}
$mInviteCode = new Invitecode();
//判断邀请码是否存在
if (isset($params['code']) && $params['code']) {
$isBindMobile = $mInviteCode->isBindMobile($params['code'], $params['mobile']);
if (!$isBindMobile) {
return $util->formatResData(1108, '该手机号与邀请码未绑定', (object)[], false);
}
$data['is_receive_invokecode'] = HmcUser::STATUS_INVOKECODE_RECEIVED;
}
$tmp = $this->judgePassword($params['password'], $params['confirm_password'], $regular);
if ($tmp) {
return $tmp;
}
//从redis中获取用户ID
$data = $this->getUid();
if (!isset($data['user_id']) || !$data['user_id']) {
return $data;
}
$params['user_id'] = $data['user_id'];
unset($params['confirm_password'], $params['vcode'], $params['code']);
$mHmcUser = new HmcUser();
if (!$status) {
$result = $mAccount->add($params);
} else {
$params['user_id'] = $users['user_id'];
$updateVal['salt'] = $util->random(6);
$updateVal['password'] = md5(md5($params['password']).$updateVal['salt']);
$updateVal['status'] = Account::STATUS_ENABLE;
$result = $mAccount->renew($params['mobile'], $updateVal);
}
if ($result) {
$data['user_id'] = $params['user_id'];
$data['mobile'] = $params['mobile'];
if (!$status) {
$userDetail = $mHmcUser->add($data);
}
//创建token,并将token存储在redis
$handleData = $this->handleSession($params["user_id"], $params["mobile"]);
if (!isset($handleData['variable']) || !$handleData['variable']) {
return $handleData['error'];
}
$session = $handleData['variable'];
Yii::$app->api->account_import((string)$params["user_id"], (string)$params["user_id"], '');
try {
$smsData = ['mobile'=> $params['mobile'], 'business_type' => 'reg'];
$result = $this->renewSmsUseage($smsData);
} catch (\Exception $e) {
//TODO 将短信使用情况出现错误的地方写到日志里面
}
return $util->formatResData(0, '注册成功', $session, false);
} else {
return $util->formatResData(1201, '注册失败', (object)[], false);
}
}
刷新refresh_token步骤
流程如图所示:
代码如下:
/**
* Action RefreshToken
* 刷新refresh_token
*/
public function actionRefreshToken()
{
$params = Yii::$app->request->get();
if (!isset($params['refresh_token']) || !$params['refresh_token']) {
return yii::$app->util->formatResData(1101, '未传递refresh_token参数', (object)[], false);
}
Yii::info($params['refresh_token'], 'api\refreshtoken');
$cache = Yii::$app->cache->instance('base');
$rtCountKey = 'user:rtcount:' . $params['refresh_token'];
$unitMseconds = 500000;
$count = $cache->incr($rtCountKey);
$cache->expire($rtCountKey, 10);
if ($count > 1 && $count <= 10) {
// usleep($unitMseconds * $count);
usleep($unitMseconds);
}
try {
$tokenKey = "user:rt:" . $params['refresh_token'];
$tmp = $cache->get($tokenKey);
//判断refresh_token是否过期
if (!$tmp) {
$tokens = $cache->exists('user:oldrt:' . $params['refresh_token']);
if (!$tokens) {
return yii::$app->util->formatResData(1102, 'refresh_token过期', (object)[], false);
} else {
$tokensKey = 'user:oldrt:' . $params['refresh_token'];
$session['user_id'] = $cache->hget($tokensKey, 'user_id');
$session['access_token'] = $cache->hget($tokensKey, 'access_token');
$session['refresh_token'] = $cache->hget($tokensKey, 'refresh_token');
$session['sig'] = $cache->hget($tokensKey, 'sig');
}
} else {
//如果不过期,user:rt:refresh_token 获取对应的用户ID,然后先删除rt,再重新创建两个token
$userId = $tmp;
$cache->del($tokenKey);
$mAccount = new Account();
$users = $mAccount->getByUid($userId, 'mobile');
//重新创建两个token
$handleData = $this->handleSession($userId, $users['mobile']);
//创建旧的refresh_token,与新的refresh_token之间关联的数据,防止并发导致找不到已经删过的refresh_token, 10s
if (!isset($handleData['variable']) || !$handleData['variable']) {
return $handleData['error'];
}
$session = $handleData['variable'];
$accessToken = $session['access_token'];
$refreshToken = $session['refresh_token'];
$tmpTokenKey = "user:oldrt";
$oldRefreshToken = $params['refresh_token'];
$sig = Yii::$app->sig->getSig($userId);
$cache->hmset('user:oldrt:' . $oldRefreshToken, 'access_token', $accessToken, 'refresh_token', $refreshToken, 'user_id', $userId, 'sig', $sig);
$cache->expire('user:oldrt:' . $oldRefreshToken, 60);
}
return yii::$app->util->formatResData(0, 'refresh_token刷新成功', $session, false);
} catch (\Exception $e) {
return yii::$app->util->formatResData(-1, '服务器连接失败,请稍后重试', (object)[], false);
}
}
里面涉及的方法:
1: 创建token方法 handleSession
/**
* 创建token,并将token存储在redis上
*
* @param string $userId 用户ID
* @param string $mobile 手机号
* @param int $atTime access_token保存时间
* @param int $rtTime refresh_token保存时间
*
* @return array
* 说明: 返回的值: access_token,refresh_token,user_id,sig
*/
// protected function handleSession($userId, $mobile, $atTime = 300, $rtTime = 2592000)
// protected function handleSession($userId, $mobile, $atTime = 120, $rtTime = 2592000)
protected function handleSession($userId, $mobile, $atTime = 86400, $rtTime = 2592000)
{
$now = time();
$accessToken = md5('at' . $mobile . $userId . $now);
$refreshToken = md5('rt' . $mobile . $userId . $now);
$token = ['access_token' => $accessToken, 'refresh_token' => $refreshToken];
try {
// FIXED: 设置过期时间设置的为经过多少秒过去而不是时间点,如果是时间点应该单独使用expireat
$cache = Yii::$app->cache->instance('base');
$cache->setex('user:at:' . $accessToken, $atTime, $userId);
$cache->setex('user:rt:' . $refreshToken, $rtTime, $userId);
$sig = Yii::$app->sig->getSig($userId);
$result['variable'] = [
'access_token' => $accessToken,
'refresh_token' => $refreshToken,
'user_id' => $userId,
'sig' => $sig
];
} catch (\Exception $e) {
$result['error'] = yii::$app->util->formatResData(-100, '连接redis服务器失败,请稍后重试 ', (object)[], false);
}
return $result;
}
2:验证手机号格式方法: regularMobile()
/**
* 中国移动
*/
const CHINA_MOBILE_EXPR = '/^134[0-8]\d{7}$|^(?:13[5-9]|147|15[0-27-9]|178|18[2-478])\d{8}$/';
/**
* 中国联通
*/
const CHINA_UNION_EXPR = '/^(?:13[0-2]|145|15[56]|176|18[56])\d{8}$/';
/**
* 中国电信
*/
const CHINA_TELCOM_EXPR = '/^(?:133|153|177|18[019])\d{8}$/';
/**
* 手机正则表达式验证
*
* @param string $mobile 手机
* @return int false 表示验证失败 true表示成功
*/
public function regularMobile($mobile)
{
return preg_match(self::CHINA_MOBILE_EXPR, $mobile)
|| preg_match(self::CHINA_UNION_EXPR, $mobile)
|| preg_match(self::CHINA_TELCOM_EXPR, $mobile)
|| preg_match(self::CHINA_OTHER_EXPR, $mobile);
}
3: 判断密码和确认密码 judgePassword()
/**
* 判断密码和确认密码
*/
public function judgePassword($password, $confirmPwd, $regular)
{
if (!preg_match($regular, $password)) {
return yii::$app->util->formatResData(1401, '密码格式不正确', (object)[], false);
}
if (!preg_match($regular, $confirmPwd)) {
return yii::$app->util->formatResData(1402, '确认密码格式不正确', (object)[], false);
}
if ($password !== $confirmPwd) {
return yii::$app->util->formatResData(1403, '密码与确认密码不一致', (object)[], false);
}
}
* 为什么要创建两个token ?
- 假如你只设置一个token表示app端用户的登录状态, 那么如果你的token被别人采用非正当手段截取了, 他是不是就可以"为所欲为"了, 而我们设计两个token, access_token设置1天, refresh_token设置30天,
如果access_token过期了, refresh_token会刷新重新创建新的两个token, 并且把旧的refresh_token删掉. 由于access_token生存时间短暂, 那么假如被人盗取了, 没关系, 我们access_token过期后, 会用refresh_token刷新, 再重新创建新的两个token, 即便是refresh_token被盗取了, 也没关系, 因为又重新创建两个新的access_token和refresh_token, 你说这样是不是就比较完美了!
总结:
我只是把部分的流程列了出来,以后要是再碰到类似的,我就可以直接使用了,对于我来说,现在该学会的就是能够拿别人的代码应用到我的应用中,即便是我没明白,但是功能实现是第一步,因为工作领导不管你怎么样,人家要的是结果,因为现实的社会很残酷,不管你是谁,你是女生还是男生。
所以加油,我可以的,不能被别人看不起!