Ask me anything
Ithy Logo
Sign in to save

增强Flask应用的域账户登录与权限管理

实现安全的用户认证与权限控制,优化用户体验

server rooms dark

关键要点

  • 集成Active Directory认证:通过LDAP与Flask-Login实现域账户的安全认证。
  • 限制特定OU的访问:仅允许特定组织单位的用户登录,增强访问控制。
  • 扩展功能:包括用户账户解锁、日志记录和未来的功能扩展,以提升应用的灵活性和安全性。

1. 集成Active Directory认证

使用Flask-Login与LDAP实现域账户登录

为了实现基于域账户的登录验证,首先需要集成Active Directory (AD)认证。这可以通过结合使用Flask-Login和ldap3库来完成。

步骤1:安装必要的库

在项目环境中安装所需的Python库:

pip install Flask-Login ldap3

步骤2:配置LDAP连接

在app.py中配置与Active Directory的连接参数:

from flask import Flask, jsonify, request, redirect, url_for, render_template
from flask_cors import CORS
from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user
from ldap3 import Server, Connection, ALL
import json
import subprocess

app = Flask(__name__)
app.secret_key = 'your-secret-key'  # 替换为安全的密钥
CORS(app)

login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login'

# 用户类
class User(UserMixin):
    def __init__(self, id, username):
        self.id = id
        self.username = username

users = {}  # 存储登录用户

@login_manager.user_loader
def load_user(user_id):
    return users.get(user_id)

步骤3:实现登录路由

创建一个登录路由,用于处理用户的登录请求,并验证其属于特定的OU:

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        data = request.json
        username = data.get('username')
        password = data.get('password')
        ldap_url = data.get('ldap_url')  # LDAP服务器地址
        allowed_ou = data.get('allowed_ou')  # 允许的OU列表

        if not username or not password or not ldap_url:
            return jsonify({"error": "缺少必要参数"}), 400

        server = Server(ldap_url, get_info=ALL)
        try:
            conn = Connection(server, user=f'DOMAIN\\{username}', password=password)
            if not conn.bind():
                return jsonify({"error": "无效的凭证"}), 401

            # 检查用户所属OU
            conn.search('DC=yourdomain,DC=com', f'(sAMAccountName={username})', attributes=['distinguishedName'])
            if len(conn.entries) == 0:
                return jsonify({"error": "用户信息未找到"}), 404

            dn = conn.entries[0].distinguishedName.value
            user_ou = dn.split(',')[1].split('=')[1]

            if user_ou not in allowed_ou:
                return jsonify({"error": "用户不在允许的OU中"}), 403

            user = User(id=username, username=username)
            users[username] = user
            login_user(user)
            return jsonify({"message": "登录成功"}), 200
        except Exception as e:
            print(f"登录验证异常: {e}")
            return jsonify({"error": "内部服务器错误"}), 500
    return render_template('login.html')

步骤4:创建登录模板

在templates目录下创建login.html模板:

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>登录 - 权限管理系统</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">
    <div class="container mt-5">
        <div class="row justify-content-center">
            <div class="col-md-6">
                <div class="card">
                    <div class="card-header">
                        <h3 class="text-center">域账户登录</h3>
                    </div>
                    <div class="card-body">
                        <form id="loginForm" onsubmit="event.preventDefault(); login();">
                            <div class="mb-3">
                                <label for="username" class="form-label">用户名</label>
                                <input type="text" class="form-control" id="username" name="username" required>
                            </div>
                            <div class="mb-3">
                                <label for="password" class="form-label">密码</label>
                                <input type="password" class="form-control" id="password" name="password" required>
                            </div>
                            <button type="submit" class="btn btn-primary w-100">登录</button>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <script>
        function login() {
            const username = document.getElementById('username').value;
            const password = document.getElementById('password').value;
            const ldap_url = 'your-ldap-url';  // 替换为实际LDAP地址
            const allowed_ou = ['AllowedOU1', 'AllowedOU2'];  // 替换为实际允许的OU

            fetch('/login', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ username, password, ldap_url, allowed_ou })
            })
            .then(response => response.json())
            .then(data => {
                if (data.message) {
                    alert(data.message);
                    window.location.href = '/';
                } else {
                    alert(`登录失败: ${data.error}`);
                }
            })
            .catch(error => {
                alert('请求失败');
                console.error('错误:', error);
            });
        }
    </script>
</body>
</html>

2. 限制特定OU的访问

确保只有特定OU的用户可以登录访问

在登录验证过程中,通过检查用户的组织单位(OU)来限制访问权限。这需要在用户成功绑定到AD后,解析其distinguishedName以确定其所属的OU。

步骤1:解析用户的OU

在上述登录路由中,已经通过以下代码获取用户的OU:

dn = conn.entries[0].distinguishedName.value
user_ou = dn.split(',')[1].split('=')[1]

然后将其与允许的OU列表进行比较:

if user_ou not in allowed_ou:
    return jsonify({"error": "用户不在允许的OU中"}), 403

步骤2:保护路由

确保敏感路由仅对已登录且属于允许OU的用户开放:

@app.route('/')
@login_required
def index():
    return render_template('index.html', username=current_user.username)

3. 增加解锁用户账户功能

提供管理员功能以解锁被锁定的用户账户

为了实现解锁用户账户的功能,可以创建一个受保护的API端点,并调用PowerShell脚本来执行解锁操作。

步骤1:创建PowerShell脚本

编写一个PowerShell脚本powershell-UnLockUser.ps1,用于解锁用户账户:

param (
    [string]$SamAccountName
)

try {
    $user = Get-ADUser -Identity $SamAccountName -ErrorAction Stop
    if ($user) {
        Unlock-ADAccount -Identity $user.DistinguishedName
        Write-Output "成功解锁用户 $SamAccountName"
    } else {
        Write-Output "未找到用户 $SamAccountName"
    }
} catch {
    Write-Error "解锁用户失败: $_"
}

步骤2:在Flask中创建解锁用户的路由

@app.route('/unlock-user', methods=['POST'])
@login_required
def unlock_user():
    try:
        data = request.json
        samaccountname = data.get('samaccountname')

        if not samaccountname:
            return jsonify({"error": "缺少必要参数"}), 400

        # 仅允许管理员执行此操作
        if current_user.username not in admin_users:
            return jsonify({"error": "权限不足"}), 403

        unlock_command = f"powershell -File powershell-UnLockUser.ps1 -SamAccountName {samaccountname}"
        unlock_result = subprocess.run(
            ["powershell", "-File", "powershell-UnLockUser.ps1", "-SamAccountName", samaccountname],
            capture_output=True,
            text=True
        )

        if unlock_result.returncode == 0:
            return jsonify({"message": f"成功解锁用户 {samaccountname}"}), 200
        else:
            error_message = unlock_result.stderr.strip()
            if "ObjectNotFound" in error_message:
                return jsonify({"error": f"用户 {samaccountname} 不存在"}), 404
            else:
                return jsonify({"error": f"用户 {samaccountname} 解锁失败: {error_message}"}), 500

    except Exception as e:
        print(f"解锁用户失败: {e}")
        return jsonify({"error": "内部服务器错误"}), 500

步骤3:更新前端界面

在前端页面index.html中添加一个解锁用户的表单:

<!-- 解锁用户表单在现有表单容器外部 -->
<div class="form-container mt-5">
    <h2 class="text-center" style="color:#cc9900;">解锁用户账户</h2>
    <form id="unlockForm" onsubmit="event.preventDefault(); unlockUser();">
        <div class="mb-3">
            <label for="samaccountname" class="form-label">输入SAMAccountName:</label>
            <input type="text" id="samaccountname" name="samaccountname" class="form-control" placeholder="请输入SAMAccountName" required>
        </div>
        <button type="submit" class="btn btn-danger w-100">解锁用户</button>
    </form>
</div>
<script>
    function unlockUser() {
        const samaccountname = document.getElementById('samaccountname').value.trim();

        fetch('http://127.0.0.1:5000/unlock-user', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({ samaccountname })
        })
        .then(response => response.json())
        .then(data => {
            if (data.message) {
                alert(data.message);
            } else {
                alert(`操作失败: ${data.error}`);
            }
        })
        .catch(error => {
            alert('请求失败');
            console.error('错误:', error);
        });
    }
</script>

4. 页面布局优化

设计友好且功能齐全的用户界面

为了提升用户体验,建议将不同的功能模块进行分隔,并使用Bootstrap框架来实现响应式设计。

示例布局

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>共享文件夹权限管理</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
    <style>
        body {
            background-color: #f8f9fa;
            padding: 20px;
        }
        h1 {
            margin-bottom: 20px;
        }
        .form-container {
            max-width: 700px;
            margin: auto;
            background: white;
            padding: 30px;
            border-radius: 10px;
            box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
        }
        .btn-submit {
            background-color: #28a745;
            color: white;
        }
        .btn-submit:hover {
            background-color: #218838;
        }
        .form-select {  
            height: 150px; /* 设置自定义高度 */  
            overflow-y: auto; /* 超出时显示滚动条 */  
        }
    </style>
</head>
<body>
    <div class="form-container">
        <h1 class="text-center" style="color:#cc9900;">共享文件夹权限管理</h1>
        <form id="permissionForm" onsubmit="event.preventDefault(); submitForm();">
            <div class="mb-3">
                <label for="folders" class="form-label">选择共享文件夹 (可多选):</label>
                <select id="folders" name="folders" class="form-select" multiple required>
                    <option value="" disabled>请选择共享文件夹</option>
                </select>
                <div class="form-text">按住 Ctrl 键 (Windows) 或 Command 键 (Mac) 进行多选。</div>
            </div>
            <div class="mb-3">
                <label for="username" class="form-label">输入用户名(多个用逗号分隔):</label>
                <input type="text" id="username" name="username" class="form-control" placeholder="例如:zuokunlin, anda, zhuoke" required>
            </div>
            <div class="mb-3">
                <label class="form-label">选择权限类型:</label>
                <div class="form-check">
                    <input type="radio" id="readOnly" name="permission" value="ReadOnly" class="form-check-input" required>
                    <label for="readOnly" class="form-check-label">只读</label>
                </div>
                <div class="form-check">
                    <input type="radio" id="readWrite" name="permission" value="ReadWrite" class="form-check-input" required>
                    <label for="readWrite" class="form-check-label">读写</label>
                </div>
            </div>
            <button type="submit" class="btn btn-submit w-100">提交</button>
        </form>
    </div>
    
    <!-- 解锁用户表单 -->
    <div class="form-container mt-5">
        <h2 class="text-center" style="color:#cc9900;">解锁用户账户</h2>
        <form id="unlockForm" onsubmit="event.preventDefault(); unlockUser();">
            <div class="mb-3">
                <label for="samaccountname" class="form-label">输入SAMAccountName:</label>
                <input type="text" id="samaccountname" name="samaccountname" class="form-control" placeholder="请输入SAMAccountName" required>
            </div>
            <button type="submit" class="btn btn-danger w-100">解锁用户</button>
        </form>
    </div>
    
    <script>
        // 页面加载时请求共享文件夹路径
        window.onload = function() {
            fetch('/get-folders')
                .then(response => response.json())
                .then(folders => {
                    const folderSelect = document.getElementById('folders');
                    folders.forEach(folder => {
                        const option = document.createElement('option');
                        option.value = folder;
                        option.textContent = folder;
                        folderSelect.appendChild(option);
                    });
                })
                .catch(error => console.error('加载共享路径失败:', error));
        };
        
        // 提交权限表单
        function submitForm() {
            const folderSelect = document.getElementById('folders');
            const selectedOptions = Array.from(folderSelect.selectedOptions).map(option => option.value);
            const usernames = document.getElementById('username').value.split(',').map(user => user.trim());
            const permission = document.querySelector('input[name="permission"]:checked').value;
            
            fetch('/add-permission', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({ folders: selectedOptions, usernames: usernames, permission: permission })
            })
            .then(response => {
                if (!response.ok) {
                    return response.json().then(data => {
                        throw new Error(data.error || '未知错误');
                    });
                }
                return response.json();
            })
            .then(data => {
                alert(data.message);
            })
            .catch(error => {
                alert(`操作失败: ${error.message}`);
                console.error('请求失败:', error);
            });
        }
        
        // 提交解锁用户表单
        function unlockUser() {
            const samaccountname = document.getElementById('samaccountname').value.trim();

            fetch('/unlock-user', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({ samaccountname })
            })
            .then(response => response.json())
            .then(data => {
                if (data.message) {
                    alert(data.message);
                } else {
                    alert(`操作失败: ${data.error}`);
                }
            })
            .catch(error => {
                alert('请求失败');
                console.error('错误:', error);
            });
        }
    </script>
</body>
</html>

5. 后期功能扩展建议

模块化与角色管理

为了便于未来功能的扩展,建议将代码进行模块化,并引入角色管理机制:

步骤1:使用Flask蓝图模块化代码

将不同的功能划分到不同的蓝图中,如认证(auth)、权限管理(permissions)、管理员功能(admin)等:

from flask import Blueprint

auth_bp = Blueprint('auth', __name__)
permissions_bp = Blueprint('permissions', __name__)
admin_bp = Blueprint('admin', __name__)

# 在各自的蓝图中定义路由
# 例如,在auth.py中定义认证相关路由

步骤2:实现角色基础的访问控制(RBAC)

定义不同的角色,如管理员(admin)、普通用户(user),并根据角色限制访问权限:

from functools import wraps
from flask_login import current_user

def role_required(role):
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            if not current_user.is_authenticated or current_user.role != role:
                return jsonify({"error": "权限不足"}), 403
            return f(*args, **kwargs)
        return decorated_function
    return decorator

# 使用示例
@app.route('/admin-dashboard')
@login_required
@role_required('admin')
def admin_dashboard():
    return render_template('admin_dashboard.html')

步骤3:使用环境变量管理敏感配置

将敏感信息如LDAP服务器地址、密钥等存储在环境变量中,避免硬编码:

import os

app.secret_key = os.getenv('SECRET_KEY', 'default_secret')
ldap_url = os.getenv('LDAP_URL', 'your-ldap-url')
allowed_ous = os.getenv('ALLOWED_OUS', 'AllowedOU1,AllowedOU2').split(',')

步骤4:添加日志记录与监控

实现日志记录,监控用户的登录、权限变更等操作:

import logging

logging.basicConfig(filename='app.log', level=logging.INFO,
                    format='%(asctime)s %(levelname)s:%(message)s')

@app.route('/login', methods=['POST'])
def login():
    # 登录逻辑
    if success:
        logging.info(f"用户 {username} 登录成功")
    else:
        logging.warning(f"用户 {username} 登录失败")

步骤5:增强安全措施

  • 启用HTTPS:确保所有数据传输通过加密连接进行。
  • 输入验证:验证和清理所有用户输入,防止注入攻击。
  • 错误处理:避免向用户泄露敏感错误信息。

6. 参考资料

名称 链接
Flask-Login官方文档 https://flask-login.readthedocs.io/en/latest/
LDAP3库文档 https://ldap3.readthedocs.io/
Flask-Blueprints详解 https://flask.palletsprojects.com/en/2.3.x/blueprints/
Flask官方文档 https://flask.palletsprojects.com/en/2.3.x/

结论

通过集成Active Directory认证、限制特定OU的访问权限,并实现诸如用户账户解锁等功能,可以显著提升Flask应用的安全性与管理效率。模块化设计与角色管理机制的引入,为未来功能扩展奠定了坚实的基础。同时,遵循最佳安全实践,如启用HTTPS和完善的日志记录,确保系统的持续稳定与安全。


Last updated January 13, 2025

AI-assisted research. Sources, methodology and corrections · Source-checked guides and tools · Explore published answers

Ask Ithy AI