feat: update auth and security flow
This commit is contained in:
@@ -7,17 +7,19 @@ use CodeIgniter\Config\BaseConfig;
|
|||||||
/**
|
/**
|
||||||
* 로그인·2차 인증(TOTP) 관련 설정
|
* 로그인·2차 인증(TOTP) 관련 설정
|
||||||
*
|
*
|
||||||
* .env 예:
|
* .env 의 auth.requireTotp 가 Config 기본값보다 우선합니다. 끄려면 반드시 false 로 두세요.
|
||||||
* auth.requireTotp = true
|
* 예:
|
||||||
* auth.totpIssuer = "쓰레기봉투 물류시스템"
|
* auth.requireTotp = false
|
||||||
|
* auth.requireTotp = true # 운영에서 2FA 켤 때
|
||||||
|
* auth.totpIssuer = "종량제 시스템"
|
||||||
*/
|
*/
|
||||||
class Auth extends BaseConfig
|
class Auth extends BaseConfig
|
||||||
{
|
{
|
||||||
/** 운영·스테이징 true 권장. 로컬 개발 시 false 로 1단계만 로그인 가능 */
|
/** false 이면 로그인 시 TOTP·등록 유도 없음. 운영에서 켤 때 .env 에 auth.requireTotp = true */
|
||||||
public bool $requireTotp = true;
|
public bool $requireTotp = false;
|
||||||
|
|
||||||
/** 인증 앱에 표시되는 발급자(issuer) */
|
/** 인증 앱에 표시되는 발급자(issuer) */
|
||||||
public string $totpIssuer = '쓰레기봉투 물류시스템';
|
public string $totpIssuer = '종량제 시스템';
|
||||||
|
|
||||||
/** TOTP 연속 실패 시 세션 종료 전 허용 횟수 */
|
/** TOTP 연속 실패 시 세션 종료 전 허용 횟수 */
|
||||||
public int $totpMaxAttempts = 5;
|
public int $totpMaxAttempts = 5;
|
||||||
|
|||||||
@@ -27,8 +27,40 @@ class Encryption extends BaseConfig
|
|||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
$hex = (string) env('encryption.key', '');
|
$hex = trim((string) env('encryption.key', ''));
|
||||||
|
if (
|
||||||
|
(str_starts_with($hex, "'") && str_ends_with($hex, "'"))
|
||||||
|
|| (str_starts_with($hex, '"') && str_ends_with($hex, '"'))
|
||||||
|
) {
|
||||||
|
$hex = substr($hex, 1, -1);
|
||||||
|
}
|
||||||
$this->key = (strlen($hex) === 64 && ctype_xdigit($hex)) ? hex2bin($hex) : '';
|
$this->key = (strlen($hex) === 64 && ctype_xdigit($hex)) ? hex2bin($hex) : '';
|
||||||
|
|
||||||
|
$prev = trim((string) env('encryption.previousKeys', ''));
|
||||||
|
if ($prev !== '') {
|
||||||
|
$parsed = [];
|
||||||
|
$parts = array_map('trim', explode(',', $prev));
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
if ($part === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (str_starts_with($part, 'hex2bin:')) {
|
||||||
|
$part = substr($part, 8);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
(str_starts_with($part, "'") && str_ends_with($part, "'"))
|
||||||
|
|| (str_starts_with($part, '"') && str_ends_with($part, '"'))
|
||||||
|
) {
|
||||||
|
$part = substr($part, 1, -1);
|
||||||
|
}
|
||||||
|
if (strlen($part) === 64 && ctype_xdigit($part)) {
|
||||||
|
$parsed[] = 'hex2bin:' . $part;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (! empty($parsed)) {
|
||||||
|
$this->previousKeys = $parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ declare(strict_types=1);
|
|||||||
*
|
*
|
||||||
* 저장 형식: 암호화된 값은 "ENC:" + base64(암호문) 으로 저장. "ENC:" 없으면 평문(기존)으로 간주.
|
* 저장 형식: 암호화된 값은 "ENC:" + base64(암호문) 으로 저장. "ENC:" 없으면 평문(기존)으로 간주.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if (! function_exists('pii_encrypt')) {
|
if (! function_exists('pii_encrypt')) {
|
||||||
function pii_encrypt(?string $value): string
|
function pii_encrypt(?string $value): string
|
||||||
{
|
{
|
||||||
@@ -21,9 +22,8 @@ if (! function_exists('pii_encrypt')) {
|
|||||||
}
|
}
|
||||||
$encrypter = service('encrypter');
|
$encrypter = service('encrypter');
|
||||||
$encrypted = $encrypter->encrypt($value);
|
$encrypted = $encrypter->encrypt($value);
|
||||||
|
|
||||||
return 'ENC:' . base64_encode($encrypted);
|
return 'ENC:' . base64_encode($encrypted);
|
||||||
} catch (Throwable) {
|
} catch (Throwable $e) {
|
||||||
return $value;
|
return $value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -44,13 +44,23 @@ if (! function_exists('pii_decrypt')) {
|
|||||||
return $value;
|
return $value;
|
||||||
}
|
}
|
||||||
$encrypter = service('encrypter');
|
$encrypter = service('encrypter');
|
||||||
$raw = base64_decode(substr($value, 4), true);
|
$payload = substr($value, 4);
|
||||||
if ($raw === false) {
|
|
||||||
return $value;
|
// 현재 포맷: ENC: + base64(raw ciphertext)
|
||||||
|
$raw = base64_decode($payload, true);
|
||||||
|
if ($raw !== false) {
|
||||||
|
try {
|
||||||
|
return $encrypter->decrypt($raw);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
// legacy 포맷 재시도
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $encrypter->decrypt($raw);
|
// 레거시 포맷 호환:
|
||||||
} catch (Throwable) {
|
// - ENC: + encrypter 반환값(rawData=false 환경 등) 또는
|
||||||
|
// - ENC: + 기타 문자열 포맷
|
||||||
|
return $encrypter->decrypt($payload);
|
||||||
|
} catch (Throwable $e) {
|
||||||
return $value;
|
return $value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="flex items-center justify-between mb-1">
|
<div class="flex items-center justify-between mb-1">
|
||||||
<h3 class="text-sm font-bold text-gray-700">최근 발주 5건</h3>
|
<h3 class="text-sm font-bold text-gray-700">최근 발주 5건</h3>
|
||||||
<a href="<?= base_url('admin/bag-orders') ?>" class="text-xs text-blue-600 hover:underline">전체보기</a>
|
<a href="<?= base_url('bag/bag-orders') ?>" class="text-xs text-blue-600 hover:underline">전체보기</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="border border-gray-300 overflow-auto">
|
<div class="border border-gray-300 overflow-auto">
|
||||||
<table class="w-full data-table">
|
<table class="w-full data-table">
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
?>
|
?>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="font-mono text-sm">
|
<td class="font-mono text-sm">
|
||||||
<a href="<?= base_url('admin/bag-orders/detail/' . (int) $order->bo_idx) ?>" class="text-blue-600 hover:underline"><?= esc($order->bo_lot_no) ?></a>
|
<a href="<?= base_url('bag/bag-orders/detail/' . (int) $order->bo_idx) ?>" class="text-blue-600 hover:underline"><?= esc($order->bo_lot_no) ?></a>
|
||||||
</td>
|
</td>
|
||||||
<td><?= esc($order->bo_order_date) ?></td>
|
<td><?= esc($order->bo_order_date) ?></td>
|
||||||
<td>
|
<td>
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="flex items-center justify-between mb-1">
|
<div class="flex items-center justify-between mb-1">
|
||||||
<h3 class="text-sm font-bold text-gray-700">최근 판매 5건</h3>
|
<h3 class="text-sm font-bold text-gray-700">최근 판매 5건</h3>
|
||||||
<a href="<?= base_url('admin/bag-sales') ?>" class="text-xs text-blue-600 hover:underline">전체보기</a>
|
<a href="<?= base_url('bag/bag-sales') ?>" class="text-xs text-blue-600 hover:underline">전체보기</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="border border-gray-300 overflow-auto">
|
<div class="border border-gray-300 overflow-auto">
|
||||||
<table class="w-full data-table">
|
<table class="w-full data-table">
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8"/>
|
<meta charset="utf-8"/>
|
||||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||||
<title>로그인 - 쓰레기봉투 물류시스템</title>
|
<title>로그인 - 종량제 시스템</title>
|
||||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet"/>
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet"/>
|
||||||
<script>
|
<script>
|
||||||
@@ -26,14 +26,12 @@ tailwind.config = {
|
|||||||
</head>
|
</head>
|
||||||
<body class="bg-gray-100 text-gray-800 flex flex-col h-screen font-sans antialiased">
|
<body class="bg-gray-100 text-gray-800 flex flex-col h-screen font-sans antialiased">
|
||||||
<header class="bg-white border-b border-gray-300 h-12 flex items-center justify-between px-4 shrink-0">
|
<header class="bg-white border-b border-gray-300 h-12 flex items-center justify-between px-4 shrink-0">
|
||||||
<div class="flex items-center gap-2">
|
<a href="<?= base_url() ?>" class="flex items-center gap-2 shrink-0 text-base font-semibold text-gray-800 tracking-tight hover:text-blue-600" title="종량제 시스템">
|
||||||
<div class="w-6 h-6 flex items-center justify-center shrink-0">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="h-6 w-6 text-blue-900 translate-y-[1px]" aria-hidden="true" focusable="false">
|
||||||
<svg class="h-5 w-5" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
<path fill="currentColor" d="M9 3a1 1 0 00-1 1v1H5.75a.75.75 0 000 1.5h12.5a.75.75 0 000-1.5H16V4a1 1 0 00-1-1H9zm9 4H6v11a2 2 0 002 2h8a2 2 0 002-2V7zM10 9a.75.75 0 01.75.75v6a.75.75 0 01-1.5 0v-6A.75.75 0 0110 9zm4 0a.75.75 0 01.75.75v6a.75.75 0 01-1.5 0v-6A.75.75 0 0114 9z"/>
|
||||||
<rect width="16" height="16" fill="#2563eb"/><rect x="2" y="2" width="7" height="7" fill="white"/><rect x="5" y="5" width="9" height="9" fill="white"/>
|
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
<span class="whitespace-nowrap">종량제 시스템</span>
|
||||||
<a href="<?= base_url() ?>" class="text-base font-semibold text-gray-800 tracking-tight hover:text-blue-600">쓰레기봉투 물류시스템</a>
|
</a>
|
||||||
</div>
|
|
||||||
</header>
|
</header>
|
||||||
<div class="bg-title-bar text-white px-4 py-2 text-sm font-medium shrink-0">
|
<div class="bg-title-bar text-white px-4 py-2 text-sm font-medium shrink-0">
|
||||||
로그인
|
로그인
|
||||||
@@ -68,6 +66,6 @@ tailwind.config = {
|
|||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
<footer class="bg-gray-200 border-t border-gray-300 px-4 py-1 text-xs text-gray-600 shrink-0">쓰레기봉투 물류시스템</footer>
|
<footer class="bg-gray-200 border-t border-gray-300 px-4 py-1 text-xs text-gray-600 shrink-0">종량제 시스템</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8"/>
|
<meta charset="utf-8"/>
|
||||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||||
<title>2차 인증 - 쓰레기봉투 물류시스템</title>
|
<title>2차 인증 - 종량제 시스템</title>
|
||||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet"/>
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet"/>
|
||||||
<script>
|
<script>
|
||||||
@@ -25,9 +25,7 @@ tailwind.config = {
|
|||||||
</head>
|
</head>
|
||||||
<body class="bg-gray-100 text-gray-800 flex flex-col min-h-screen font-sans antialiased">
|
<body class="bg-gray-100 text-gray-800 flex flex-col min-h-screen font-sans antialiased">
|
||||||
<header class="bg-white border-b border-gray-300 h-12 flex items-center justify-between px-4 shrink-0">
|
<header class="bg-white border-b border-gray-300 h-12 flex items-center justify-between px-4 shrink-0">
|
||||||
<div class="flex items-center gap-2">
|
<?= view('components/header_brand') ?>
|
||||||
<a href="<?= base_url() ?>" class="text-base font-semibold text-gray-800 tracking-tight hover:text-blue-600">쓰레기봉투 물류시스템</a>
|
|
||||||
</div>
|
|
||||||
</header>
|
</header>
|
||||||
<div class="bg-title-bar text-white px-4 py-2 text-sm font-medium shrink-0">
|
<div class="bg-title-bar text-white px-4 py-2 text-sm font-medium shrink-0">
|
||||||
2차 인증 (TOTP)
|
2차 인증 (TOTP)
|
||||||
@@ -56,6 +54,6 @@ tailwind.config = {
|
|||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
<footer class="bg-gray-200 border-t border-gray-300 px-4 py-1 text-xs text-gray-600 shrink-0">쓰레기봉투 물류시스템</footer>
|
<footer class="bg-gray-200 border-t border-gray-300 px-4 py-1 text-xs text-gray-600 shrink-0">종량제 시스템</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8"/>
|
<meta charset="utf-8"/>
|
||||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||||
<title>2차 인증 등록 - 쓰레기봉투 물류시스템</title>
|
<title>2차 인증 등록 - 종량제 시스템</title>
|
||||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet"/>
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet"/>
|
||||||
<script>
|
<script>
|
||||||
@@ -24,7 +24,7 @@ tailwind.config = {
|
|||||||
</head>
|
</head>
|
||||||
<body class="bg-gray-100 text-gray-800 flex flex-col min-h-screen font-sans antialiased">
|
<body class="bg-gray-100 text-gray-800 flex flex-col min-h-screen font-sans antialiased">
|
||||||
<header class="bg-white border-b border-gray-300 h-12 flex items-center px-4 shrink-0">
|
<header class="bg-white border-b border-gray-300 h-12 flex items-center px-4 shrink-0">
|
||||||
<a href="<?= base_url() ?>" class="text-base font-semibold text-gray-800 tracking-tight hover:text-blue-600">쓰레기봉투 물류시스템</a>
|
<?= view('components/header_brand') ?>
|
||||||
</header>
|
</header>
|
||||||
<div class="bg-title-bar text-white px-4 py-2 text-sm font-medium shrink-0">
|
<div class="bg-title-bar text-white px-4 py-2 text-sm font-medium shrink-0">
|
||||||
2차 인증 앱 등록
|
2차 인증 앱 등록
|
||||||
@@ -64,6 +64,6 @@ tailwind.config = {
|
|||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
<footer class="bg-gray-200 border-t border-gray-300 px-4 py-1 text-xs text-gray-600 shrink-0">쓰레기봉투 물류시스템</footer>
|
<footer class="bg-gray-200 border-t border-gray-300 px-4 py-1 text-xs text-gray-600 shrink-0">종량제 시스템</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user