feat(security): 중복 로그인 방지 (나중 로그인 우선)
- 로그인 시 세션 토큰 발급→회원 행(mb_session_token) 저장 - SessionGuardFilter(전역 before)로 매 요청 토큰 대조, 다르면 세션 무효화 후 로그인 이동 - DB 공유로 로컬·운영 등 서버 간에도 동작. 기존 로그인 세션은 재로그인 전까지 유지 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,7 @@ class Filters extends BaseFilters
|
||||
'adminAuth' => \App\Filters\AdminAuthFilter::class,
|
||||
'loginAuth' => \App\Filters\LoginAuthFilter::class,
|
||||
'embedRedirect' => \App\Filters\EmbedRedirectFilter::class,
|
||||
'sessionGuard' => \App\Filters\SessionGuardFilter::class,
|
||||
'csrf' => CSRF::class,
|
||||
'toolbar' => DebugToolbar::class,
|
||||
'honeypot' => Honeypot::class,
|
||||
@@ -78,6 +79,7 @@ class Filters extends BaseFilters
|
||||
// 'honeypot',
|
||||
// 'csrf',
|
||||
// 'invalidchars',
|
||||
'sessionGuard', // 중복 로그인 방지(나중 로그인 우선)
|
||||
],
|
||||
'after' => [
|
||||
// 'honeypot',
|
||||
|
||||
@@ -549,6 +549,8 @@ class Auth extends BaseController
|
||||
private function completeLogin(object $member, array $logData): RedirectResponse
|
||||
{
|
||||
$this->clearPending2faSession();
|
||||
// 중복 로그인 방지(나중 로그인 우선): 새 세션 토큰 발급 → 기존 다른 세션은 다음 요청 때 무효화됨
|
||||
$sessionToken = bin2hex(random_bytes(16));
|
||||
$sessionData = [
|
||||
'mb_idx' => $member->mb_idx,
|
||||
'mb_id' => $member->mb_id,
|
||||
@@ -556,6 +558,7 @@ class Auth extends BaseController
|
||||
'mb_level' => $member->mb_level,
|
||||
'mb_lg_idx' => $member->mb_lg_idx ?? null,
|
||||
'logged_in' => true,
|
||||
'session_token' => $sessionToken,
|
||||
];
|
||||
session()->set($sessionData);
|
||||
|
||||
@@ -563,6 +566,7 @@ class Auth extends BaseController
|
||||
'mb_latestdate' => date('Y-m-d H:i:s'),
|
||||
'mb_login_fail_count' => 0,
|
||||
'mb_locked_until' => null,
|
||||
'mb_session_token' => $sessionToken,
|
||||
]);
|
||||
|
||||
// 로그인 알림 — 직전 성공 로그인과 IP 비교(새 IP면 경고). 현재 로그인 기록 INSERT 이전에 조회.
|
||||
|
||||
62
app/Filters/SessionGuardFilter.php
Normal file
62
app/Filters/SessionGuardFilter.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Filters;
|
||||
|
||||
use App\Models\MemberModel;
|
||||
use CodeIgniter\Filters\FilterInterface;
|
||||
use CodeIgniter\HTTP\RequestInterface;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
|
||||
/**
|
||||
* 중복 로그인 방지(나중 로그인 우선).
|
||||
* 로그인 시 발급한 세션 토큰(session_token)을 회원 행(mb_session_token)과 매 요청마다 대조해,
|
||||
* 다른 곳에서 로그인해 토큰이 바뀌면 이 세션을 무효화하고 로그인으로 보낸다.
|
||||
* (DB를 공유하므로 로컬·운영 등 서로 다른 서버 간에도 동작)
|
||||
*/
|
||||
class SessionGuardFilter implements FilterInterface
|
||||
{
|
||||
public function before(RequestInterface $request, $arguments = null)
|
||||
{
|
||||
$session = session();
|
||||
if (! $session->get('logged_in')) {
|
||||
return null; // 비로그인 요청은 통과
|
||||
}
|
||||
|
||||
$mbIdx = (int) ($session->get('mb_idx') ?? 0);
|
||||
if ($mbIdx <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$member = model(MemberModel::class)->find($mbIdx);
|
||||
if ($member === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$dbToken = (string) ($member->mb_session_token ?? '');
|
||||
$myToken = (string) ($session->get('session_token') ?? '');
|
||||
|
||||
// DB에 토큰이 있고 내 세션 토큰과 다르면 → 다른 곳에서 더 늦게 로그인한 것
|
||||
if ($dbToken !== '' && $dbToken !== $myToken) {
|
||||
$session->destroy();
|
||||
|
||||
// AJAX/JSON 요청은 401, 일반 요청은 로그인으로
|
||||
if (strtolower((string) $request->getHeaderLine('X-Requested-With')) === 'xmlhttprequest'
|
||||
|| str_contains(strtolower((string) $request->getHeaderLine('Accept')), 'application/json')) {
|
||||
return service('response')->setStatusCode(401)->setJSON([
|
||||
'ok' => false, 'message' => '다른 곳에서 로그인되어 로그아웃되었습니다.',
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()->to(site_url('login'))->with('error', '다른 곳에서 로그인되어 자동 로그아웃되었습니다.');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
|
||||
{
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ class MemberModel extends Model
|
||||
'mb_leavedate',
|
||||
'mb_login_fail_count',
|
||||
'mb_locked_until',
|
||||
'mb_session_token',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
2
writable/database/member_session_token_add.sql
Normal file
2
writable/database/member_session_token_add.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- 중복 로그인 방지: 회원별 현재 세션 토큰(나중 로그인 우선)
|
||||
ALTER TABLE member ADD COLUMN mb_session_token VARCHAR(64) NULL DEFAULT NULL AFTER mb_locked_until;
|
||||
Reference in New Issue
Block a user