본문으로 건너뛰기

관리자 동시 접속 보안 강화.

PHP 8 · GODO26
변경 파일 수: 5개
변경 파일 목록
5
변경 파일 코드
추가삭제
Admin/base/duplicate_login.php+115
변경 전duplicate_login.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
변경 후duplicate_login.php
<link rel="stylesheet" href="<?= PATH_ADMIN_GD_SHARE ?>ncds/css/duplicate-login-ncds.css"/>
<form id="frmDuplicateLogin" action="login_ps.php" method="post">
<input type="hidden" name="mode" value="concurrentLoginConfirm"/>
<input type="hidden" name="returnUrl" value=""/>
<input type="hidden" name="keepExisting" id="keepExistingInput" value="n"/>
</form>
 
<script>
(() => {
const init = () => {
if (typeof window.ncua === 'undefined' || typeof window.ncua.Modal === 'undefined') {
setTimeout(init, 50);
return;
}
 
const makeBtn = (label, hierarchy, onClick) => {
if (window.createButton) { return window.createButton(label, hierarchy, onClick, 'sm'); }
const tpl = document.createElement('template');
tpl.innerHTML = `<button type="button" class="ncua-btn ncua-btn--sm ncua-btn--${hierarchy}"><span class="ncua-btn__label">${label}</span></button>`;
const btn = tpl.content.firstElementChild;
btn.addEventListener('click', onClick);
return btn;
};
 
const cancel = () => {
location.href = '/base/login_ps.php?mode=logout';
};
 
const modal = new window.ncua.Modal({
size: 'lg',
className: 'duplicate-login-ncds-modal',
closeOnBackdropClick: false,
onClose: cancel,
});
 
const header = new window.ncua.Modal.Header({
title: '고객님의 계정이 이미 사용중입니다.',
});
header.setCloseHandler(cancel);
 
const contentHtml = `
<div class="duplicate-login-ncds-content">
<p class="duplicate-login-ncds-desc">
• 고객님의 계정이 다른 IP에서 로그인되었습니다. 아래 버튼을 선택하여 로그인을 진행해주세요.<br>
• 안전하게 이용하려면 이전 로그인을 종료하고 새로 로그인해주세요.
</p>
<div class="ncua-table ncua-table--vertical">
<table>
<colgroup><col width="50%"><col width="50%"></colgroup>
<tbody>
<tr>
<th><div>현재 로그인한 IP</div></th>
<td><div><?= gd_htmlspecialchars($currentLoginIp) ?></div></td>
</tr>
<tr>
<th><div>이전 로그인한 IP</div></th>
<td><div><?= gd_htmlspecialchars($lastLoginIp) ?></div></td>
</tr>
</tbody>
</table>
</div>
<div class="duplicate-login-ncds-radio-group">
<label class="ncua-radio-field ncua-radio-field--xs has-text">
<span class="ncua-radio-input ncua-radio-input--xs ncua-radio-field__input">
<input type="radio" name="keepExistingModal" value="y" checked />
</span>
<span><span class="ncua-radio-field__text">이전 로그인을 그대로 유지한 상태에서 로그인 (동시 로그인 허용)</span></span>
</label>
<label class="ncua-radio-field ncua-radio-field--xs has-text">
<span class="ncua-radio-input ncua-radio-input--xs ncua-radio-field__input">
<input type="radio" name="keepExistingModal" value="n" />
</span>
<span><span class="ncua-radio-field__text">이전 로그인은 종료하고, 새로 로그인</span></span>
</label>
</div>
</div>
`;
const content = new window.ncua.Modal.Content(contentHtml);
 
const actions = new window.ncua.Modal.Actions('', { layout: 'horizontal', align: 'right' });
const confirmBtn = makeBtn('확인', 'primary', () => {
const checked = modal.getModalElement().querySelector('input[name="keepExistingModal"]:checked');
const keepExisting = checked?.value ?? 'n';
const standalone = keepExisting !== 'y';
 
fetch('/share/duplicate_login_ps.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
body: JSON.stringify({ standalone: standalone }),
}).then((response) => {
if (!response.ok) {
throw new Error('request failed');
}
location.reload();
}).catch(() => {
alert('요청하신 작업을 완료하지 못했습니다.\n잠시 후 다시 시도해 주세요.');
});
});
const cancelBtn = makeBtn('취소', 'secondary-gray', cancel);
actions.setContent([cancelBtn, confirmBtn]);
 
const modalEl = modal.getModalElement();
header.appendTo(modalEl);
content.appendTo(modalEl);
actions.appendTo(modalEl);
modal.open();
};
 
init();
})();
</script>
 
Component/Admin/AdminLogDAO.php+6
변경 전AdminLogDAO.php
 
const LOGIN = 'login';
const LOGOUT = 'logout';
 
 
const ALLOWED_MODES = ['authEmail', 'authSms', 'register', 'modify', 'delete', 'setManagePermission', 'getManagerListLayout'];
 
$page = '주문 내역 삭제';
}
 
 
 
 
 
 
$logType = $pageData['type'] ?? AdminLogDAO::LOG_TYPE_P2Y;
// 개인정보접속기록 다운로드로그 5년보관
if ($page == 'adminLogExcel') {
 
변경 후AdminLogDAO.php
 
const LOGIN = 'login';
const LOGOUT = 'logout';
const LOGOUT_SESSION_END = 'logoutSessionEnd';
 
const ALLOWED_MODES = ['authEmail', 'authSms', 'register', 'modify', 'delete', 'setManagePermission', 'getManagerListLayout'];
 
$page = '주문 내역 삭제';
}
 
if ($mode === self::LOGOUT_SESSION_END) {
$page = 'adminInfo';
$action = '로그아웃(세션 종료)';
}
 
$logType = $pageData['type'] ?? AdminLogDAO::LOG_TYPE_P2Y;
// 개인정보접속기록 다운로드로그 5년보관
if ($page == 'adminLogExcel') {
 
Component/Member/ManagerCs.php+9
변경 전ManagerCs.php
use Framework\Debug\Exception\DatabaseException;
use Framework\Security\Digester;
use Framework\Security\Otp;
 
use Framework\Utility\ComponentUtils;
use Framework\Utility\DateTimeUtils;
use Framework\Utility\StringUtils;
use Framework\Utility\GodoUtils;
 
 
/**
* Class ManagerCs
* @package Bundle\Component\Member
$request = \App::getInstance('request');
$session = \App::getInstance('session');
$logger = \App::getInstance('logger');
 
 
 
 
 
 
$this->db->query_reset();
$data['lastLoginIp'] = $request->getRemoteAddress();
$data['sessionLimitTime'] = $sessionLimitTime;
$session->del(self::SESSION_TEMP_MANAGER);
$session->set(self::SESSION_MANAGER_LOGIN, $data);
 
}
 
/**
 
변경 후ManagerCs.php
use Framework\Debug\Exception\DatabaseException;
use Framework\Security\Digester;
use Framework\Security\Otp;
use Framework\SimpleCache\SimpleCache;
use Framework\Utility\ComponentUtils;
use Framework\Utility\DateTimeUtils;
use Framework\Utility\StringUtils;
use Framework\Utility\GodoUtils;
use Origin\Service\Storage\ManagerDuplicateLoginService;
 
/**
* Class ManagerCs
* @package Bundle\Component\Member
$request = \App::getInstance('request');
$session = \App::getInstance('session');
$logger = \App::getInstance('logger');
$duplicateLoginService = new ManagerDuplicateLoginService(
\App::getInstance('application'),
\App::getInstance('session'),
SimpleCache::init(SimpleCache::REDIS)
);
 
$this->db->query_reset();
$data['lastLoginIp'] = $request->getRemoteAddress();
$data['sessionLimitTime'] = $sessionLimitTime;
$session->del(self::SESSION_TEMP_MANAGER);
$session->set(self::SESSION_MANAGER_LOGIN, $data);
$duplicateLoginService->addCurrentLoginToList($data['sno']);
}
 
/**
 
Component/Member/Manager.php+58-3
변경 전Manager.php
 
namespace Bundle\Component\Member;
 
 
use Component\Admin\AdminMenu;
use Component\Database\DBTableField;
use Component\Member\Exception\LoginException;
use Framework\Debug\Exception\AlertRedirectException;
use Framework\Security\Digester;
use Framework\Security\Otp;
 
use Framework\Utility\ComponentUtils;
use Framework\Utility\DateTimeUtils;
use Framework\Utility\GodoUtils;
use Framework\Utility\StringUtils;
use Origin\Service\Admin\Menu\AdminMenuFlagService;
use Origin\Service\Member\ManagerLogoutService;
 
use Password;
use Request;
use Session;
*/
private $_storage;
 
 
 
 
 
 
/**
* 생성자
*/
$this->smsAutoReceiveKind[$newKey] = $val;
}
unset($tmp, $newKey);
 
 
 
 
 
 
}
 
/**
$this->setManagerOldMenuAccessChange(); // 운영자 접속 권한 - 세부 권한으로 마이그레이션
$this->setManagerFunctionAuthChange(); // 운영자 기능 권한 - 기능 권한으로 마이그레이션
$this->setManagerScmFunctionAuthChange(); // 공급사 기능 권한 - 기능 권한으로 마이그레이션
 
 
// 관리자 로그인 성공 로그
$request->request()->set('adminLoginFl', true);
 
$adminMenuFlagService->updateAdminMenuFlagsInSession();
}
 
 
 
 
 
 
/**
* setManagerOldMenuAccessChange
* 운영자 세부 접속 권한 마이그레이션
}
}
 
 
 
 
 
 
 
 
 
 
 
 
/**
* 관리자 로그아웃
 
*/
public function managerLogout()
{
$cookie = \App::getInstance('cookie');
 
// 관리자 로그아웃 로그 작성
 
$adminLog = \App::load('Component\\Admin\\AdminLogDAO');
$adminLog->setAdminLog($adminLog::LOGOUT);
 
// 세션을 없애기전에 세션 쿠키를 지웁니다.
if ($cookie->has($session->getName())) {
// 관리자 통합회원 로그아웃
/** @var ManagerLogoutService $managerLogoutService */
$managerLogoutService = \App::getInstance(ManagerLogoutService::class);
$managerLogoutService->logout();
 
 
// 관리자만 로그아웃하는 경우 세션 클리어
if (MemberUtil::isLogin()) {
}
 
if ($result > 0 && $type != 'reset') {
Manager::managerLogout();
}
}
 
unset($arrBind);
return gd_htmlspecialchars_stripslashes($data);
}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
}
 
변경 후Manager.php
 
namespace Bundle\Component\Member;
 
use Component\Admin\AdminLogDAO;
use Component\Admin\AdminMenu;
use Component\Database\DBTableField;
use Component\Member\Exception\LoginException;
use Framework\Debug\Exception\AlertRedirectException;
use Framework\Security\Digester;
use Framework\Security\Otp;
use Framework\SimpleCache\SimpleCache;
use Framework\Utility\ComponentUtils;
use Framework\Utility\DateTimeUtils;
use Framework\Utility\GodoUtils;
use Framework\Utility\StringUtils;
use Origin\Service\Admin\Menu\AdminMenuFlagService;
use Origin\Service\Member\ManagerLogoutService;
use Origin\Service\Storage\ManagerDuplicateLoginService;
use Password;
use Request;
use Session;
*/
private $_storage;
 
private ManagerDuplicateLoginService $managerLoginStorage;
 
/** @var bool 중복 로그인으로 인한 (강제) 로그아웃 여부 */
private bool $shouldDuplicateLogout = false;
 
/**
* 생성자
*/
$this->smsAutoReceiveKind[$newKey] = $val;
}
unset($tmp, $newKey);
 
$this->managerLoginStorage = new ManagerDuplicateLoginService(
\App::getInstance('application'),
\App::getInstance('session'),
SimpleCache::init(SimpleCache::REDIS)
);
}
 
/**
$this->setManagerOldMenuAccessChange(); // 운영자 접속 권한 - 세부 권한으로 마이그레이션
$this->setManagerFunctionAuthChange(); // 운영자 기능 권한 - 기능 권한으로 마이그레이션
$this->setManagerScmFunctionAuthChange(); // 공급사 기능 권한 - 기능 권한으로 마이그레이션
$this->setDuplicateLoginList($data['sno']); // 중복 로그인 감지용 로그인 세션 목록에 현재 세션 등록
 
// 관리자 로그인 성공 로그
$request->request()->set('adminLoginFl', true);
 
$adminMenuFlagService->updateAdminMenuFlagsInSession();
}
 
public function setDuplicateLoginList(int $sno): void
{
$this->managerLoginStorage->addCurrentLoginToList($sno);
}
 
/**
* setManagerOldMenuAccessChange
* 운영자 세부 접속 권한 마이그레이션
}
}
 
/**
* 중복 로그인으로 인한 (강제) 로그아웃 여부 주입.
* managerLogout() 을 무인자로 유지하기 위해 파라미터 대신 setter 로 받는다.
*
* @param bool $shouldDuplicateLogout true 면 es_adminLog 에 세션 종료 action 으로 기록
*/
public function setShouldDuplicateLogout(bool $shouldDuplicateLogout): void
{
$this->shouldDuplicateLogout = $shouldDuplicateLogout;
}
 
/**
* 관리자 로그아웃
* 중복 로그인 여부는 setShouldDuplicateLogout() 로 주입한다.
*/
public function managerLogout()
{
$cookie = \App::getInstance('cookie');
 
// 관리자 로그아웃 로그 작성
/** @var AdminLogDAO $adminLog */
$adminLog = \App::load('Component\\Admin\\AdminLogDAO');
$adminLog->setAdminLog($this->shouldDuplicateLogout ? $adminLog::LOGOUT_SESSION_END : $adminLog::LOGOUT);
 
// 세션을 없애기전에 세션 쿠키를 지웁니다.
if ($cookie->has($session->getName())) {
// 관리자 통합회원 로그아웃
/** @var ManagerLogoutService $managerLogoutService */
$managerLogoutService = \App::getInstance(ManagerLogoutService::class);
$managerLogoutService->logout($this->shouldDuplicateLogout);
$this->managerLoginStorage->deleteCurrentLoginFromList();
 
// 관리자만 로그아웃하는 경우 세션 클리어
if (MemberUtil::isLogin()) {
}
 
if ($result > 0 && $type != 'reset') {
$this->managerLogout();
}
}
 
unset($arrBind);
return gd_htmlspecialchars_stripslashes($data);
}
 
public function isCurrentLoginRegistered(): bool
{
return $this->managerLoginStorage->isCurrentLoginRegistered();
}
 
public function isDuplicateLogin(): bool
{
return $this->managerLoginStorage->isDuplicateLatestLogin();
}
 
public function getLastLoginIp(): ?string
{
return $this->managerLoginStorage->getPreviousLoginIp();
}
 
public function allLogoutWithoutMe(): void
{
$this->managerLoginStorage->deleteLoginAllWithoutMe();
}
}
 
Controller/Admin/Base/IndexController.php+21
변경 전IndexController.php
use Component\Member\Manager;
use Core\Base\View\Alert;
use Framework\Enum\ExternalUrl;
 
use Framework\Utility\StringUtils;
use Origin\Service\License\LicenseRenewService;
 
$mainSettingPolicy = \App::load('Component\\Policy\\MainSettingPolicy');
$sessionByManager = $session->get(Manager::SESSION_MANAGER_LOGIN);
 
 
 
// 메인 통계의 권한 체크
$adminMenu = \App::load('Component\\Admin\\AdminMenu');
if ($this->getData('managerPermissionMethodExist') === true && $this->getData('adminMenuPermissionMethodExist') === true) {
 
$this->getView()->setPageName('base/index.php');
}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
}
 
변경 후IndexController.php
use Component\Member\Manager;
use Core\Base\View\Alert;
use Framework\Enum\ExternalUrl;
use Framework\Http\Session\SessionManager;
use Framework\Utility\StringUtils;
use Origin\Service\License\LicenseRenewService;
 
$mainSettingPolicy = \App::load('Component\\Policy\\MainSettingPolicy');
$sessionByManager = $session->get(Manager::SESSION_MANAGER_LOGIN);
 
$this->checkDuplicateLogin($session);
 
// 메인 통계의 권한 체크
$adminMenu = \App::load('Component\\Admin\\AdminMenu');
if ($this->getData('managerPermissionMethodExist') === true && $this->getData('adminMenuPermissionMethodExist') === true) {
 
$this->getView()->setPageName('base/index.php');
}
 
public function checkDuplicateLogin(SessionManager $session)
{
$manager = \App::getInstance(\Manager::class);
$request = \App::getInstance('request');
 
if (
$manager->isDuplicateLogin() &&
!$session->get('manager.allowDuplicateLogin')
) {
$this->getView()->setDefine('layout', 'layout_blank_noiframe.php');
$this->getView()->setPageName('base/duplicate_login.php');
$this->getView()->setData('lastLoginIp',$manager->getLastLoginIp());
$this->getView()->setData('currentLoginIp', $request->getRemoteAddress());
echo $this->getView()->render();
exit;
}
}
}