model = new Member(); } /** * 会员注册 * @param $data */ public function register($data) { //检测设置是否自动注册 //自动注册检测授权信息 //注册登录 } /** * 登录操作 * @param Member $member_info * @param string $login_type * @return array */ public function login(Member $member_info, string $login_type) { //绑定第三方授权 $this->bingOpenid($member_info); if (!$member_info->status) throw new ApiException('MEMBER_LOCK'); $member_info->login_time = time(); $member_info->login_ip = $this->request->ip(); $member_info->login_channel = $this->channel; $member_info->login_type = $login_type; $member_info->login_count++; $member_info->last_visit_time = time(); $member_info->save(); $token_info = $this->createToken($member_info); event("MemberLogin", $member_info); return [ 'token' => $token_info[ 'token' ], 'expires_time' => $token_info[ 'params' ][ 'exp' ], 'mobile' => $member_info->mobile ]; } /** * 账号登录 * @param string $username * @param string $password * @return array|false */ public function account(string $username, string $password) { $member_service = new MemberService(); $member_info = $member_service->findMemberInfo([ 'username|mobile' => $username ]); if ($member_info->isEmpty()) throw new AuthException('MEMBER_NOT_EXIST');//账号不存在 if (!check_password($password, $member_info->password)) return false;//密码与账号不匹配 return $this->login($member_info, MemberLoginTypeDict::USERNAME); } /** * 手机号登录 * @param array $params * @return array */ public function mobile($params) { //校验手机验证码 $this->checkMobileCode($params[ 'mobile' ]); //登录注册配置 $config = ( new MemberConfigService() )->getLoginConfig(); $is_mobile = $config[ 'is_mobile' ]; $is_bind_mobile = $config[ 'is_bind_mobile' ]; if ($is_mobile != 1 && $is_bind_mobile != 1) throw new AuthException('MOBILE_LOGIN_UNOPENED'); $member_service = new MemberService(); if (!empty($params['openid'])) { $mobile = $params[ 'mobile' ]; $openid = $params[ 'openid' ]; $openid_field = match ( $this->channel ) { 'wechat' => 'wx_openid', 'weapp' => 'weapp_openid', 'app' => 'wxapp_openid', default => '' }; $openid_member_info = $member_service->findMemberInfo([ $openid_field => $openid ]); $mobile_member_info = $member_service->findMemberInfo([ 'mobile' => $mobile ]); //openid 账号已存在 if (!$openid_member_info->isEmpty()) { // 手机号也存在 if (!$mobile_member_info->isEmpty()) { // 如果不是同一个人 抛异常 if ($openid_member_info['member_id'] != $mobile_member_info['member_id']) { throw new AuthException('MOBILE_IS_EXIST'); // 手机号已被其他账号绑定 } // 是同一个用户,直接登录 return $this->login($mobile_member_info, MemberLoginTypeDict::MOBILE); } // 手机号不存在,给 openid 账号绑定手机号 $openid_member_info->mobile = $mobile; $openid_member_info->save(); return $this->login($openid_member_info, MemberLoginTypeDict::MOBILE); } //openid 账号不存在 if (!$mobile_member_info->isEmpty()) { // 手机号已存在,不能注册,避免重复 throw new AuthException('MOBILE_IS_EXIST'); } //都不存在,执行注册 $data = [ 'mobile' => $mobile, 'nickname' => $params['nickname'] ?? '', 'headimg' => $params['headimg'] ?? '', $openid_field => $openid ]; return (new RegisterService())->register($mobile, $data, MemberRegisterTypeDict::MOBILE, false); } $member_info = $member_service->findMemberInfo([ 'mobile' => $params[ 'mobile' ] ]); if ($member_info->isEmpty()) { //开启强制绑定手机号,登录会自动注册并绑定手机号 if ($is_bind_mobile == 1) { $data = array( 'mobile' => $params[ 'mobile' ], 'nickname' => $params[ 'nickname' ], 'headimg' => $params[ 'headimg' ], 'wx_openid' => $params[ 'openid' ] ); return ( new RegisterService() )->register($params[ 'mobile' ], $data, MemberRegisterTypeDict::MOBILE, false); } else { throw new AuthException('MEMBER_NOT_EXIST');//账号不存在 } } return $this->login($member_info, MemberLoginTypeDict::MOBILE); } /** * 生成token * @param $member_info * @return array|null */ public function createToken($member_info) : ?array { $expire_time = env('system.api_token_expire_time') ?? 3600;//todo 不一定和管理端合用这个token时限 return TokenAuth::createToken($member_info->member_id, AppTypeDict::API, [ 'member_id' => $member_info->member_id, 'username' => $member_info->username ] , $expire_time); } /** * 登陆退出(当前账户) */ public function logout() : ?bool { self::clearToken($this->member_id, $this->request->apiToken()); return true; } /** * 清理token * @param int $member_id * @param string|null $token * @return bool|null */ public static function clearToken(int $member_id, ?string $token = '') : ?bool { TokenAuth::clearToken($member_id, AppTypeDict::API, $token); return true; } /** * 解析token * @param string|null $token * @return array */ public function parseToken(?string $token) { if (empty($token)) { //定义专属于授权认证机制的错误响应, 定义专属语言包 throw new AuthException('MUST_LOGIN', 401); } try { $token_info = TokenAuth::parseToken($token, AppTypeDict::API); } catch (Throwable $e) { // if(env('app_debug', false)){ // throw new AuthException($e->getMessage(), 401); // }else{ throw new AuthException('LOGIN_EXPIRE', 401); // } } if (!$token_info) { throw new AuthException('MUST_LOGIN', 401); } //验证有效次数或过期时间 return $token_info; } /** * 手机发送验证码 * @param $mobile * @param string $type 发送短信的业务场景 * @return array * @throws Exception */ public function sendMobileCode($mobile, string $type = '') { ( new CaptchaService() )->check(); if (empty($mobile)) throw new AuthException('MOBILE_NEEDED'); //发送 if (!in_array($type, SmsDict::SCENE_TYPE)) throw new AuthException('MEMBER_MOBILE_CAPTCHA_ERROR'); $code = str_pad(random_int(1, 9999), 4, 0, STR_PAD_LEFT);// 生成4位随机数,左侧补0 ( new NoticeService() )->send('member_verify_code', [ 'code' => $code, 'mobile' => $mobile ]); //将验证码存入缓存 $key = md5(uniqid('', true)); $cache_tag_name = "mobile_key" . $mobile . $type; $this->clearMobileCode($mobile, $type); Cache::tag($cache_tag_name)->set($key, [ 'mobile' => $mobile, 'code' => $code, 'type' => $type ], 600); return [ 'key' => $key ]; } public function getMobileCodeCacheName() { } public function clearMobileCode($mobile, $type) { $cache_tag_name = "mobile_key" . $mobile . $type; Cache::tag($cache_tag_name)->clear(); } /** * 校验手机验证码 * @param string $mobile * @return true */ public function checkMobileCode(string $mobile) { if (empty($mobile)) throw new AuthException('MOBILE_NEEDED'); $mobile_key = request()->param('mobile_key', ''); $mobile_code = request()->param('mobile_code', ''); if (empty($mobile_key) || empty($mobile_code)) throw new AuthException('MOBILE_CAPTCHA_ERROR'); $cache = Cache::get($mobile_key); if (empty($cache)) throw new AuthException('MOBILE_CAPTCHA_ERROR'); $temp_mobile = $cache[ 'mobile' ]; $temp_code = $cache[ 'code' ]; $temp_type = $cache[ 'type' ]; if ($temp_mobile != $mobile || $temp_code != $mobile_code) throw new AuthException('MOBILE_CAPTCHA_ERROR'); $this->clearMobileCode($temp_mobile, $temp_type); return true; } /** * 绑定openid * @param $member * @return true */ public function bingOpenid($member) { $open_id = $this->request->param('openid'); if (!empty($open_id)) { Log::write('channel_1' . $this->channel); if (!empty($this->channel)) { $openid_field = match ( $this->channel ) { 'wechat' => 'wx_openid', 'weapp' => 'weapp_openid', default => '' }; if (!empty($openid_field)) { if (!$member->isEmpty()) { if (empty($member->$openid_field)) { $member_service = new MemberService(); $open_member = $member_service->findMemberInfo([ $openid_field => $open_id ]); // 检测openid是否存在账号验证 if (empty($open_member)) { $member->$openid_field = $open_id; $member->save(); } } else { if ($member->$openid_field != $open_id) { throw new AuthException('MEMBER_IS_BIND_AUTH'); } } } } } } return true; } /** * 重置密码 * @param string $mobile * @param string $password * @return true */ public function resetPassword(string $mobile, string $password) { $member_service = new MemberService(); //校验手机验证码 $this->checkMobileCode($mobile); $member_info = $member_service->findMemberInfo([ 'mobile' => $mobile ]); if ($member_info->isEmpty()) throw new AuthException('MOBILE_NOT_EXIST_MEMBER');//账号不存在 //todo 需要考虑一下,新的密码和原密码一样能否通过验证 $password_hash = create_password($password); $data = array( 'password' => $password_hash, ); $member_service->editByFind($member_info, $data); self::clearToken($member_info[ 'member_id' ], $this->request->apiToken()); return true; } public function loginScanCode() { } public function loginByScanCode() { } public function checkScanCode() { } }