feat(security): 로그인 알림 — 직전 로그인과 IP 비교, 새 환경 로그인 시 경고

completeLogin에서 직전 성공 로그인 IP와 비교해 다르면 로그인 메시지에 경고 표시(이전 로그인 일시·IP 안내). 기존 member_log 활용, 외부 인프라 불필요.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
taekyoungc
2026-06-24 18:40:20 +09:00
parent 3b3f83461a
commit 76260572b1

View File

@@ -565,16 +565,39 @@ class Auth extends BaseController
'mb_locked_until' => null, 'mb_locked_until' => null,
]); ]);
// 로그인 알림 — 직전 성공 로그인과 IP 비교(새 IP면 경고). 현재 로그인 기록 INSERT 이전에 조회.
$loginNotice = '';
try {
$curIp = (string) $this->request->getIPAddress();
$prev = model(MemberLogModel::class)
->where('mb_idx', (int) $member->mb_idx)
->where('mll_success', 1)
->orderBy('mll_idx', 'DESC')
->first();
if ($prev) {
$prevIp = (string) ($prev->mll_ip ?? '');
$prevAt = (string) ($prev->mll_regdate ?? '');
if ($prevIp !== '' && $prevIp !== $curIp) {
$loginNotice = ' ⚠ 새로운 환경(IP ' . $curIp . ')에서 로그인되었습니다. 본인이 아니면 비밀번호를 변경하세요. (이전 로그인: ' . $prevAt . ' · ' . $prevIp . ')';
} elseif ($prevAt !== '') {
$loginNotice = ' (이전 로그인: ' . $prevAt . ')';
}
}
} catch (\Throwable $e) {
// 알림 계산 실패는 로그인에 영향 주지 않음
}
$this->insertMemberLog($logData, true, '로그인 성공', (int) $member->mb_idx); $this->insertMemberLog($logData, true, '로그인 성공', (int) $member->mb_idx);
$successMsg = '로그인되었습니다.' . $loginNotice;
if ((int) $member->mb_level === \Config\Roles::LEVEL_LOCAL_ADMIN) { if ((int) $member->mb_level === \Config\Roles::LEVEL_LOCAL_ADMIN) {
return redirect()->to(site_url('admin'))->with('success', '로그인되었습니다.'); return redirect()->to(site_url('admin'))->with('success', $successMsg);
} }
if (\Config\Roles::isSuperAdminEquivalent((int) $member->mb_level)) { if (\Config\Roles::isSuperAdminEquivalent((int) $member->mb_level)) {
return redirect()->to(site_url('admin/select-local-government'))->with('success', '로그인되었습니다.'); return redirect()->to(site_url('admin/select-local-government'))->with('success', $successMsg);
} }
return redirect()->to(site_url('/'))->with('success', '로그인되었습니다.'); return redirect()->to(site_url('/'))->with('success', $successMsg);
} }
private function buildLogData(string $mbId, ?int $mbIdx): array private function buildLogData(string $mbId, ?int $mbIdx): array