增强Flask应用的域账户登录与权限管理
实现安全的用户认证与权限控制,优化用户体验
关键要点
- 集成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