login을 API로 이동

This commit is contained in:
2021-11-08 02:10:01 +09:00
parent f45e0c9c4b
commit fccc973c8b
11 changed files with 436 additions and 221 deletions
+25
View File
@@ -151,3 +151,28 @@ function logExceptionByCustomHandler(\Throwable $e)
throw $e;
}
set_exception_handler('\\sammo\\logExceptionByCustomHandler');
function getAPIExecutorClass($path){
static $basePath = __NAMESPACE__.'\\API\\';
if(is_string($path)){
}
else if(is_array($path)){
$path = join('\\', $path);
}
else{
throw new \InvalidArgumentException("{$path}는 올바른 API 지시자가 아님");
}
$classPath = str_replace('/', '\\', $basePath.$path);
if(class_exists($classPath)){
return $classPath;
}
throw new \InvalidArgumentException("{$path}는 올바른 API 경로가 아님");
}
function buildAPIExecutorClass($type, string $rootPath, array $args):\sammo\BaseAPI{
$class = getAPIExecutorClass($type);
return new $class($rootPath, $args);
}
+17 -2
View File
@@ -58,7 +58,7 @@ CREATE TABLE `member_log` (
INDEX `action` (`member_no`, `action_type`, `date`),
INDEX `member` (`member_no`, `date`)
)
ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
ENGINE=Aria DEFAULT CHARSET=utf8mb4;
###################
# KV storage
@@ -72,4 +72,19 @@ CREATE TABLE if not exists `storage` (
UNIQUE INDEX `key` (`namespace`, `key`)
)
COLLATE='utf8mb4_general_ci'
ENGINE=MyISAM
ENGINE=Aria;
CREATE TABLE `login_token` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
`base_token` INT(11) NOT NULL,
`reg_ip` VARCHAR(40) NOT NULL COLLATE 'utf8mb4_general_ci',
`reg_date` DATETIME NOT NULL,
`expire_date` DATETIME NOT NULL,
PRIMARY KEY (`id`),
UNIQUE INDEX `by_token` (`user_id`, `base_token`),
INDEX `by_date` (`user_id`, `expire_date`)
)
COLLATE='utf8mb4_general_ci'
ENGINE=Aria
;
-25
View File
@@ -357,31 +357,6 @@ function buildNationCommandClass(?string $type, General $generalObj, array $env,
return new $class($generalObj, $env, $lastTurn, $arg);
}
function getAPIExecutorClass($path){
static $basePath = __NAMESPACE__.'\\API\\';
if(is_string($path)){
}
else if(is_array($path)){
$path = join('\\', $path);
}
else{
throw new \InvalidArgumentException("{$path}는 올바른 API 지시자가 아님");
}
$classPath = str_replace('/', '\\', $basePath.$path);
if(class_exists($classPath)){
return $classPath;
}
throw new \InvalidArgumentException("{$path}는 올바른 API 경로가 아님");
}
function buildAPIExecutorClass($type, string $rootPath, array $args):\sammo\BaseAPI{
$class = getAPIExecutorClass($type);
return new $class($rootPath, $args);
}
function getWarUnitTriggerClass(string $type){
static $basePath = __NAMESPACE__.'\\WarUnitTrigger\\';
$classPath = ($basePath.$type);
+11 -8
View File
@@ -1,7 +1,7 @@
import $ from 'jquery';
import 'popper.js';
import Popper from 'popper.js';
(window as unknown as {Popper:unknown}).Popper = Popper;
(window as unknown as { Popper: unknown }).Popper = Popper;
import 'bootstrap';
import { JQValidateForm, NamedRules } from '../util/jqValidateForm';
import axios from 'axios';
@@ -194,13 +194,16 @@ $(function ($) {
try {
const response = await axios({
url: 'j_login.php',
url: 'api.php',
responseType: 'json',
method: 'post',
data: convertFormData({
username: values.username,
password: hash_pw,
})
data: {
path: ["Login", "LoginByID"],
args: {
username: values.username,
password: hash_pw,
}
}
});
result = response.data;
}
@@ -268,9 +271,9 @@ $(function ($) {
getOAuthToken('change_pw', 'talk_message');
});
$('#btn_kakao_login').on('click', function(e){
$('#btn_kakao_login').on('click', function (e) {
e.preventDefault();
getOAuthToken('login', ['account_email','talk_message']);
getOAuthToken('login', ['account_email', 'talk_message']);
})
-181
View File
@@ -1,181 +0,0 @@
<?php
namespace sammo;
require(__DIR__ . '/vendor/autoload.php');
require(__DIR__ . '/oauth_kakao/lib.join.php');
WebUtil::requireAJAX();
use \kakao\Kakao_REST_API_Helper as Kakao_REST_API_Helper;
$RootDB = RootDB::db();
$session = Session::getInstance();
if ($session->isLoggedIn()) {
$session->logout();
}
$username = mb_strtolower(Util::getPost('username'), 'utf-8');
$password = Util::getPost('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 = $userInfo['token_valid_until'];
if (!$accessToken || !$refreshToken || !$accessTokenValidUntil || !$refreshTokenValidUntil) {
return [false, 'OAuth 정보가 보관되어 있지 않습니다. 카카오 로그인을 수행해 주세요.'];
}
$now = TimeUtil::now();
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'] ?? null;
if (!$accessToken) {
trigger_error("refreshToken 에러 " . Json::encode($refreshResult) . "," . $refreshToken . "," . substr(\kakao\KakaoKey::REST_KEY, 0, 6), E_USER_NOTICE);
return [false, '로그인 토큰 자동 갱신을 실패했습니다. 카카오 로그인을 수행해 주세요.'];
}
$accessTokenValidUntil = TimeUtil::nowAddSeconds($refreshResult['expires_in']);
$oauthInfo['accessToken'] = $accessToken;
$oauthInfo['accessTokenValidUntil'] = $accessTokenValidUntil;
$refreshToken = $refreshResult['refresh_token'] ?? null;
if ($refreshToken) {
$refreshTokenValidUntil = TimeUtil::nowAddSeconds($refreshResult['refresh_token_expires_in']);
$oauthInfo['refreshToken'] = $refreshToken;
$oauthInfo['refresh_token_expires_in'] = $refreshTokenValidUntil;
}
RootDB::db()->update('member', [
'oauth_info' => Json::encode($oauthInfo)
], 'no=%i', $userInfo['no']);
}
if ($tokenValidUntil && $now <= $tokenValidUntil) {
return null;
}
//인증 시스템 가동
$session = Session::getInstance();
$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`, oauth_id, oauth_type, oauth_info, token_valid_until ' .
'from member where id=%s_username AND ' .
'pw=sha2(concat(salt, %s_password, salt), 512)',
[
'username' => $username,
'password' => $password
]
);
if (!$userInfo) {
Json::die([
'result' => false,
'reqOTP' => false,
'reason' => '아이디나 비밀번호가 올바르지 않습니다.'
]);
}
$canLogin = $RootDB->queryFirstField('SELECT `LOGIN` FROM `system` WHERE `NO` = 1');
if ($canLogin != 'Y' && $userInfo['grade'] < 5) {
Json::die([
'result' => false,
'reason' => '현재는 로그인이 금지되어있습니다!'
]);
}
$nowDate = TimeUtil::now();
if ($userInfo['delete_after']) {
if ($userInfo['delete_after'] < $nowDate) {
$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']}]"
]);
}
}
$RootDB->insert('member_log', [
'member_no' => $userInfo['no'],
'action_type' => 'login',
'action' => Json::encode([
'ip' => Util::get_client_ip(true),
'type' => 'plain'
])
]);
if ($userInfo['oauth_type'] == 'KAKAO') {
$oauthFailResult = kakaoOAuthCheck($userInfo);
if ($oauthFailResult !== null) {
$session->login($userInfo['no'], $userInfo['id'], $userInfo['grade'], true, $userInfo['token_valid_until'], Json::decode($userInfo['acl'] ?? '{}'));
[$oauthReqOTP, $oauthFailReason] = $oauthFailResult;
Json::die([
'result' => false,
'reqOTP' => $oauthReqOTP,
'reason' => $oauthFailReason
]);
}
}
$session->login($userInfo['no'], $userInfo['id'], $userInfo['grade'], false, $userInfo['token_valid_until'], Json::decode($userInfo['acl'] ?? '{}'));
Json::die([
'result' => true,
'reqOTP' => false,
'reason' => '로그인 되었습니다.'
]);
+6 -3
View File
@@ -4,6 +4,9 @@ namespace sammo;
require(__DIR__.'/../vendor/autoload.php');
require('lib.join.php');
//TODO: KakaoOauth를 API로 정식으로 이동
WebUtil::requireAJAX();
use \kakao\Kakao_REST_API_Helper as Kakao_REST_API_Helper;
@@ -112,7 +115,7 @@ if(!$email){
'reason'=>'카카오 계정 이메일이 아직 인증되지 않았습니다',
]);
}
$email = $kakao_account['email'];
$session->kaccount_email = $email;
}
@@ -152,7 +155,7 @@ if($userInfo['delete_after']){
'reason'=>"삭제 요청된 계정입니다.[{$userInfo['delete_after']}]"
]);
}
}
$oauthInfo = Json::decode($userInfo['oauth_info'])??[];
@@ -176,7 +179,7 @@ if(!$tokenValidUntil || $tokenValidUntil < $now){
'result'=>false,
'reqOTP'=>false,
'reason'=>'인증 코드를 보내는데 실패했습니다.'
]);
]);
}
$session->login($userInfo['no'], $userInfo['id'], $userInfo['grade'], true, $userInfo['token_valid_until'], Json::decode($userInfo['acl']??'{}'));
Json::die([
+125
View File
@@ -0,0 +1,125 @@
<?php
namespace sammo\API\Login;
use sammo\Session;
use DateTimeInterface;
use sammo\BaseAPI;
use sammo\Json;
use sammo\KakaoUtil;
use sammo\RootDB;
use sammo\TimeUtil;
use sammo\Util;
use sammo\Validator;
class LoginByID extends \sammo\BaseAPI
{
public function validateArgs(): ?string
{
$v = new Validator($this->args);
$v
->rule('required', [
'username',
'password'
]);
if (!$v->validate()) {
return $v->errorStr();
}
return null;
}
public function getRequiredSessionMode(): int
{
return BaseAPI::NO_SESSION;
}
public function launch(Session $session, ?DateTimeInterface $modifiedSince, ?string $reqEtag)
{
$RootDB = RootDB::db();
if ($session->isLoggedIn()) {
$session->logout();
}
$username = $this->args['username'];
$password = $this->args['password'];
$userInfo = $RootDB->queryFirstRow(
'SELECT `no`, `id`, `name`, `grade`, `delete_after`, `acl`, oauth_id, oauth_type, oauth_info, token_valid_until ' .
'from member where id=%s_username AND ' .
'pw=sha2(concat(salt, %s_password, salt), 512)',
[
'username' => $username,
'password' => $password
]
);
if (!$userInfo) {
return '아이디나 비밀번호가 올바르지 않습니다.';
}
$canLogin = $RootDB->queryFirstField('SELECT `LOGIN` FROM `system` WHERE `NO` = 1');
if ($canLogin != 'Y' && $userInfo['grade'] < 5) {
return '현재는 로그인이 금지되어있습니다!';
}
$nowDate = TimeUtil::now();
if ($userInfo['delete_after']) {
if ($userInfo['delete_after'] < $nowDate) {
$RootDB->delete('member', 'no=%i', $userInfo['no']);
return [
'result' => false,
'reqOTP' => false,
'reason' => "기간 만기로 삭제되었습니다. 재 가입을 시도해주세요."
];
} else {
return [
'result' => false,
'reqOTP' => false,
'reason' => "삭제 요청된 계정입니다.[{$userInfo['delete_after']}]"
];
}
}
$RootDB->insert('member_log', [
'member_no' => $userInfo['no'],
'action_type' => 'login',
'action' => Json::encode([
'ip' => Util::get_client_ip(true),
'type' => 'plain'
])
]);
$RootDB->insert('member_log', [
'member_no' => $userInfo['no'],
'action_type' => 'login',
'action' => Json::encode([
'ip' => Util::get_client_ip(true),
'type' => 'plain'
])
]);
if ($userInfo['oauth_type'] == 'KAKAO') {
$oauthFailResult = KakaoUtil::kakaoOAuthCheck($userInfo);
if ($oauthFailResult !== null) {
$session->login($userInfo['no'], $userInfo['id'], $userInfo['grade'], true, $userInfo['token_valid_until'], Json::decode($userInfo['acl'] ?? '{}'));
[$oauthReqOTP, $oauthFailReason] = $oauthFailResult;
return [
'result' => false,
'reqOTP' => $oauthReqOTP,
'reason' => $oauthFailReason
];
}
}
$session->login($userInfo['no'], $userInfo['id'], $userInfo['grade'], false, $userInfo['token_valid_until'], Json::decode($userInfo['acl'] ?? '{}'));
return [
'result' => true,
'reqOTP' => false,
'reason' => '로그인 되었습니다.'
];
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace sammo\API\Login;
use sammo\Session;
use DateTimeInterface;
use sammo\BaseAPI;
use sammo\Json;
use sammo\KakaoUtil;
use sammo\RootDB;
use sammo\TimeUtil;
use sammo\Util;
use sammo\Validator;
class LoginByToken extends \sammo\BaseAPI{
public function getRequiredSessionMode(): int {
return \sammo\BaseAPI::NO_SESSION;
}
public function validateArgs(): ?string {
return null;
}
public function launch(Session $session, ?DateTimeInterface $modifiedSince, ?string $reqEtag) {
return '미구현';
}
}
+2 -2
View File
@@ -40,7 +40,7 @@ class APIHelper
$sessionMode = $obj->getRequiredSessionMode();
if ($sessionMode === BaseAPI::NO_SESSION) {
$session = null;
$session = Session::getInstance();//XXX: NoSession이면 진짜 NoSession이어야..?
} else {
if ($sessionMode & BaseAPI::REQ_GAME_LOGIN) {
$session = Session::requireGameLogin();
@@ -94,7 +94,7 @@ class APIHelper
], $cacheResult === null ? Json::NO_CACHE : 0);
}
Json::die($result, $cacheResult === null ? Json::NO_CACHE : 0);
} catch (\Exception $e) {
} catch (\Throwable $e) {
Json::dieWithReason($e->getMessage());
} catch (mixed $e) {
Json::dieWithReason(strval($e));
+221
View File
@@ -0,0 +1,221 @@
<?php
namespace sammo;
use kakao\Kakao_REST_API_Helper;
class KakaoUtil
{
private function __construct()
{
}
static function checkUsernameDup($username)
{
if (!$username) {
return '계정명을 입력해주세요';
}
$username = mb_strtolower($username, 'utf-8');
$length = strlen($username);
if ($length < 4 || $length > 64) {
return '적절하지 않은 길이입니다.';
}
$cnt = RootDB::db()->queryFirstField('SELECT count(no) FROM member WHERE `id` = %s LIMIT 1', $username);
if ($cnt != 0) {
return '이미 사용중인 계정명입니다';
}
return true;
}
static function checkNicknameDup($nickname)
{
if (!$nickname) {
return '닉네임을 입력해주세요';
}
$length = mb_strwidth($nickname, 'utf-8');
if ($length < 1 || $length > 18) {
return '적절하지 않은 길이입니다.';
}
$cnt = RootDB::db()->queryFirstField('SELECT count(no) FROM member WHERE `name` = %s LIMIT 1', $nickname);
if ($cnt != 0) {
return '이미 사용중인 닉네임입니다';
}
return true;
}
static function checkEmailDup($email)
{
if (!$email) {
return '이메일을 입력해주세요';
}
$length = strlen($email);
if ($length < 1 || $length > 64) {
return '적절하지 않은 길이입니다.';
}
$userInfo = RootDB::db()->queryFirstField('SELECT `no`, `delete_after` FROM member WHERE `email` = %s LIMIT 1', $email);
if ($userInfo) {
if (!$userInfo['delete_after']) {
return '이미 사용중인 이메일입니다. 관리자에게 문의해주세요.';
}
if ($userInfo['delete_after'] >= $userInfo) {
return "삭제 요청된 계정입니다.[{$userInfo['delete_after']}]";
}
//$userInfo['delete_after'] < $userInfo
RootDB::db()->delete('member', 'no=%i', $userInfo['no']);
}
return true;
}
static 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::now();
if ($OTPTrialUntil && $OTPValue && $OTPTrialUntil > $now) {
return true;
}
[$OTPValue, $OTPTrialUntil] = static::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;
}
static function createOTP(string $accessToken): ?array
{
$restAPI = new Kakao_REST_API_Helper($accessToken);
$OTPValue = Util::randRangeInt(1000, 9999);
$OTPTrialUntil = TimeUtil::nowAddSeconds(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];
}
static 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 = $userInfo['token_valid_until'];
if (!$accessToken || !$refreshToken || !$accessTokenValidUntil || !$refreshTokenValidUntil) {
return [false, 'OAuth 정보가 보관되어 있지 않습니다. 카카오 로그인을 수행해 주세요.'];
}
$now = TimeUtil::now();
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'] ?? null;
if (!$accessToken) {
trigger_error("refreshToken 에러 " . Json::encode($refreshResult) . "," . $refreshToken . "," . substr(\kakao\KakaoKey::REST_KEY, 0, 6), E_USER_NOTICE);
return [false, '로그인 토큰 자동 갱신을 실패했습니다. 카카오 로그인을 수행해 주세요.'];
}
$accessTokenValidUntil = TimeUtil::nowAddSeconds($refreshResult['expires_in']);
$oauthInfo['accessToken'] = $accessToken;
$oauthInfo['accessTokenValidUntil'] = $accessTokenValidUntil;
$refreshToken = $refreshResult['refresh_token'] ?? null;
if ($refreshToken) {
$refreshTokenValidUntil = TimeUtil::nowAddSeconds($refreshResult['refresh_token_expires_in']);
$oauthInfo['refreshToken'] = $refreshToken;
$oauthInfo['refresh_token_expires_in'] = $refreshTokenValidUntil;
}
RootDB::db()->update('member', [
'oauth_info' => Json::encode($oauthInfo)
], 'no=%i', $userInfo['no']);
}
if ($tokenValidUntil && $now <= $tokenValidUntil) {
return null;
}
//인증 시스템 가동
$session = Session::getInstance();
$session->access_token = $accessToken;
$session->expires = $accessTokenValidUntil;
$session->refresh_token = $refreshToken;
$session->refresh_token_expires = $refreshTokenValidUntil;
if (!createOTPbyUserNO($userInfo['no'])) {
return [false, '인증 코드를 보내는데 실패했습니다.'];
}
return [true, '인증 코드를 입력해주세요'];
}
}