diff --git a/css/login.css b/css/login.css index 3232b9a7..66fdacf3 100644 --- a/css/login.css +++ b/css/login.css @@ -52,8 +52,8 @@ input::-webkit-input-placeholder { } -.login-button{ - margin-top: 5px; +.login_btn_group{ + margin-top: 5px; } .login-register{ @@ -98,6 +98,10 @@ input::-webkit-input-placeholder { z-index:800; } +#login_card{ + z-index:801; +} + #btn_kakao_login{ cursor: pointer; height:46px; diff --git a/f_install/sql/common_schema.sql b/f_install/sql/common_schema.sql index 7e09f2a8..973a5b6f 100644 --- a/f_install/sql/common_schema.sql +++ b/f_install/sql/common_schema.sql @@ -20,16 +20,18 @@ ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `member` ( `NO` INT(11) NOT NULL AUTO_INCREMENT, `oauth_id` BIGINT(20) NULL DEFAULT NULL, - `oauth_type` ENUM('NONE','KAKAO') NOT NULL, `ID` VARCHAR(64) NOT NULL, `EMAIL` VARCHAR(64) NULL DEFAULT NULL, + `oauth_type` ENUM('NONE','KAKAO') NOT NULL, + `oauth_info` TEXT NOT NULL DEFAULT '{}', + `token_valid_until` DATETIME NULL DEFAULT NULL, `PW` CHAR(128) NOT NULL, `salt` CHAR(16) NOT NULL, `third_use` INT(1) NOT NULL DEFAULT '0', `NAME` VARCHAR(64) NOT NULL, `PICTURE` VARCHAR(64) NULL DEFAULT 'default.jpg', `IMGSVR` INT(1) NULL DEFAULT '0', - `acl` text NOT NULL DEFAULT '{}' COMMENT 'json', + `acl` TEXT NOT NULL DEFAULT '{}' COMMENT 'json', `GRADE` INT(1) NULL DEFAULT '1', `REG_NUM` INT(3) NULL DEFAULT '0', `REG_DATE` DATETIME NOT NULL, diff --git a/hwe/sammo/WarUnitGeneral.php b/hwe/sammo/WarUnitGeneral.php index 89539731..841c2fb6 100644 --- a/hwe/sammo/WarUnitGeneral.php +++ b/hwe/sammo/WarUnitGeneral.php @@ -784,7 +784,7 @@ class WarUnitGeneral extends WarUnit{ foreach($table as $skillKey => $skillMultiply){ if($oppose->hasActivatedSkill($skillKey)){ $josaUl = \sammo\JosaUtil::pick($skillKey, '을'); - $thisLogger->pushGeneralBattleDetailLog("반계로 상대의 {$skillKey}{$josaUl} 되돌렸다!"); + $thisLogger->pushGeneralBattleDetailLog("반계로 상대의 {$skillKey}{$josaUl} 되돌렸다!"); $opposeLogger->pushGeneralBattleDetailLog("{$skillKey}{$josaUl} 역으로 당했다!"); $this->multiplyWarPowerMultiply($skillMultiply); diff --git a/i_entrance/j_get_user_info.php b/i_entrance/j_get_user_info.php index a3c291ac..2c1bb432 100644 --- a/i_entrance/j_get_user_info.php +++ b/i_entrance/j_get_user_info.php @@ -9,7 +9,7 @@ $userID = Session::getUserID(); // 외부 파라미터 $db = RootDB::db(); -$member = $db->queryFirstRow('SELECT `id`, `name`, `grade`, `picture`, reg_date, third_use, acl FROM `member` WHERE `NO` = %i', $userID); +$member = $db->queryFirstRow('SELECT `id`, `name`, `grade`, `picture`, reg_date, third_use, acl, oauth_type, token_valid_until FROM `member` WHERE `NO` = %i', $userID); if(!$member['picture']){ $picture = ServConfig::getSharedIconPath().'/default.jpg'; @@ -30,6 +30,8 @@ else{ } } +$tokenValidUntil = $member['token_valid_until']; + if($member['grade'] == 6) { $grade = '운영자'; } elseif($member['grade'] == 5) { @@ -81,5 +83,8 @@ Json::die([ 'global_salt'=>RootDB::getGlobalSalt(), 'join_date'=>$member['reg_date'], 'third_use'=>($member['third_use']!=0), - 'acl'=>$acl + 'acl'=>$acl, + 'oauth_type'=>$member['oauth_type'], + 'token_valid_until'=>$tokenValidUntil + ]); diff --git a/i_entrance/user_info.php b/i_entrance/user_info.php index fa046738..7203a283 100644 --- a/i_entrance/user_info.php +++ b/i_entrance/user_info.php @@ -23,6 +23,7 @@ require(__dir__.'/../vendor/autoload.php'); + @@ -87,6 +88,14 @@ require(__dir__.'/../vendor/autoload.php'); 개인정보 3자 제공 동의 : + + 인증 방식 + + + + + 까지 유효 + 회원 탈퇴 diff --git a/index.php b/index.php index 9b50cd88..0e5db2e4 100644 --- a/index.php +++ b/index.php @@ -64,8 +64,17 @@ function getOAuthToken(mode, scope_list){ function sendTempPasswordToKakaoTalk(){ $.post({ - url:'j_change_pw.php', + url:'oauth_kakao/j_login_oauth.php', dataType:'json' + }).then(function(obj){ + var t = $.Deferred(); + if(!obj.result){ + t.reject(); + } + return $.post({ + url:'oauth_kakao/j_change_pw.php', + dataType:'json' + }); }).then(function(obj){ if(!obj.result){ alert(obj.reason); @@ -81,12 +90,18 @@ function doLoginUsingOAuth(){ url:'oauth_kakao/j_login_oauth.php', dataType:'json' }).then(function(obj){ - if(!obj.result){ - alert(obj.reason); - } - else{ + if(obj.result){ window.location.href = "./"; + return; } + if(!obj.reqOTP){ + alert(obj.reason); + return; + } + $('#modalOTP').modal().on('shown.bs.modal', function(){ + $('#otp_code').focus(); + }); + }); } @@ -94,6 +109,9 @@ function postOAuthResult(result){ if(result == 'join'){ window.location.href = 'oauth_kakao/join.php'; } + else if(result == 'req_email'){ + alert('이메일 정보 공유를 허가해 주셔야 합니다.'); + } else if(result == 'login'){ console.log('로그인모드'); if(oauthMode=='change_pw'){ @@ -126,7 +144,7 @@ function postOAuthResult(result){

삼국지 모의전투 HiDCHe

-
+

로그인

@@ -152,9 +170,17 @@ function postOAuthResult(result){
-
+
- +
@@ -172,9 +198,42 @@ function postOAuthResult(result){
-
개인정보처리방침 & 이용약관
© 2018 • HideD
크롬과 파이어폭스에 최적화되어있습니다.
+ + + + diff --git a/j_login.php b/j_login.php index 3aeecc99..ac28aa51 100644 --- a/j_login.php +++ b/j_login.php @@ -2,7 +2,9 @@ namespace sammo; require(__dir__.'/vendor/autoload.php'); +require(__dir__.'/oauth_kakao/lib.join.php'); +use \kakao\Kakao_REST_API_Helper as Kakao_REST_API_Helper; $RootDB = RootDB::db(); $session = Session::getInstance(); @@ -16,12 +18,86 @@ $password = Util::getReq('password'); if(!$username || !$password){ Json::die([ 'result'=>false, + 'reqOTP'=>false, 'reason'=>'올바르지 않은 입력입니다.' ]); } +function kakaoOAuthCheck(array $userInfo) : ?array { + + if(!\kakao\KakaoKey::REST_KEY){ + return [false, '카카오 API 앱이 등록되지 않았습니다. 관리자에게 문의해 주세요.']; + } + + $oauthID = $userInfo['oauth_id']; + $oauthInfo = Json::decode($userInfo['oauth_info'])??[]; + if(!$oauthInfo){ + return [false, 'OAuth 정보가 보관되어 있지 않습니다. 카카오 로그인을 수행해 주세요.']; + } + + $accessToken = $oauthInfo['accessToken']??null; + $refreshToken = $oauthInfo['refreshToken']??null; + $accessTokenValidUntil = $oauthInfo['accessTokenValidUntil']??null; + $refreshTokenValidUntil = $oauthInfo['refreshTokenValidUntil']??null; + $OTPValue = $oauthInfo['OTPValue']??null; + $OTPTrialUntil = $oauthInfo['OTPTrialUntil']??null; + $tokenValidUntil = $member['token_valid_until']; + + if(!$accessToken || !$refreshToken || !$accessTokenValidUntil || !$refreshTokenValidUntil){ + return [false, 'OAuth 정보가 보관되어 있지 않습니다. 카카오 로그인을 수행해 주세요.']; + } + + $now = TimeUtil::DatetimeNow(); + + if($now > $refreshTokenValidUntil){ + return [false, '로그인 토큰이 만료되었습니다. 카카오 로그인을 수행해 주세요.']; + } + + if($now > $accessTokenValidUntil){ + $apiHelper = new Kakao_REST_API_Helper($accessToken); + $refreshResult = $apiHelper->refresh_access_token($refreshToken); + if(!$refreshResult){ + return [false, '로그인 토큰 자동 갱신을 실패했습니다. 카카오 로그인을 수행해 주세요.']; + } + + $accessToken = $refreshResult['access_token']; + $accessTokenValidUntil = TimeUtil::DatetimeFromNowSecond($refreshResult['expires_in']); + + $oauthInfo['accessToken'] = $accessToken; + $oauthInfo['accessTokenValidUntil'] = $accessTokenValidUntil; + + $refreshToken = $refreshResult['refresh_token']??null; + if($refreshToken){ + $refreshTokenValidUntil = TimeUtil::DatetimeFromNowSecond($refreshResult['refresh_token_expires_in']); + + $oauthInfo['refreshToken'] = $refreshToken; + $oauthInfo['refresh_token_expires_in'] = $refresh_token_expires_in; + } + + RootDB::db()->update('member', [ + 'oauth_info'=>Json::encode($oauthInfo) + ], 'no=%i', $userInfo['no']); + } + + if($tokenValidUntil && $now <= $tokenValidUntil){ + return null; + } + + //인증 시스템 가동 + $session->access_token = $accessToken; + $session->expires = $accessTokenValidUntil; + $session->refresh_token = $refreshToken; + $session->refresh_token_expires = $refreshTokenValidUntil; + + if(!createOTPbyUserNO($userInfo['no'])){ + return [false, '인증 코드를 보내는데 실패했습니다.']; + } + + return [true, '인증 코드를 입력해주세요']; +} + $userInfo = $RootDB->queryFirstRow( - 'SELECT `no`, `id`, `name`, `grade`, `delete_after`, `acl` '. + 'SELECT `no`, `id`, `name`, `grade`, `delete_after`, `acl`, oauth_id, oauth_type, oauth_info '. 'from member where id=%s_username AND '. 'pw=sha2(concat(salt, %s_password, salt), 512)',[ 'username'=>$username, @@ -31,6 +107,7 @@ $userInfo = $RootDB->queryFirstRow( if(!$userInfo){ Json::die([ 'result'=>false, + 'reqOTP'=>false, 'reason'=>'아이디나 비밀번호가 올바르지 않습니다.' ]); } @@ -49,12 +126,14 @@ if($userInfo['delete_after']){ $RootDB->delete('member', 'no=%i', $userInfo['no']); Json::die([ 'result'=>false, + 'reqOTP'=>false, 'reason'=>"기간 만기로 삭제되었습니다. 재 가입을 시도해주세요." ]); } else{ Json::die([ 'result'=>false, + 'reqOTP'=>false, 'reason'=>"삭제 요청된 계정입니다.[{$userInfo['delete_after']}]" ]); } @@ -70,10 +149,23 @@ $RootDB->insert('member_log',[ ]) ]); +if($userInfo['oauth_type'] == 'KAKAO'){ + $oauthFailResult = kakaoOAuthCheck($userInfo); + if($oauthFailResult !== null){ + $session->login($userInfo['no'], $userInfo['id'], $userInfo['grade'], true, Json::decode($userInfo['acl']??'{}')); + [$oauthReqOTP, $oauthFailReason] = $oauthFailResult; + Json::die([ + 'result'=>false, + 'reqOTP'=>$oauthReqOTP, + 'reason'=>$oauthFailReason + ]); + } +} -$session->login($userInfo['no'], $userInfo['id'], $userInfo['grade'], Json::decode($userInfo['acl']??'{}')); +$session->login($userInfo['no'], $userInfo['id'], $userInfo['grade'], false, Json::decode($userInfo['acl']??'{}')); Json::die([ 'result'=>true, + 'reqOTP'=>false, 'reason'=>'로그인 되었습니다.' ]); \ No newline at end of file diff --git a/js/join.js b/js/join.js index cd204ab6..acddf6b6 100644 --- a/js/join.js +++ b/js/join.js @@ -111,7 +111,7 @@ $(document).ready( function () { alert(obj.reason); } else{ - alert('정상적으로 가입되었습니다.'); + alert('회원 등록되었습니다.\n첫 로그인 과정에서 인증 코드를 입력하는 것으로 계정이 활성화됩니다.'); } window.location.href = "../"; diff --git a/js/login.js b/js/login.js index f5c7ed10..2e9e89b7 100644 --- a/js/login.js +++ b/js/login.js @@ -48,14 +48,44 @@ $(document).ready( function () { 'password':hash_pw } }).then(function(obj){ - if(!obj.result){ + if(obj.result){ + window.location.href = "./"; + return; + } + if(!obj.reqOTP){ alert(obj.reason); + return; } - else{ - window.location.href = 'i_entrance/entrance.php'; - } + $('#modalOTP').modal().on('shown.bs.modal', function(){ + $('#otp_code').focus(); + }); + }); return false; }); + + $('#otp_form').submit(function(){ + $.post({ + url:'oauth_kakao/j_check_OTP.php', + dataType:'json', + data:{ + 'otp':$('#otp_code').val(), + } + }).then(function(obj){ + if(obj.result){ + alert(obj.reason); + window.location.href = "./"; + return; + } + + alert(obj.reason); + + if(obj.reset){ + $('#modalOTP').modal('hide') + return; + } + }); + return false; + }) } ); \ No newline at end of file diff --git a/js/user_info.js b/js/user_info.js index 027473b5..8f31836d 100644 --- a/js/user_info.js +++ b/js/user_info.js @@ -20,6 +20,16 @@ function fillUserInfo(result){ if(result.third_use){ $('#third_use_disallow').show(); } + $('#slot_oauth_type').text(result.oauth_type); + if(result.oauth_type != 'NONE'){ + $('#slot_token_valid_until').text(result.token_valid_until); + } + else{ + $('#slot_token_valid_until').parent().html(''); + } + + + } function changeIconPreview(){ @@ -217,6 +227,37 @@ function deleteMe(){ }); } +function extendAuth(){ + var validUntil = $('#slot_token_valid_until').html(); + var availableAt = moment(validUntil).subtract(5, 'days').format('YYYY-MM-DD HH:mm:ss'); + var now = moment().format('YYYY-MM-DD HH:mm:ss'); + + if(now < availableAt){ + alert('{0}부터 초기화할 수 있습니다.'.format(availableAt)); + return false; + } + + if(!confirm('로그아웃됩니다. 진행할까요?')){ + return; + } + + $.ajax({ + type:'post', + url:'../oauth_kakao/j_reset_token.php', + dataType:'json', + }).then(function(result){ + if(!result.result){ + alert(result.reason); + } + else{ + alert('초기화했습니다. 다시 로그인해 주십시오.'); + location.href='../'; + } + },function(){ + alert('알 수 없는 이유로 로그인 토큰 연장에 실패했습니다.'); + }); +} + $(function(){ $('#slot_icon, #slot_new_icon').attr('src', pathConfig.sharedIcon+'/default.jpg'); $.ajax({ @@ -255,4 +296,6 @@ $(function(){ deleteMe(e); } }); + + $('#expand_login_token').click(extendAuth); }) diff --git a/oauth_kakao/index.php b/oauth_kakao/index.php index e75648cd..8914f658 100644 --- a/oauth_kakao/index.php +++ b/oauth_kakao/index.php @@ -64,6 +64,9 @@ function postOAuthResult(result){ if(result == 'join'){ window.location.href = 'join.php'; } + else if(result == 'req_email'){ + alert('이메일 정보 공유를 허가해 주셔야 합니다.'); + } else if(result == 'login'){ console.log('로그인모드'); if(oauthMode=='change_pw'){ diff --git a/oauth_kakao/j_change_pw.php b/oauth_kakao/j_change_pw.php index aa950fd8..37b23c3a 100644 --- a/oauth_kakao/j_change_pw.php +++ b/oauth_kakao/j_change_pw.php @@ -5,7 +5,7 @@ require(__dir__.'/../vendor/autoload.php'); use \kakao\Kakao_REST_API_Helper as Kakao_REST_API_Helper; -$nowDate = TimeUtil::DatetimeNow(); +$now = TimeUtil::DatetimeNow(); $session = Session::requireLogin([ 'reason'=>'로그인이 되어있지 않습니다' @@ -13,8 +13,6 @@ $session = Session::requireLogin([ $userID = Session::getUserID(); $access_token = $session->access_token; $expires = $session->expires; -$refresh_token = $session->refresh_token; -$refresh_token_expires = $session->refresh_token_expires; if(!$access_token || !$expires){ Json::die([ @@ -27,44 +25,37 @@ if(!$access_token || !$expires){ //TODO: join과 login의 동작이 비슷하다. helper class로 묶자. $restAPI = new Kakao_REST_API_Helper($access_token); -if($expires < $nowDate && (!$refresh_token || ($refresh_token_expires < $nowDate))){ +$session->logout(); + +if($expires < $now){ Json::die([ 'result'=>false, - 'reason'=>'로그인 토큰 만료.'.$refresh_token_expires.' 다시 카카오로그인을 수행해주세요.' + 'reason'=>'로그인 토큰 만료. 카카오 로그인을 먼저 수행해주세요.' ]); } -if($expires < $nowDate){ - $session->kaccount_email = null; - $email = null; - - $result = $restAPI->refresh_access_token($refresh_token); - if(!isset($refresh_token)){ - Json::die([ - 'result'=>false, - 'reason'=>'카카오 로그인 과정 중 추가 갱신 절차를 실패했습니다' - ]); - } - - $access_token = $result['access_token']; - $expires = TimeUtil::DatetimeFromNowSecond($result['expires_in']); - if(isset($result['refresh_token'])){ - $refresh_token = Util::array_get($result['refresh_token']); - $refresh_token_expires = TimeUtil::DatetimeFromNowSecond($result['refresh_token_expires_in']); - } -} - RootDB::db()->query("lock tables member write, member_log write"); -$isUser = RootDB::db()->queryFirstRow( - 'SELECT count(`no`) from member where no=%i',$userID); -if(!$isUser){ +$oauthInfo = Json::decode(RootDB::db()->queryFirstField('SELECT oauth_info from member where no=%i',$userID))??[]; +if(!$oauthInfo){ Json::die([ 'result'=>false, - 'reason'=>'회원이 아닙니다. 관리자에게 문의해주세요.' + 'reason'=>'제대로 로그인이 이루어져 있지 않습니다.' ]); } +$nextPasswordChange = $oauthInfo['nextPasswordChange']??null; +if($nextPasswordChange && $now < $nextPasswordChange){ + Json::die([ + 'result'=>false, + 'reason'=>'비밀번호를 초기화한지 얼마 지나지 않았습니다.' + ]); +} + +$nextPasswordChange = TimeUtil::DatetimeFromNowHour(4); +$oauthInfo['nextPasswordChange'] = $nextPasswordChange; + + $newPassword = Util::randomStr(6); $tmpPassword = Util::hashPassword(RootDB::getGlobalSalt(), $newPassword); $newSalt = bin2hex(random_bytes(8)); @@ -89,12 +80,13 @@ if($sendResult['code'] < 0){ RootDB::db()->update('member', [ 'pw'=>$newFinalPassword, - 'salt'=>$newSalt + 'salt'=>$newSalt, + 'oauth_info'=>Json::encode($oauthInfo), ],'no=%i', $userID); RootDB::db()->insert('member_log', [ 'member_no'=>$userID, - 'date'=>$nowDate, + 'date'=>$now, 'action_type'=>'change_pw', 'action'=>Json::encode([ 'type'=>'kakao', diff --git a/oauth_kakao/j_check_OTP.php b/oauth_kakao/j_check_OTP.php new file mode 100644 index 00000000..bf8b72dc --- /dev/null +++ b/oauth_kakao/j_check_OTP.php @@ -0,0 +1,92 @@ +isLoggedIn()){ + Json::die([ + 'result'=>false, + 'reset'=>true, + 'reason'=>'이미 로그인 되어있습니다.' + ]); +} + +if(!$session->isLoggedIn(true)){ + Json::die([ + 'result'=>false, + 'reset'=>true, + 'reason'=>'인증 코드를 입력할 수 있는 상태가 아닙니다.' + ]); +} + +$otp = Util::getReq('otp', 'int'); +if(!$otp){ + Json::die([ + 'result'=>false, + 'reset'=>false, + 'reason'=>'인증 코드가 입력되지 않았습니다.' + ]); +} + +$userNo = $session->getUserID(); + +$oauthInfo = Json::decode(RootDB::db()->queryFirstField('SELECT oauth_info FROM member WHERE no=%i', $userNo))??[]; +if(!$oauthInfo){ + $session->logout(); + Json::die([ + 'result'=>false, + 'reset'=>true, + 'reason'=>'계정이 정상적으로 등록되어있지 않습니다.' + ]); +} + + +$OTPValue = $oauthInfo['OTPValue']??null; +$OTPTrialUntil = $oauthInfo['OTPTrialUntil']??null; +$OTPTrialCount = $oauthInfo['OTPTrialCount']??0; +$now = TimeUtil::DatetimeNow(); + +if(!$OTPTrialUntil || $OTPTrialUntil <= $now){ + $session->logout(); + Json::die([ + 'result'=>false, + 'reset'=>true, + 'reason'=>'인증 기한이 만료되었습니다. 다시 로그인해주세요.' + ]); +} + +if($OTPTrialCount <= 0){ + Json::die([ + 'result'=>false, + 'reset'=>false, + 'reason'=>"인증 실패 횟수를 초과했습니다. {$OTPTrialUntil}까지 기다려주세요." + ]); +} + +if($OTPValue != $otp){ + $OTPTrialCount -= 1; + $oauthInfo['OTPTrialCount'] = $OTPTrialCount; + RootDB::db()->update('member', [ + 'oauth_info'=>Json::encode($oauthInfo) + ], 'no=%i', $userNo); + Json::die([ + 'result'=>false, + 'reset'=>false, + 'reason'=>"인증 번호가 틀렸습니다. {$OTPTrialCount}회 더 시도할 수 있습니다." + ]); +} + +$tokenValidUntil = TimeUtil::DatetimeFromNowDay(10); +$session->setReqOTP(false); +RootDB::db()->update('member', [ + 'oauth_info'=>Json::encode($oauthInfo), + 'token_valid_until'=>$tokenValidUntil, +], 'no=%i', $userNo); + +Json::die([ + 'result'=>true, + 'reset'=>false, + 'reason'=>"로그인을 성공했습니다. {$tokenValidUntil}까지 유효합니다." +]); \ No newline at end of file diff --git a/oauth_kakao/j_join_process.php b/oauth_kakao/j_join_process.php index da51e1e7..daeb7522 100644 --- a/oauth_kakao/j_join_process.php +++ b/oauth_kakao/j_join_process.php @@ -125,23 +125,35 @@ if(!$kakaoID && Util::array_get($signupResult['msg'])!='already registered'){ $me = $restAPI->meWithEmail(); $me['code'] = Util::array_get($me['code'], 0); +$kakao_account = $me['kakao_account']??[]; if ($me['code']< 0) { $restAPI->unlink(); Json::die([ 'result'=>false, - 'reason'=>'카카오로그인이 정상적으로 수행되지 않았습니다.' + 'reason'=>'카카오로그인이 정상적으로 수행되지 않았습니다.', ]); } -if(!Util::array_get($me['kaccount_email_verified'],false)){ +if(!($kakao_account['has_email']??false)){ + Json::die([ + 'result'=>false, + 'reqOTP'=>false, + 'reason'=>'이메일 정보 공유를 허가해 주셔야 합니다.', + ]); +} + +$validEmail = $kakao_account['is_email_valid']??false; +$verifiedEmail = $kakao_account['is_email_verified']??false; + +if(!$validEmail || !$verifiedEmail){ $restAPI->unlink(); Json::die([ 'result'=>false, - 'reason'=>'카카오 계정 이메일이 아직 인증되지 않았습니다' + 'reason'=>'카카오 계정 이메일이 아직 인증되지 않았습니다', ]); } -$email = $me['kaccount_email']; +$email = $kakao_account['email']??null; $session->kaccount_email = $email; $emailChk = checkEmailDup($email); if($emailChk !== true){ @@ -162,7 +174,13 @@ RootDB::db()->insert('member',[ 'pw' => $finalPassword, 'salt' => $userSalt, 'name'=>$nickname, - 'reg_date'=>$nowDate + 'reg_date'=>$nowDate, + 'oauth_info'=>Json::encode([ + 'accessToken'=>$access_token, + 'refreshToken'=>$refresh_token, + 'accessTokenValidUntil'=>$expires, + 'refreshTokenValidUntil'=>$refresh_token_expires + ]) ]); $userID = RootDB::db()->insertId(); diff --git a/oauth_kakao/j_login_oauth.php b/oauth_kakao/j_login_oauth.php index 343c6f6b..1eb6601d 100644 --- a/oauth_kakao/j_login_oauth.php +++ b/oauth_kakao/j_login_oauth.php @@ -2,7 +2,7 @@ namespace sammo; require(__dir__.'/../vendor/autoload.php'); - +require('lib.join.php'); use \kakao\Kakao_REST_API_Helper as Kakao_REST_API_Helper; $RootDB = RootDB::db(); @@ -15,12 +15,13 @@ $canLogin = RootDB::db()->queryFirstField('SELECT `LOGIN` FROM `system` WHERE `N if($canLogin != 'Y'){ Json::die([ 'result'=>false, + 'reqOTP'=>false, 'reason'=>'현재는 로그인이 금지되어있습니다!', 'noRetry'=>true ]); } -$nowDate = TimeUtil::DatetimeNow(); +$now = TimeUtil::DatetimeNow(); $access_token = $session->access_token; $expires = $session->expires; @@ -32,6 +33,7 @@ $email = $session->kaccount_email; if(!$access_token || !$expires){ Json::die([ 'result'=>false, + 'reqOTP'=>false, 'reason'=>'카카오로그인이 이루어지지 않았습니다.' ]); } @@ -39,14 +41,15 @@ if(!$access_token || !$expires){ //TODO: join과 login의 동작이 비슷하다. helper class로 묶자. $restAPI = new Kakao_REST_API_Helper($access_token); -if($expires < $nowDate && (!$refresh_token || ($refresh_token_expires < $nowDate))){ +if($expires < $now && (!$refresh_token || ($refresh_token_expires < $now))){ Json::die([ 'result'=>false, + 'reqOTP'=>false, 'reason'=>'로그인 토큰 만료.'.$refresh_token_expires.' 다시 카카오로그인을 수행해주세요.' ]); } -if($expires < $nowDate){ +if($expires < $now){ $session->kaccount_email = null; $email = null; @@ -54,6 +57,7 @@ if($expires < $nowDate){ if(!isset($refresh_token)){ Json::die([ 'result'=>false, + 'reqOTP'=>false, 'reason'=>'카카오 로그인 과정 중 추가 갱신 절차를 실패했습니다' ]); } @@ -73,43 +77,68 @@ if(!$email){ if ($me['code']< 0) { Json::die([ 'result'=>false, + 'reqOTP'=>false, 'reason'=>'카카오로그인이 정상적으로 수행되지 않았습니다.' ]); } - if(!Util::array_get($me['kaccount_email_verified'],false)){ + $kakao_account = $me['kakao_account']??null; + if (!$kakao_account) { + Json::die([ + 'result'=>false, + 'reqOTP'=>false, + 'reason'=>'카카오 로그인 정보를 제대로 받아오지 못했습니다.' + ]); + } + + if(!($kakao_account['has_email']??false)){ + Json::die([ + 'result'=>false, + 'reqOTP'=>false, + 'reason'=>'이메일 정보 공유를 허가해 주셔야 합니다.', + ]); + } + + $validEmail = $kakao_account['is_email_valid']??false; + $verifiedEmail = $kakao_account['is_email_verified']??false; + + if(!$validEmail || !$verifiedEmail){ $restAPI->unlink(); Json::die([ 'result'=>false, - 'reason'=>'카카오 계정 이메일이 아직 인증되지 않았습니다' + 'reqOTP'=>false, + 'reason'=>'카카오 계정 이메일이 아직 인증되지 않았습니다', ]); } - $email = $me['kaccount_email']; + $email = $kakao_account['email']; $session->kaccount_email = $email; } $userInfo = $RootDB->queryFirstRow( - 'SELECT `no`, `id`, `name`, `grade`, `delete_after`, `acl` from member where email=%s',$email); + 'SELECT `no`, `id`, `name`, `grade`, `delete_after`, `acl`, oauth_info, token_valid_until from member where email=%s',$email); if(!$userInfo){ $restAPI->unlink(); $session->access_token = null; Json::die([ 'result'=>false, - 'reason'=>'카카오로그인에 해당하는 계정이 없습니다. 재 가입을 시도해주세요.' + 'reqOTP'=>false, + 'reason'=>'카카오로그인에 해당하는 계정이 없습니다. 재 가입을 시도해주세요.', + 'aux'=>$session->tmpx, ]); } if($userInfo['delete_after']){ - if($userInfo['delete_after'] < $nowDate){ + if($userInfo['delete_after'] < $now){ $restAPI->unlink(); $session->access_token = null; $RootDB->delete('member', 'no=%i', $userInfo['no']); Json::die([ 'result'=>false, - 'reason'=>"기간 만기로 삭제되었습니다. 재 가입을 시도해주세요." + 'reqOTP'=>false, + 'reason'=>"기간 만기로 삭제되었습니다. 재 가입을 시도해주세요.", ]); } else{ @@ -117,12 +146,43 @@ if($userInfo['delete_after']){ $session->access_token = null; Json::die([ 'result'=>false, + 'reqOTP'=>false, 'reason'=>"삭제 요청된 계정입니다.[{$userInfo['delete_after']}]" ]); } } +$oauthInfo = Json::decode($userInfo['oauth_info'])??[]; +$oauthInfo['accessToken'] = $access_token; +$oauthInfo['refreshToken'] = $refresh_token; +$oauthInfo['accessTokenValidUntil'] = $expires; +$oauthInfo['refreshTokenValidUntil'] = $refresh_token_expires; + +RootDB::db()->update('member', [ + 'oauth_info'=>Json::encode($oauthInfo) +], 'no=%i', $userInfo['no']); + + +$tokenValidUntil = $userInfo['token_valid_until']; + +if(!$tokenValidUntil || $tokenValidUntil < $now){ + if(!createOTPbyUserNO($userInfo['no'])){ + Json::die([ + 'result'=>false, + 'reqOTP'=>false, + 'reason'=>'인증 코드를 보내는데 실패했습니다.' + ]); + } + $session->login($userInfo['no'], $userInfo['id'], $userInfo['grade'], true, Json::decode($userInfo['acl']??'{}')); + Json::die([ + 'result'=>false, + 'reqOTP'=>true, + 'reason'=>'인증 코드를 입력해주세요.' + ]); +} + + $RootDB->insert('member_log',[ 'member_no'=>$userInfo['no'], 'action_type'=>'login', @@ -132,8 +192,9 @@ $RootDB->insert('member_log',[ ]) ]); -$session->login($userInfo['no'], $userInfo['id'], $userInfo['grade'], Json::decode($userInfo['acl']??'{}')); +$session->login($userInfo['no'], $userInfo['id'], $userInfo['grade'], false, Json::decode($userInfo['acl']??'{}')); Json::die([ 'result'=>true, + 'reqOTP'=>false, 'reason'=>'로그인 되었습니다.' ]); \ No newline at end of file diff --git a/oauth_kakao/j_reset_token.php b/oauth_kakao/j_reset_token.php new file mode 100644 index 00000000..92f6c143 --- /dev/null +++ b/oauth_kakao/j_reset_token.php @@ -0,0 +1,44 @@ +'로그인이 되어있지 않습니다' +]); + + +$userID = Session::getUserID(); + +$tokenValidUntil = RootDB::db()->queryFirstField('SELECT token_valid_until from member where no=%i',$userID); + +if(!$tokenValidUntil){ + Json::die([ + 'result'=>false, + 'reason'=>'초기화 가능한 로그인 상태가 아닙니다.' + ]); +} + +$now = TimeUtil::DatetimeNow(); +$expectedDate = TimeUtil::DatetimeFromNowDay(5); + +if($expectedDate <= $tokenValidUntil){ + Json::die([ + 'result'=>false, + 'reason'=>'아직 연장 가능하지 않습니다.' + ]); +} + +unset($oauthInfo['tokenValidUntil']); +RootDB::db()->update('member', [ + 'token_valid_until'=> null +], 'no=%i', $userID); + +$session->logout(); + +Json::die([ + 'result'=>true, + 'reason'=>'초기화 완료.' +]); diff --git a/oauth_kakao/lib.join.php b/oauth_kakao/lib.join.php index da840493..d0ac4e82 100644 --- a/oauth_kakao/lib.join.php +++ b/oauth_kakao/lib.join.php @@ -1,5 +1,6 @@ delete('member', 'no=%i', $userInfo['no']); } return true; -} \ No newline at end of file +} + +function createOTPbyUserNO(int $userNo):bool{ + $userInfo = RootDB::db()->queryFirstRow('SELECT oauth_info FROM member WHERE no=%i', $userNo); + if(!$userInfo){ + return false; + } + + $oauthInfo = Json::decode($userInfo['oauth_info']); + if(!$oauthInfo){ + return false; + } + + $accessToken = $oauthInfo['accessToken']; + $OTPValue = $oauthInfo['OTPValue']??null; + $OTPTrialUntil = $oauthInfo['OTPTrialUntil']??null; + + $now = TimeUtil::DatetimeNow(); + + + if($OTPTrialUntil && $OTPValue && $OTPTrialUntil > $now){ + return true; + } + + [$OTPValue, $OTPTrialUntil] = createOTP($accessToken); + + if(!$OTPValue){ + return false; + } + + $oauthInfo['OTPValue'] = $OTPValue; + $oauthInfo['OTPTrialUntil'] = $OTPTrialUntil; + $oauthInfo['OTPTrialCount'] = 3; + + RootDB::db()->update('member', [ + 'oauth_info'=>Json::encode($oauthInfo) + ], 'no=%i', $userNo); + + return true; +} + +function createOTP(string $accessToken):array{ + $restAPI = new Kakao_REST_API_Helper($accessToken); + + $OTPValue = Util::randRangeInt(10000, 99999); + $OTPTrialUntil = TimeUtil::DatetimeFromNowSecond(180); + + $sendResult = $restAPI->talk_to_me_default([ + "object_type"=> "text", + "text"=> "인증 코드는 $OTPValue 입니다. $OTPTrialUntil 이내에 입력해주세요.", + "link"=> [ + "web_url"=> ServConfig::getServerBasepath(), + "mobile_web_url" => ServConfig::getServerBasepath() + ], + "button_title"=> "로그인 페이지 열기" + ]); + $sendResult['code'] = Util::array_get($sendResult['code'], 0); + if($sendResult['code'] < 0){ + return null; + } + + return [$OTPValue, $OTPTrialUntil]; +} + diff --git a/oauth_kakao/oauth.php b/oauth_kakao/oauth.php index 4a9017e8..c39f2f4c 100644 --- a/oauth_kakao/oauth.php +++ b/oauth_kakao/oauth.php @@ -47,7 +47,15 @@ $me = $restAPI->meWithEmail(); $oauth_mode = 'login'; $me['code'] = Util::array_get($me['code'], 0); -if($me['code']< 0){ +$signed = $me['has_signed_up']??false; +$kakao_account = $me['kakao_account']??[]; +if(!($kakao_account['has_email']??false)||!($kakao_account['email']??false)){ + $oauth_mode = 'req_email'; +} +else if(!$signed){ + $oauth_mode = 'join'; +} +else if($me['code']< 0){ switch(Util::array_get($me['msg'])){ case 'NotRegisteredUserException': $oauth_mode = 'join'; @@ -57,7 +65,7 @@ if($me['code']< 0){ } } else{ - $session->kaccount_email = $me['kaccount_email']; + $session->kaccount_email = $kakao_account['email']; } ?> diff --git a/oauth_kakao/oauth_fail.html b/oauth_kakao/oauth_fail.html index 6580afb6..46a19df0 100644 --- a/oauth_kakao/oauth_fail.html +++ b/oauth_kakao/oauth_fail.html @@ -1,7 +1,7 @@ - > + OAuth 로그인 실패 diff --git a/src/kakao/Kakao_REST_API_Helper.php b/src/kakao/Kakao_REST_API_Helper.php index 7e568cc9..882d8168 100644 --- a/src/kakao/Kakao_REST_API_Helper.php +++ b/src/kakao/Kakao_REST_API_Helper.php @@ -25,7 +25,7 @@ class User_Management_Path public static $SIGNUP = "/v1/user/signup"; public static $UNLINK = "/v1/user/unlink"; public static $LOGOUT = "/v1/user/logout"; - public static $ME = "/v1/user/me"; + public static $ME = "/v2/user/me"; public static $UPDATE_PROFILE = "/v1/user/update_profile"; public static $USER_IDS = "/v1/user/ids"; } @@ -97,12 +97,12 @@ class Kakao_REST_API_Helper $requestUrl .= '?'.$params; } - $opts = array( - CURLOPT_URL => $requestUrl, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_SSL_VERIFYPEER => false, - CURLOPT_SSLVERSION => 1, - ); + $opts = [ + CURLOPT_URL => $requestUrl, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1, + ]; if ($api_path != '/oauth/token') { if (in_array($api_path, self::$admin_apis)) { @@ -213,8 +213,12 @@ class Kakao_REST_API_Helper public function meWithEmail() { $params = [ - 'propertyKeys'=>'["id","kaacount_email","kaccount_email_verified"]' - ]; + 'property_keys'=>'['. + '"id",'. + '"kakao_account.has_email","kakao_account.email",'. + '"kakao_account.is_email_valid","kakao_account.is_email_verified"'. + ']' + ]; return $this->request(User_Management_Path::$ME); } diff --git a/src/sammo/Session.php b/src/sammo/Session.php index c25c5a34..392c0308 100644 --- a/src/sammo/Session.php +++ b/src/sammo/Session.php @@ -8,12 +8,14 @@ namespace sammo; * @property string $userName 유저명 * @property int $userGrade 유저등급 * @property string $ip IP + * @property bool $reqOTP 인증 코드 필요 * @property array $acl 권한 */ class Session { const PROTECTED_NAMES = [ 'ip'=>true, + 'reqOTP'=>true, 'time'=>true, 'userID'=>true, 'userName'=>true, @@ -163,7 +165,7 @@ class Session return Util::array_get($_SESSION[$name]); } - public function login(int $userID, string $userName, int $grade, array $acl): Session + public function login(int $userID, string $userName, int $grade, bool $reqOTP, array $acl): Session { $this->set('userID', $userID); $this->set('userName', $userName); @@ -171,10 +173,14 @@ class Session $this->set('time', time()); $this->set('userGrade', $grade); $this->set('acl', $acl); - $this->set('access_token', null); + $this->set('reqOTP', $reqOTP); return $this; } + public function setReqOTP(bool $reqOTP=false){ + $this->set('reqOTP', $reqOTP); + } + public function logout(): Session { @@ -189,6 +195,7 @@ class Session $this->set('userName', null); $this->set('userGrade', null); $this->set('acl', null); + $this->set('reqOTP', null); $this->set('time', time()); return $this; } @@ -324,9 +331,12 @@ class Session return $obj->userID; } - public function isLoggedIn(): bool + public function isLoggedIn(bool $ignoreOTP = false): bool { - if ($this->userID) { + if (!$ignoreOTP && $this->reqOTP){ + return false; + } + else if ($this->userID) { return true; } else { return false;