适用于小型组织企业内部 smb共享盘账户密码修改。
环境:
python3.11
pip install flask pywin32 ip address
可选 pip install pyinstaller
效果:
代码:
from flask import Flask, render_template_string, request, flash, abort
import win32net
import win32security
import win32api
import os
import ctypes
import sys
import re
import logging
from logging.handlers import RotatingFileHandler
import ipaddress # 用于处理IP地址和CIDR段
app = Flask(__name__)
app.secret_key = os.urandom(24) # 用于flash消息
# 配置日志
if not os.path.exists('logs'):
os.mkdir('logs')
file_handler = RotatingFileHandler('logs/password_changer.log', maxBytes=10240, backupCount=10)
file_handler.setFormatter(logging.Formatter(
'%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
))
file_handler.setLevel(logging.INFO)
app.logger.addHandler(file_handler)
app.logger.setLevel(logging.INFO)
app.logger.info('SMB密码修改工具启动')
# 允许访问的IP地址或CIDR段列表
# 支持单个IP(如"127.0.0.1")和CIDR段(如"172.16.0.0/24")
ALLOWED_IPS = [
# 本地回环地址
# 可以添加更多IP或网段
# "192.168.1.0/24",
# "10.0.0.100",
"172.16.0.0/24", # 示例:允许172.16.0.0-172.16.0.255整个网段
"192.168.88.0/24",
"10.0.202.0/24",
]
# 检查是否以管理员身份运行
def is_admin():
try:
return ctypes.windll.shell32.IsUserAnAdmin()
except:
return False
# 获取客户端真实IP
def get_real_ip():
# 检查代理头,获取真实IP
if 'X-Forwarded-For' in request.headers:
# X-Forwarded-For格式: client_ip, proxy1_ip, proxy2_ip...
return request.headers['X-Forwarded-For'].split(',')[0].strip()
elif 'X-Real-IP' in request.headers:
return request.headers['X-Real-IP']
else:
return request.remote_addr
# 检查IP是否在允许的列表或网段中
def is_ip_allowed(ip):
try:
client_ip = ipaddress.IPv4Address(ip)
for allowed in ALLOWED_IPS:
try:
# 尝试解析为IP网段
network = ipaddress.IPv4Network(allowed, strict=False)
if client_ip in network:
return True
except ValueError:
# 如果不是网段,则尝试解析为单个IP
try:
allowed_ip = ipaddress.IPv4Address(allowed)
if client_ip == allowed_ip:
return True
except ValueError:
# 无效的IP格式,跳过
app.logger.warning(f"无效的允许IP配置: {allowed}")
continue
return False
except ValueError:
app.logger.warning(f"无效的客户端IP地址: {ip}")
return False
# IP访问限制中间件
@app.before_request
def restrict_ip():
# 获取客户端真实IP地址
client_ip = get_real_ip()
# 记录访问尝试
app.logger.info(f"访问尝试 from {client_ip} to {request.path}")
# 检查IP是否在允许列表中
if not is_ip_allowed(client_ip):
# 记录未授权访问尝试
app.logger.warning(f"未授权的访问尝试 from {client_ip}")
abort(403) # 拒绝访问
# 验证用户密码
def verify_password(username, password):
try:
# 使用Windows API验证凭据
hToken = win32security.LogonUser(
username,
None,
password,
win32security.LOGON32_LOGON_INTERACTIVE,
win32security.LOGON32_PROVIDER_DEFAULT
)
hToken.Close()
return True
except win32security.error as e:
error_code = e.winerror
error_msg = e.strerror
app.logger.warning(f"账户验证发生错误 ==》 {error_code}; {error_msg}")
return False
# 获取用户全名(用于密码验证)
def get_user_fullname(username):
try:
server = win32api.GetComputerName()
user_info = win32net.NetUserGetInfo(server, username, 1)
return user_info.get('comment', '').strip() # 通常全名存储在comment字段
except:
return ''
# 密码复杂度验证
def validate_password(password, username, fullname):
errors = []
# 1. 密码长度必须为8至16位
if len(password) < 8 or len(password) > 16:
errors.append("密码长度必须为8至16位")
# 2. 必须包含:数字、大写字母、小写字母、特殊字符中的4种字符
has_digit = re.search(r'\d', password) is not None
has_upper = re.search(r'[A-Z]', password) is not None
has_lower = re.search(r'[a-z]', password) is not None
has_special = re.search(r'[^A-Za-z0-9]', password) is not None
if sum([has_digit, has_upper, has_lower, has_special]) < 4:
errors.append("密码必须包含数字、大写字母、小写字母和特殊字符")
# 3. 连续3位及以上数字不能连号(例如123、654)
digit_sequences = re.findall(r'\d{3,}', password)
for seq in digit_sequences:
for i in range(len(seq) - 2):
triplet = seq[i:i+3]
# 检查升序连号 (123)
if int(triplet[0]) + 1 == int(triplet[1]) and int(triplet[1]) + 1 == int(triplet[2]):
errors.append(f"密码包含连续数字序列: {triplet}")
break
# 检查降序连号 (321)
if int(triplet[0]) - 1 == int(triplet[1]) and int(triplet[1]) - 1 == int(triplet[2]):
errors.append(f"密码包含连续数字序列: {triplet}")
break
# 4. 连续3位及以上字母不能连续(例如abc、cba)
alpha_sequences = re.findall(r'[A-Za-z]{3,}', password)
for seq in alpha_sequences:
for i in range(len(seq) - 2):
triplet = seq[i:i+3].lower()
# 检查升序连续 (abc)
if ord(triplet[0]) + 1 == ord(triplet[1]) and ord(triplet[1]) + 1 == ord(triplet[2]):
errors.append(f"密码包含连续字母序列: {seq[i:i+3]}")
break
# 检查降序连续 (cba)
if ord(triplet[0]) - 1 == ord(triplet[1]) and ord(triplet[1]) - 1 == ord(triplet[2]):
errors.append(f"密码包含连续字母序列: {seq[i:i+3]}")
break
# 5. 密码不能包含连续3个及以上相同字符(例如aaa、rrr)
same_chars = re.findall(r'(.)\1{2,}', password)
if same_chars:
errors.append(f"密码包含连续相同字符: {', '.join(set(same_chars))}")
# 6. 密码不能包含账号
if username.lower() in password.lower():
errors.append("密码不能包含用户名")
# 7. 密码不能包含用户姓名大小写全拼
if fullname:
# 检查全名的各种形式
name_variations = [
fullname.lower(),
fullname.replace(' ', '').lower(),
fullname.replace(' ', '').upper(),
fullname.upper()
]
for variation in name_variations:
if variation and variation in password:
errors.append("密码不能包含用户姓名")
break
return errors
# 修改用户密码
def change_password(username, old_password, new_password):
try:
if not verify_password(username, old_password):
return False, "原密码不正确"
# 获取用户全名用于密码验证
fullname = get_user_fullname(username)
# 验证新密码是否符合规则
password_errors = validate_password(new_password, username, fullname)
if password_errors:
return False, ";".join(password_errors)
server = win32api.GetComputerName()
# 设置密码需要管理员权限
win32net.NetUserChangePassword(server, username, old_password, new_password)
return True, "密码修改成功"
except win32net.error as e:
# 处理不同的错误情况
error_code = e.args[0]
if error_code == 2221:
return False, "用户名不存在"
elif error_code == 86:
return False, "原密码不正确"
elif error_code == 2245:
return False, "新密码不符合系统密码策略"
else:
return False, f"修改失败: {str(e.args)}"
# 主页面模板 - 直接输入用户名和旧密码
index_template = '''
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SMB账户密码修改</title>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css">
</head>
<body class="bg-gray-100 min-h-screen">
<div class="container mx-auto px-4 py-8 max-w-3xl">
<h1 class="text-3xl font-bold text-center mb-8 text-gray-800">SMB账户密码修改</h1>
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="mb-6">
{% for message in messages %}
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative" role="alert">
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<div class="bg-white rounded-lg shadow-md p-6">
<form method="POST" action="/verify-password">
<div class="mb-4">
<label for="username" class="block text-gray-700 text-sm font-bold mb-2">用户名</label>
<input type="text" id="username" name="username" required
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
</div>
<div class="mb-4">
<label for="old_password" class="block text-gray-700 text-sm font-bold mb-2">输入旧密码</label>
<input type="password" id="old_password" name="old_password" required
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
</div>
<div class="flex items-center justify-between">
<button type="submit"
class="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
验证并修改密码
</button>
</div>
</form>
</div>
</div>
</body>
</html>
'''
# 修改密码页面模板 - 包含密码规则说明
change_password_template = '''
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>修改密码 - SMB账户密码修改</title>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css">
</head>
<body class="bg-gray-100 min-h-screen">
<div class="container mx-auto px-4 py-8 max-w-2xl">
<h1 class="text-2xl font-bold text-center mb-6 text-gray-800">修改密码</h1>
<h2 class="text-center text-gray-600 mb-8">用户: {{ username }}</h2>
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="mb-6">
{% for message in messages %}
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative" role="alert">
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<div class="grid grid-cols-1 md:grid-cols-12 gap-6">
<div class="md:col-span-7 bg-white rounded-lg shadow-md p-6">
<form method="POST" action="/do-change-password">
<input type="hidden" name="username" value="{{ username }}">
<input type="hidden" name="old_password" value="{{ old_password }}">
<div class="mb-4">
<label for="new_password" class="block text-gray-700 text-sm font-bold mb-2">新密码</label>
<input type="password" id="new_password" name="new_password" required
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
</div>
<div class="mb-6">
<label for="confirm_password" class="block text-gray-700 text-sm font-bold mb-2">确认新密码</label>
<input type="password" id="confirm_password" name="confirm_password" required
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
</div>
<div class="flex items-center justify-between">
<button type="submit"
class="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
确认修改
</button>
<a href="/" class="inline-block align-baseline font-bold text-sm text-blue-500 hover:text-blue-800">
返回主页
</a>
</div>
</form>
</div>
<div class="md:col-span-5 bg-blue-50 rounded-lg p-6">
<h3 class="text-lg font-semibold mb-3 text-gray-800">
<i class="fa fa-info-circle text-blue-500 mr-2"></i>密码规则
</h3>
<ul class="text-sm text-gray-700 space-y-1">
<li><i class="fa fa-check-circle text-blue-500 mr-1"></i>必须包含数字、大写字母、小写字母和特殊字符</li>
<li><i class="fa fa-check-circle text-blue-500 mr-1"></i>长度必须为8至16位</li>
<li><i class="fa fa-check-circle text-blue-500 mr-1"></i>不能包含连续3位及以上连号数字(如123、654)</li>
<li><i class="fa fa-check-circle text-blue-500 mr-1"></i>不能包含连续3位及以上连续字母(如abc、cba)</li>
<li><i class="fa fa-check-circle text-blue-500 mr-1"></i>不能包含连续3个及以上相同字符(如aaa、111)</li>
<li><i class="fa fa-check-circle text-blue-500 mr-1"></i>不能包含用户名</li>
<li><i class="fa fa-check-circle text-blue-500 mr-1"></i>不能包含用户姓名的全拼</li>
</ul>
</div>
</div>
</div>
</body>
</html>
'''
# 成功页面模板
success_template = '''
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>操作成功 - SMB账户密码修改</title>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css">
</head>
<body class="bg-gray-100 min-h-screen flex items-center justify-center">
<div class="bg-white rounded-lg shadow-md p-8 max-w-md w-full text-center">
<div class="text-green-500 text-5xl mb-4">
<i class="fa fa-check-circle"></i>
</div>
<h1 class="text-2xl font-bold text-gray-800 mb-2">操作成功</h1>
<p class="text-gray-600 mb-6">{{ message }}</p>
<a href="/" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline inline-block">
返回主页
</a>
</div>
</body>
</html>
'''
# 403错误页面
@app.errorhandler(403)
def forbidden(error):
client_ip = get_real_ip()
return '''
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>访问被拒绝</title>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css">
</head>
<body class="bg-gray-100 min-h-screen flex items-center justify-center">
<div class="bg-white rounded-lg shadow-md p-8 max-w-md w-full text-center">
<div class="text-red-500 text-5xl mb-4">
<i class="fa fa-exclamation-triangle"></i>
</div>
<h1 class="text-2xl font-bold text-gray-800 mb-2">访问被拒绝</h1>
<p class="text-gray-600 mb-6">您的IP地址(''' + client_ip + ''')没有权限访问此页面。</p>
</div>
</body>
</html>
''', 403
@app.route('/')
def index():
return render_template_string(index_template)
@app.route('/verify-password', methods=['POST'])
def verify_password_route():
username = request.form.get('username')
old_password = request.form.get('old_password')
client_ip = get_real_ip()
if not username or not old_password:
flash("请输入用户名和密码")
return render_template_string(index_template)
app.logger.info(f"用户 {username} 尝试验证密码 from {client_ip}")
if verify_password(username, old_password):
app.logger.info(f"用户 {username} 密码验证成功 from {client_ip}")
return render_template_string(change_password_template, username=username, old_password=old_password)
else:
app.logger.warning(f"用户 {username} 密码验证失败 from {client_ip}")
# 不明确提示是用户名不存在还是密码错误,提高安全性
flash("用户名或密码不正确")
return render_template_string(index_template)
@app.route('/do-change-password', methods=['POST'])
def do_change_password():
username = request.form.get('username')
old_password = request.form.get('old_password')
new_password = request.form.get('new_password')
confirm_password = request.form.get('confirm_password')
client_ip = get_real_ip()
app.logger.info(f"用户 {username} 尝试修改密码 from {client_ip}")
if new_password != confirm_password:
flash("两次输入的新密码不一致")
return render_template_string(change_password_template, username=username, old_password=old_password)
success, message = change_password(username, old_password, new_password)
if success:
app.logger.info(f"用户 {username} 密码修改成功 from {client_ip}")
return render_template_string(success_template, message=message)
else:
app.logger.warning(f"用户 {username} 密码修改失败: {message} from {client_ip}")
flash(message)
return render_template_string(change_password_template, username=username, old_password=old_password)
if __name__ == '__main__':
# 检查是否以管理员身份运行
if not is_admin():
print("错误:此应用程序需要以管理员权限运行才能修改用户密码。")
print("请右键点击命令提示符或PyCharm,选择'以管理员身份运行',然后重新启动应用。")
sys.exit(1)
# 安装必要的依赖提示
try:
import ipaddress
except ImportError:
print("检测到缺少ipaddress模块,正在尝试安装...")
import subprocess
subprocess.check_call([sys.executable, "-m", "pip", "install", "ipaddress"])
# 启动应用
app.run(host='172.16.0.253', port=8080, debug=True)
注意:
需要管理员权限运行