feat: 메일 인증 링크 유효시간 설정 추가

This commit is contained in:
whitedot
2026-09-09 07:58:05 +00:00
parent 7b3c2e845b
commit d111100856
11 changed files with 105 additions and 14 deletions
+16
View File
@@ -12,6 +12,15 @@ if ($is_admin != 'super') {
$sql = " select * from {$g5['config_table']} limit 1";
$config = sql_fetch($sql);
if (!isset($config['cf_email_certify_minutes'])) {
sql_query(
" ALTER TABLE `{$g5['config_table']}`
ADD `cf_email_certify_minutes` INT(11) NOT NULL DEFAULT '60' AFTER `cf_use_email_certify` ",
true
);
$config['cf_email_certify_minutes'] = 60;
}
if (!isset($config['cf_add_script'])) {
sql_query(
" ALTER TABLE `{$g5['config_table']}`
@@ -1189,6 +1198,13 @@ if ($config['cf_sms_use'] && $config['cf_icode_id'] && $config['cf_icode_pw']) {
<input type="checkbox" name="cf_use_email_certify" value="1" id="cf_use_email_certify" <?php echo $config['cf_use_email_certify'] ? 'checked' : ''; ?>> 사용
</td>
</tr>
<tr>
<th scope="row"><label for="cf_email_certify_minutes">메일인증 유효시간</label></th>
<td>
<?php echo help('인증메일 발송 후 링크를 사용할 수 있는 시간을 분 단위로 설정합니다. 0은 만료시간을 적용하지 않습니다.') ?>
<input type="number" name="cf_email_certify_minutes" value="<?php echo isset($config['cf_email_certify_minutes']) ? (int) $config['cf_email_certify_minutes'] : 60; ?>" id="cf_email_certify_minutes" class="frm_input" min="0"> 분
</td>
</tr>
<tr>
<th scope="row"><label for="cf_formmail_is_member">폼메일 사용 여부</label></th>
<td>
+4
View File
@@ -56,6 +56,7 @@ if (isset($_POST['cf_intercept_ip']) && $_POST['cf_intercept_ip']) {
$check_keys = array(
'cf_use_email_certify' => 'int',
'cf_email_certify_minutes' => 'int',
'cf_use_homepage' => 'int',
'cf_req_homepage' => 'int',
'cf_use_tel' => 'int',
@@ -178,6 +179,8 @@ foreach ($check_keys as $k => $v) {
}
}
$_POST['cf_email_certify_minutes'] = max(0, $_POST['cf_email_certify_minutes']);
// 본인확인을 사용할 경우 아이핀, 휴대폰인증 중 하나는 선택되어야 함
if ($_POST['cf_cert_use'] && !$_POST['cf_cert_ipin'] && !$_POST['cf_cert_hp'] && !$_POST['cf_cert_simple']) {
alert('본인확인을 위해 아이핀, 휴대폰 본인확인, KG이니시스 간편인증 서비스 중 하나 이상 선택해 주십시오.');
@@ -226,6 +229,7 @@ $sql = " update {$g5['config_table']}
cf_point_term = '{$_POST['cf_point_term']}',
cf_use_copy_log = '{$_POST['cf_use_copy_log']}',
cf_use_email_certify = '{$_POST['cf_use_email_certify']}',
cf_email_certify_minutes = '{$_POST['cf_email_certify_minutes']}',
cf_login_point = '{$_POST['cf_login_point']}',
cf_cut_name = '{$_POST['cf_cut_name']}',
cf_nick_modify = '{$_POST['cf_nick_modify']}',
+9
View File
@@ -9,6 +9,15 @@ include_once('./admin.head.php');
$is_check = false;
// 메일 인증 링크 유효시간 설정 추가
if (!isset($config['cf_email_certify_minutes'])) {
sql_query("ALTER TABLE `{$g5['config_table']}`
ADD `cf_email_certify_minutes` int(11) NOT NULL DEFAULT '60' AFTER `cf_use_email_certify`
", true);
$is_check = true;
}
//소셜 로그인 관련 필드 및 구글 리챕챠 필드 추가
if(!isset($config['cf_social_login_use'])) {
sql_query("ALTER TABLE `{$g5['config_table']}`
+18 -7
View File
@@ -6,8 +6,9 @@ if(function_exists('check_mail_bot')){ check_mail_bot($_SERVER['REMOTE_ADDR']);
$mb_id = isset($_GET['mb_id']) ? trim($_GET['mb_id']) : '';
$mb_md5 = isset($_GET['mb_md5']) ? trim($_GET['mb_md5']) : '';
$esc_mb_id = sql_real_escape_string($mb_id);
$sql = " select mb_id, mb_email_certify2, mb_leave_date, mb_intercept_date from {$g5['member_table']} where mb_id = '{$mb_id}' ";
$sql = " select mb_id, mb_datetime, mb_email_certify2, mb_leave_date, mb_intercept_date from {$g5['member_table']} where mb_id = '{$esc_mb_id}' ";
$row = sql_fetch($sql);
if (!$row['mb_id'])
alert('존재하는 회원이 아닙니다.', G5_URL);
@@ -16,21 +17,31 @@ if ( $row['mb_leave_date'] || $row['mb_intercept_date'] ){
alert('탈퇴 또는 차단된 회원입니다.', G5_URL);
}
// 인증 링크는 한번만 처리가 되게 한다.
sql_query(" update {$g5['member_table']} set mb_email_certify2 = '' where mb_id = '$mb_id' ");
if ($mb_md5)
{
if ($mb_md5 == $row['mb_email_certify2'])
$valid_minutes = isset($config['cf_email_certify_minutes']) ? (int) $config['cf_email_certify_minutes'] : 60;
if (is_valid_email_certify_token($mb_md5, $row['mb_email_certify2'], $row['mb_datetime'], $valid_minutes))
{
sql_query(" update {$g5['member_table']} set mb_email_certify = '".G5_TIME_YMDHIS."' where mb_id = '{$mb_id}' ");
// 인증 링크는 한번만 처리되도록 인증과 동시에 토큰을 폐기한다.
$esc_mb_md5 = sql_real_escape_string($mb_md5);
sql_query(" update {$g5['member_table']} set mb_email_certify = '".G5_TIME_YMDHIS."', mb_email_certify2 = '' where mb_id = '{$esc_mb_id}' and mb_email_certify2 = '{$esc_mb_md5}' ");
if (get_sql_affected_rows() <= 0) {
alert('이미 처리되었거나 올바르지 않은 메일인증 요청입니다.', G5_URL);
}
alert("메일인증 처리를 완료 하였습니다.\\n\\n지금부터 {$mb_id} 아이디로 로그인 가능합니다.", G5_URL);
}
else
{
if ($mb_md5 === $row['mb_email_certify2']) {
sql_query(" update {$g5['member_table']} set mb_email_certify2 = '' where mb_id = '{$esc_mb_id}' ");
alert('메일인증 유효시간이 만료되었습니다. 인증메일을 다시 요청해 주십시오.', G5_URL);
}
alert('메일인증 요청 정보가 올바르지 않습니다.', G5_URL);
}
}
alert('제대로 된 값이 넘어오지 않았습니다.', G5_URL);
alert('제대로 된 값이 넘어오지 않았습니다.', G5_URL);
+2 -2
View File
@@ -46,7 +46,7 @@ $subject = '['.$config['cf_title'].'] 인증확인 메일입니다.';
$mb_name = $mb['mb_name'];
// 어떠한 회원정보도 포함되지 않은 일회용 난수를 생성하여 인증에 사용 (CSPRNG 사용)
$mb_md5 = get_random_token_string(16);
$mb_md5 = get_email_certify_token();
sql_query(" update {$g5['member_table']} set mb_email_certify2 = '$mb_md5' where mb_id = '{$esc_mb_id}' ");
@@ -62,4 +62,4 @@ mailer($config['cf_admin_email_name'], $config['cf_admin_email'], $mb_email, $su
$sql = " update {$g5['member_table']} set mb_email = '$mb_email' where mb_id = '{$esc_mb_id}' ";
sql_query($sql);
alert("인증메일을 {$mb_email} 메일로 다시 보내 드렸습니다.\\n\\n잠시후 {$mb_email} 메일을 확인하여 주십시오.", G5_URL);
alert("인증메일을 {$mb_email} 메일로 다시 보내 드렸습니다.\\n\\n잠시후 {$mb_email} 메일을 확인하여 주십시오.", G5_URL);
+2 -2
View File
@@ -325,7 +325,7 @@ if ($w == '') {
// 어떠한 회원정보도 포함되지 않은 일회용 난수를 생성하여 인증에 사용 (CSPRNG 사용)
if ($config['cf_use_email_certify']) {
$mb_md5 = get_random_token_string(16);
$mb_md5 = get_email_certify_token();
sql_query(" update {$g5['member_table']} set mb_email_certify2 = '$mb_md5' where mb_id = '$mb_id' ");
$certify_href = G5_BBS_URL.'/email_certify.php?mb_id='.$mb_id.'&amp;mb_md5='.$mb_md5;
}
@@ -609,7 +609,7 @@ if ($config['cf_use_email_certify'] && $old_email != $mb_email) {
$subject = '['.$config['cf_title'].'] 인증확인 메일입니다.';
// 어떠한 회원정보도 포함되지 않은 일회용 난수를 생성하여 인증에 사용 (CSPRNG 사용)
$mb_md5 = get_random_token_string(16);
$mb_md5 = get_email_certify_token();
sql_query(" update {$g5['member_table']} set mb_email_certify2 = '$mb_md5' where mb_id = '$mb_id' ");
+2 -1
View File
@@ -23,7 +23,8 @@ if (!defined('_GNUBOARD_')) exit; // 개별 페이지 접근 불가
<p style="margin:20px 0 0;padding:30px 30px 50px;min-height:200px;height:auto !important;height:200px;border-bottom:1px solid #eee">
<b><?php echo $mb_name ?></b> 님의 회원가입을 진심으로 축하합니다.<br>
회원님의 성원에 보답하고자 더욱 더 열심히 하겠습니다.<br>
<?php if ($config['cf_use_email_certify']) { ?>아래의 <strong>메일인증</strong>을 클릭하시면 회원가입이 완료됩니다.<br><?php } ?>
<?php if ($config['cf_use_email_certify']) { ?>아래의 <strong>메일인증</strong>을 클릭하시면 회원가입이 완료됩니다.<br>
<?php if (!empty($config['cf_email_certify_minutes'])) { ?>인증 링크는 발송 후 <?php echo (int) $config['cf_email_certify_minutes']; ?>분 동안 유효합니다.<br><?php } ?><?php } ?>
감사합니다.
</p>
+1
View File
@@ -26,6 +26,7 @@ if (!defined('_GNUBOARD_')) exit; // 개별 페이지 접근 불가
<?php } ?>
아래의 주소를 클릭하시면 인증이 완료됩니다.<br>
<?php if (!empty($config['cf_email_certify_minutes'])) { ?>인증 링크는 발송 후 <?php echo (int) $config['cf_email_certify_minutes']; ?>분 동안 유효합니다.<br><?php } ?>
<a href="<?php echo $certify_href ?>" target="_blank"><b><?php echo $certify_href ?></b></a><br><br>
회원님의 성원에 보답하고자 더욱 더 열심히 하겠습니다.<br>
+2 -1
View File
@@ -200,6 +200,7 @@ CREATE TABLE IF NOT EXISTS `g5_config` (
`cf_point_term` int(11) NOT NULL DEFAULT '0',
`cf_use_copy_log` tinyint(4) NOT NULL DEFAULT '0',
`cf_use_email_certify` tinyint(4) NOT NULL DEFAULT '0',
`cf_email_certify_minutes` int(11) NOT NULL DEFAULT '60',
`cf_login_point` int(11) NOT NULL DEFAULT '0',
`cf_cut_name` tinyint(4) NOT NULL DEFAULT '0',
`cf_nick_modify` int(11) NOT NULL DEFAULT '0',
@@ -987,4 +988,4 @@ CREATE TABLE IF NOT EXISTS `g5_menu` (
`me_use` tinyint(4) NOT NULL DEFAULT '0',
`me_mobile_use` tinyint(4) NOT NULL DEFAULT '0',
PRIMARY KEY (`me_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
+48
View File
@@ -2696,6 +2696,54 @@ function get_email_cert_key($mb_id, $mb_datetime)
return hash_hmac('sha256', $payload, $key);
}
/**
* 발급 시각을 포함한 메일 인증용 일회용 토큰을 생성한다.
*
* @return string
*/
function get_email_certify_token()
{
return get_random_token_string(16) . '.' . G5_SERVER_TIME;
}
/**
* 메일 인증 토큰의 일치 여부와 유효시간을 확인한다.
*
* 발급 시각이 없는 기존 토큰은 회원가입 시각을 기준으로 만료 여부를 확인한다.
*
* @param string $token
* @param string $stored_token
* @param string $mb_datetime
* @param int $valid_minutes
* @param int|null $now
* @return bool
*/
function is_valid_email_certify_token($token, $stored_token, $mb_datetime, $valid_minutes, $now = null)
{
if (!$token || !$stored_token || !hash_equals((string) $stored_token, (string) $token)) {
return false;
}
$valid_minutes = (int) $valid_minutes;
if ($valid_minutes < 1) {
return true;
}
if (preg_match('/\.([0-9]{10})$/', $stored_token, $matches)) {
$issued_at = (int) $matches[1];
} else {
$issued_at = strtotime($mb_datetime);
}
if (!$issued_at) {
return false;
}
$now = $now === null ? G5_SERVER_TIME : (int) $now;
return $issued_at + ($valid_minutes * 60) >= $now;
}
/**
* CSRF 방지용 Origin/Referer 검증 (OWASP 권장 패턴).
*
+1 -1
View File
@@ -273,7 +273,7 @@ if($result) {
$subject = '['.$config['cf_title'].'] 인증확인 메일입니다.';
// 어떠한 회원정보도 포함되지 않은 일회용 난수를 생성하여 인증에 사용 (CSPRNG 사용)
$mb_md5 = get_random_token_string(16);
$mb_md5 = get_email_certify_token();
sql_query(" update {$g5['member_table']} set mb_email_certify2 = '$mb_md5' where mb_id = '$mb_id' ");