创建新工程
This commit is contained in:
466
scripts/SecurityManager.gd
Normal file
466
scripts/SecurityManager.gd
Normal file
@@ -0,0 +1,466 @@
|
||||
extends Node
|
||||
class_name SecurityManager
|
||||
## 安全管理器
|
||||
## 提供输入验证、防护措施和安全检查功能
|
||||
|
||||
# 安全配置常量(从SecurityConfig获取)
|
||||
const DEFAULT_MAX_MESSAGE_LENGTH = 500
|
||||
const DEFAULT_MAX_USERNAME_LENGTH = 50
|
||||
const DEFAULT_MAX_CHARACTER_NAME_LENGTH = 20
|
||||
const DEFAULT_MIN_CHARACTER_NAME_LENGTH = 2
|
||||
|
||||
# 恶意模式检测
|
||||
const MALICIOUS_PATTERNS = [
|
||||
"<script",
|
||||
"javascript:",
|
||||
"vbscript:",
|
||||
"onload=",
|
||||
"onerror=",
|
||||
"onclick=",
|
||||
"eval(",
|
||||
"document.cookie",
|
||||
"window.location",
|
||||
"alert(",
|
||||
"confirm(",
|
||||
"prompt(",
|
||||
"innerHTML",
|
||||
"outerHTML"
|
||||
]
|
||||
|
||||
# SQL注入模式(虽然我们使用JSON,但仍需防护)
|
||||
const SQL_INJECTION_PATTERNS = [
|
||||
"'",
|
||||
"\"",
|
||||
";",
|
||||
"--",
|
||||
"/*",
|
||||
"*/",
|
||||
"union",
|
||||
"select",
|
||||
"insert",
|
||||
"update",
|
||||
"delete",
|
||||
"drop",
|
||||
"create",
|
||||
"alter"
|
||||
]
|
||||
|
||||
# 会话管理
|
||||
var active_sessions: Dictionary = {}
|
||||
var session_timeout: float = 1800.0 # 30分钟会话超时
|
||||
var max_failed_attempts: int = 5
|
||||
var failed_attempts: Dictionary = {}
|
||||
var lockout_duration: float = 300.0 # 5分钟锁定时间
|
||||
|
||||
## 验证用户输入
|
||||
static func validate_input(input: String, input_type: String) -> Dictionary:
|
||||
"""
|
||||
验证用户输入的安全性和有效性
|
||||
@param input: 输入字符串
|
||||
@param input_type: 输入类型 ("username", "character_name", "message")
|
||||
@return: 验证结果 {valid: bool, error: String, sanitized: String}
|
||||
"""
|
||||
var result = {
|
||||
"valid": false,
|
||||
"error": "",
|
||||
"sanitized": ""
|
||||
}
|
||||
|
||||
# 基础检查
|
||||
if input == null:
|
||||
result.error = "输入不能为空"
|
||||
return result
|
||||
|
||||
# 转换为字符串并去除首尾空格
|
||||
var clean_input = str(input).strip_edges()
|
||||
|
||||
# 获取配置值
|
||||
var max_username_length = SecurityConfig.get_config("input_validation", "max_username_length", DEFAULT_MAX_USERNAME_LENGTH)
|
||||
var max_character_name_length = SecurityConfig.get_config("input_validation", "max_character_name_length", DEFAULT_MAX_CHARACTER_NAME_LENGTH)
|
||||
var min_character_name_length = SecurityConfig.get_config("input_validation", "min_character_name_length", DEFAULT_MIN_CHARACTER_NAME_LENGTH)
|
||||
var max_message_length = SecurityConfig.get_config("input_validation", "max_message_length", DEFAULT_MAX_MESSAGE_LENGTH)
|
||||
|
||||
# 长度检查
|
||||
match input_type:
|
||||
"username":
|
||||
if clean_input.length() == 0:
|
||||
result.error = "用户名不能为空"
|
||||
return result
|
||||
if clean_input.length() > max_username_length:
|
||||
result.error = "用户名长度不能超过 %d 个字符" % max_username_length
|
||||
return result
|
||||
|
||||
"character_name":
|
||||
if clean_input.length() < min_character_name_length:
|
||||
result.error = "角色名称至少需要 %d 个字符" % min_character_name_length
|
||||
return result
|
||||
if clean_input.length() > max_character_name_length:
|
||||
result.error = "角色名称不能超过 %d 个字符" % max_character_name_length
|
||||
return result
|
||||
|
||||
"message":
|
||||
if clean_input.length() == 0:
|
||||
result.error = "消息不能为空"
|
||||
return result
|
||||
if clean_input.length() > max_message_length:
|
||||
result.error = "消息长度不能超过 %d 个字符" % max_message_length
|
||||
return result
|
||||
|
||||
# 恶意内容检测
|
||||
var malicious_check = detect_malicious_content(clean_input)
|
||||
if not malicious_check.safe:
|
||||
result.error = "输入包含不安全内容: " + malicious_check.reason
|
||||
return result
|
||||
|
||||
# 清理输入
|
||||
var sanitized = sanitize_input(clean_input)
|
||||
|
||||
result.valid = true
|
||||
result.sanitized = sanitized
|
||||
return result
|
||||
|
||||
## 检测恶意内容
|
||||
static func detect_malicious_content(input: String) -> Dictionary:
|
||||
"""
|
||||
检测输入中的恶意内容
|
||||
@param input: 输入字符串
|
||||
@return: {safe: bool, reason: String}
|
||||
"""
|
||||
var input_lower = input.to_lower()
|
||||
|
||||
# 检查脚本注入
|
||||
for pattern in MALICIOUS_PATTERNS:
|
||||
if input_lower.contains(pattern.to_lower()):
|
||||
return {
|
||||
"safe": false,
|
||||
"reason": "检测到潜在的脚本注入: " + pattern
|
||||
}
|
||||
|
||||
# 检查SQL注入(虽然我们不使用SQL,但作为额外防护)
|
||||
for pattern in SQL_INJECTION_PATTERNS:
|
||||
if input_lower.contains(pattern.to_lower()):
|
||||
return {
|
||||
"safe": false,
|
||||
"reason": "检测到潜在的注入攻击: " + pattern
|
||||
}
|
||||
|
||||
# 检查过长的重复字符(可能的DoS攻击)
|
||||
if has_excessive_repetition(input):
|
||||
return {
|
||||
"safe": false,
|
||||
"reason": "检测到异常的重复字符模式"
|
||||
}
|
||||
|
||||
return {"safe": true, "reason": ""}
|
||||
|
||||
## 检查过度重复字符
|
||||
static func has_excessive_repetition(input: String) -> bool:
|
||||
"""
|
||||
检查字符串是否包含过度重复的字符
|
||||
@param input: 输入字符串
|
||||
@return: 是否包含过度重复
|
||||
"""
|
||||
if input.length() < 10:
|
||||
return false
|
||||
|
||||
var char_counts = {}
|
||||
for character in input:
|
||||
char_counts[character] = char_counts.get(character, 0) + 1
|
||||
|
||||
# 如果任何字符重复超过输入长度的70%,认为是异常
|
||||
var threshold = input.length() * 0.7
|
||||
for count in char_counts.values():
|
||||
if count > threshold:
|
||||
return true
|
||||
|
||||
return false
|
||||
|
||||
## 清理输入
|
||||
static func sanitize_input(input: String) -> String:
|
||||
"""
|
||||
清理输入字符串,移除潜在的危险字符
|
||||
@param input: 输入字符串
|
||||
@return: 清理后的字符串
|
||||
"""
|
||||
var sanitized = input
|
||||
|
||||
# 移除控制字符(除了换行和制表符)
|
||||
var clean_chars = []
|
||||
for i in range(sanitized.length()):
|
||||
var char_code = sanitized.unicode_at(i)
|
||||
# 保留可打印字符、空格、换行、制表符,以及Unicode字符(支持中文等)
|
||||
if (char_code >= 32 and char_code <= 126) or char_code == 10 or char_code == 9 or char_code > 127:
|
||||
clean_chars.append(sanitized[i])
|
||||
|
||||
sanitized = "".join(clean_chars)
|
||||
|
||||
# 移除HTML标签
|
||||
sanitized = remove_html_tags(sanitized)
|
||||
|
||||
# 限制连续空格
|
||||
sanitized = limit_consecutive_spaces(sanitized)
|
||||
|
||||
return sanitized.strip_edges()
|
||||
|
||||
## 移除HTML标签
|
||||
static func remove_html_tags(input: String) -> String:
|
||||
"""
|
||||
移除HTML标签
|
||||
@param input: 输入字符串
|
||||
@return: 移除标签后的字符串
|
||||
"""
|
||||
var regex = RegEx.new()
|
||||
regex.compile("<[^>]*>")
|
||||
return regex.sub(input, "", true)
|
||||
|
||||
## 限制连续空格
|
||||
static func limit_consecutive_spaces(input: String) -> String:
|
||||
"""
|
||||
将多个连续空格替换为单个空格
|
||||
@param input: 输入字符串
|
||||
@return: 处理后的字符串
|
||||
"""
|
||||
var regex = RegEx.new()
|
||||
regex.compile("\\s+")
|
||||
return regex.sub(input, " ", true)
|
||||
|
||||
## 验证消息格式
|
||||
static func validate_message_format(message: Dictionary) -> bool:
|
||||
"""
|
||||
验证网络消息格式的安全性
|
||||
@param message: 消息字典
|
||||
@return: 是否安全
|
||||
"""
|
||||
# 基础格式检查
|
||||
if not message.has("type") or not message.has("data") or not message.has("timestamp"):
|
||||
return false
|
||||
|
||||
# 检查消息类型是否在允许列表中
|
||||
var allowed_types = [
|
||||
"auth_request",
|
||||
"auth_response",
|
||||
"character_create",
|
||||
"character_move",
|
||||
"character_state",
|
||||
"dialogue_send",
|
||||
"world_state",
|
||||
"ping",
|
||||
"pong",
|
||||
"error"
|
||||
]
|
||||
|
||||
if not message.type in allowed_types:
|
||||
return false
|
||||
|
||||
# 检查时间戳是否合理(不能太旧或太新)
|
||||
var current_time = Time.get_unix_time_from_system() * 1000 # 转换为毫秒
|
||||
var msg_time = message.get("timestamp", 0)
|
||||
var time_diff = abs(current_time - msg_time)
|
||||
|
||||
# 允许5分钟的时间差(毫秒)
|
||||
if time_diff > 300000: # 5分钟 = 300000毫秒
|
||||
return false
|
||||
|
||||
# 检查数据字段
|
||||
if typeof(message.data) != TYPE_DICTIONARY:
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
## 创建会话
|
||||
func create_session(client_id: String, username: String) -> String:
|
||||
"""
|
||||
创建新的用户会话
|
||||
@param client_id: 客户端ID
|
||||
@param username: 用户名
|
||||
@return: 会话令牌
|
||||
"""
|
||||
var session_token = generate_session_token()
|
||||
var session_data = {
|
||||
"client_id": client_id,
|
||||
"username": username,
|
||||
"created_at": Time.get_unix_time_from_system(),
|
||||
"last_activity": Time.get_unix_time_from_system(),
|
||||
"is_active": true
|
||||
}
|
||||
|
||||
active_sessions[session_token] = session_data
|
||||
print("Session created for user: " + username)
|
||||
|
||||
return session_token
|
||||
|
||||
## 验证会话
|
||||
func validate_session(session_token: String) -> bool:
|
||||
"""
|
||||
验证会话是否有效
|
||||
@param session_token: 会话令牌
|
||||
@return: 是否有效
|
||||
"""
|
||||
if not active_sessions.has(session_token):
|
||||
return false
|
||||
|
||||
var session = active_sessions[session_token]
|
||||
var current_time = Time.get_unix_time_from_system()
|
||||
|
||||
# 检查会话是否过期
|
||||
if current_time - session.last_activity > session_timeout:
|
||||
invalidate_session(session_token)
|
||||
return false
|
||||
|
||||
# 更新最后活动时间
|
||||
session.last_activity = current_time
|
||||
return session.is_active
|
||||
|
||||
## 使会话无效
|
||||
func invalidate_session(session_token: String) -> void:
|
||||
"""
|
||||
使会话无效
|
||||
@param session_token: 会话令牌
|
||||
"""
|
||||
if active_sessions.has(session_token):
|
||||
var session = active_sessions[session_token]
|
||||
print("Session invalidated for user: " + session.get("username", "unknown"))
|
||||
active_sessions.erase(session_token)
|
||||
|
||||
## 生成会话令牌
|
||||
func generate_session_token() -> String:
|
||||
"""
|
||||
生成安全的会话令牌
|
||||
@return: 会话令牌
|
||||
"""
|
||||
var timestamp = Time.get_unix_time_from_system()
|
||||
var random1 = randi()
|
||||
var random2 = randi()
|
||||
var random3 = randi()
|
||||
|
||||
# 创建更复杂的令牌
|
||||
var token_data = str(timestamp) + "_" + str(random1) + "_" + str(random2) + "_" + str(random3)
|
||||
return token_data.sha256_text()
|
||||
|
||||
## 记录失败尝试
|
||||
func record_failed_attempt(identifier: String) -> bool:
|
||||
"""
|
||||
记录失败的认证尝试
|
||||
@param identifier: 标识符(IP地址或客户端ID)
|
||||
@return: 是否应该锁定
|
||||
"""
|
||||
var current_time = Time.get_unix_time_from_system()
|
||||
|
||||
if not failed_attempts.has(identifier):
|
||||
failed_attempts[identifier] = {
|
||||
"count": 0,
|
||||
"first_attempt": current_time,
|
||||
"last_attempt": current_time,
|
||||
"locked_until": 0
|
||||
}
|
||||
|
||||
var attempt_data = failed_attempts[identifier]
|
||||
|
||||
# 检查是否仍在锁定期
|
||||
if current_time < attempt_data.locked_until:
|
||||
return true # 仍被锁定
|
||||
|
||||
# 如果距离第一次尝试超过1小时,重置计数
|
||||
if current_time - attempt_data.first_attempt > 3600:
|
||||
attempt_data.count = 0
|
||||
attempt_data.first_attempt = current_time
|
||||
|
||||
attempt_data.count += 1
|
||||
attempt_data.last_attempt = current_time
|
||||
|
||||
# 检查是否需要锁定
|
||||
if attempt_data.count >= max_failed_attempts:
|
||||
attempt_data.locked_until = current_time + lockout_duration
|
||||
print("WARNING: Client locked due to too many failed attempts: " + identifier)
|
||||
return true
|
||||
|
||||
return false
|
||||
|
||||
## 检查是否被锁定
|
||||
func is_locked(identifier: String) -> bool:
|
||||
"""
|
||||
检查标识符是否被锁定
|
||||
@param identifier: 标识符
|
||||
@return: 是否被锁定
|
||||
"""
|
||||
if not failed_attempts.has(identifier):
|
||||
return false
|
||||
|
||||
var current_time = Time.get_unix_time_from_system()
|
||||
var attempt_data = failed_attempts[identifier]
|
||||
|
||||
return current_time < attempt_data.locked_until
|
||||
|
||||
## 清除失败尝试记录
|
||||
func clear_failed_attempts(identifier: String) -> void:
|
||||
"""
|
||||
清除失败尝试记录(成功认证后调用)
|
||||
@param identifier: 标识符
|
||||
"""
|
||||
if failed_attempts.has(identifier):
|
||||
failed_attempts.erase(identifier)
|
||||
|
||||
## 清理过期会话
|
||||
func cleanup_expired_sessions() -> void:
|
||||
"""清理过期的会话"""
|
||||
var current_time = Time.get_unix_time_from_system()
|
||||
var expired_tokens = []
|
||||
|
||||
for token in active_sessions:
|
||||
var session = active_sessions[token]
|
||||
if current_time - session.last_activity > session_timeout:
|
||||
expired_tokens.append(token)
|
||||
|
||||
for token in expired_tokens:
|
||||
invalidate_session(token)
|
||||
|
||||
if expired_tokens.size() > 0:
|
||||
print("Cleaned up %d expired sessions" % expired_tokens.size())
|
||||
|
||||
## 获取安全统计
|
||||
func get_security_stats() -> Dictionary:
|
||||
"""
|
||||
获取安全统计信息
|
||||
@return: 统计信息
|
||||
"""
|
||||
return {
|
||||
"active_sessions": active_sessions.size(),
|
||||
"failed_attempts": failed_attempts.size(),
|
||||
"locked_clients": _count_locked_clients()
|
||||
}
|
||||
|
||||
## 计算被锁定的客户端数量
|
||||
func _count_locked_clients() -> int:
|
||||
"""计算当前被锁定的客户端数量"""
|
||||
var current_time = Time.get_unix_time_from_system()
|
||||
var locked_count = 0
|
||||
|
||||
for identifier in failed_attempts:
|
||||
var attempt_data = failed_attempts[identifier]
|
||||
if current_time < attempt_data.locked_until:
|
||||
locked_count += 1
|
||||
|
||||
return locked_count
|
||||
|
||||
## 定期清理任务
|
||||
func _ready():
|
||||
"""初始化安全管理器"""
|
||||
# 初始化安全配置
|
||||
_initialize_config()
|
||||
|
||||
# 每5分钟清理一次过期会话
|
||||
var cleanup_interval = SecurityConfig.get_config("session_management", "cleanup_interval", 300.0)
|
||||
var cleanup_timer = Timer.new()
|
||||
cleanup_timer.wait_time = cleanup_interval
|
||||
cleanup_timer.timeout.connect(cleanup_expired_sessions)
|
||||
cleanup_timer.autostart = true
|
||||
add_child(cleanup_timer)
|
||||
|
||||
print("SecurityManager initialized with security level: " + SecurityConfig.get_security_level())
|
||||
|
||||
## 初始化配置
|
||||
func _initialize_config():
|
||||
"""从SecurityConfig初始化配置值"""
|
||||
session_timeout = SecurityConfig.get_config("session_management", "session_timeout", 1800.0)
|
||||
max_failed_attempts = SecurityConfig.get_config("session_management", "max_failed_attempts", 5)
|
||||
lockout_duration = SecurityConfig.get_config("session_management", "lockout_duration", 300.0)
|
||||
Reference in New Issue
Block a user