Python Flask: Windows 2022 server SMB账户(共享盘账户)密码修改

适用于小型组织企业内部 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) 

注意:

        需要管理员权限运行

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值