MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Python使用Flask进行用户认证与授权

2024-07-087.8k 阅读

一、Flask简介

Flask 是一个轻量级的 Python Web 应用框架,由 Armin Ronacher 开发。它基于 Werkzeug WSGI 工具包和 Jinja2 模板引擎。Flask 被设计为易于扩展和定制,非常适合快速构建小型到中型规模的 Web 应用。与一些大型的框架(如 Django)相比,Flask 核心功能简洁,开发者可以根据项目需求灵活选择各种扩展。

1.1 Flask 安装

在开始使用 Flask 之前,需要先安装它。如果已经安装了 Python 和 pip,可以通过以下命令进行安装:

pip install flask

这将从 Python Package Index(PyPI)下载并安装最新版本的 Flask。

1.2 一个简单的Flask应用

以下是一个最基本的 Flask 应用示例,用于展示 Flask 的简单使用:

from flask import Flask

app = Flask(__name__)


@app.route('/')
def hello_world():
    return 'Hello, World!'


if __name__ == '__main__':
    app.run()

在上述代码中,首先导入 Flask 类。然后创建一个 Flask 应用实例 app@app.route('/') 是一个装饰器,它将根路径('/')映射到 hello_world 函数。当访问应用的根路径时,hello_world 函数会被调用并返回 'Hello, World!'app.run() 用于启动 Flask 应用,默认在 127.0.0.1:5000 运行。

二、用户认证基础概念

在深入探讨使用 Flask 进行用户认证与授权之前,我们需要先理解一些基本概念。

2.1 认证(Authentication)

认证是验证用户身份的过程。在 Web 应用中,这通常意味着用户提供凭据(如用户名和密码),应用验证这些凭据是否与存储在数据库或其他存储中的信息匹配。如果匹配,则用户被认证,应用知道该用户是谁。

2.2 授权(Authorization)

授权是确定已认证用户是否有权执行特定操作的过程。例如,只有管理员用户可能有权删除其他用户的账户,而普通用户没有此权限。授权通常基于角色(如管理员、普通用户)或特定的权限集来进行判断。

2.3 常用认证方式

  1. 基本认证(Basic Authentication):基本认证是一种简单的认证方式,用户在每次请求时将用户名和密码以 Base64 编码的形式包含在请求头中。虽然简单,但安全性较低,因为用户名和密码在每次请求时都被发送,且 Base64 编码很容易被解码。
  2. 表单认证(Form - Based Authentication):这是一种常见的认证方式,用户通过 HTML 表单输入用户名和密码,提交后服务器验证凭据。如果验证成功,服务器通常会在用户的浏览器中设置一个会话(session),后续请求可以通过这个会话来识别用户。
  3. Token - 基于认证(Token - Based Authentication):在这种方式下,用户认证成功后,服务器会生成一个令牌(token),并返回给用户。用户在后续请求中,将这个 token 包含在请求头或其他位置发送给服务器。服务器验证 token 的有效性来确定用户身份。这种方式在现代 Web 应用尤其是前后端分离的架构中非常流行,因为 token 可以在不同的服务之间共享,且不需要在服务器端存储过多的会话信息。

三、Flask 中的用户认证实现

3.1 使用 Flask - HTTPBasicAuth 实现基本认证

Flask - HTTPBasicAuth 是一个用于在 Flask 应用中实现基本认证的扩展。

3.1.1 安装 Flask - HTTPBasicAuth

可以使用 pip 进行安装:

pip install flask - httpbasicauth

3.1.2 代码示例

from flask import Flask
from flask_httpbasicauth import HTTPBasicAuth

app = Flask(__name__)
auth = HTTPBasicAuth()

# 模拟用户数据库
users = {
    "admin": "password123",
    "user": "userpass"
}


@auth.verify_password
def verify_password(username, password):
    if username in users and users[username] == password:
        return True
    return False


@app.route('/protected')
@auth.login_required
def protected():
    return 'This is a protected resource'


if __name__ == '__main__':
    app.run()

在上述代码中,首先导入 FlaskHTTPBasicAuth。创建 Flask 应用实例 appHTTPBasicAuth 实例 auth。定义一个模拟的用户数据库 users@auth.verify_password 装饰的函数 verify_password 用于验证用户名和密码。@app.route('/protected') 装饰的函数 protected 是一个受保护的路由,@auth.login_required 确保只有认证通过的用户才能访问该路由。

3.2 使用 Flask - Login 实现表单认证

Flask - Login 是一个用于在 Flask 应用中处理用户登录状态的扩展,非常适合实现表单认证。

3.2.1 安装 Flask - Login

通过 pip 安装:

pip install flask - login

3.2.2 数据库模型准备

假设使用 SQLite 数据库和 Flask - SQLAlchemy 来管理用户数据。首先安装 Flask - SQLAlchemy

pip install flask - sqlalchemy

以下是定义用户模型的代码:

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import UserMixin

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] ='sqlite:///users.db'
db = SQLAlchemy(app)


class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    password = db.Column(db.String(120), nullable=False)


db.create_all()

在上述代码中,创建 Flask 应用实例 app,配置 SQLAlchemy 使用 SQLite 数据库。定义 User 类,它继承自 UserMixindb.ModelUserMixin 为用户模型提供了一些默认的方法,如 is_authenticatedis_activeis_anonymousget_id,这些方法在 Flask - Login 中很有用。

3.2.3 认证相关代码

from flask import Flask, render_template, request, redirect, url_for
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] ='sqlite:///users.db'
app.secret_key = 'your_secret_key'
db = SQLAlchemy(app)
login_manager = LoginManager()
login_manager.init_app(app)


class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    password = db.Column(db.String(120), nullable=False)


@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))


@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        user = User.query.filter_by(username=username, password=password).first()
        if user:
            login_user(user)
            return redirect(url_for('protected'))
        else:
            return 'Invalid credentials'
    return render_template('login.html')


@app.route('/logout')
@login_required
def logout():
    logout_user()
    return redirect(url_for('login'))


@app.route('/protected')
@login_required
def protected():
    return 'This is a protected page'


if __name__ == '__main__':
    db.create_all()
    app.run()

在上述代码中,LoginManager 实例 login_manager 被初始化并关联到应用。@login_manager.user_loader 装饰的函数 load_user 用于从数据库中加载用户对象。/login 路由处理用户登录,验证用户名和密码,如果正确则使用 login_user 登录用户并重定向到受保护页面,否则返回错误信息。/logout 路由使用 logout_user 注销用户并重定向到登录页面。/protected 路由是一个受保护的页面,只有登录用户可以访问。

四、Flask 中的用户授权实现

4.1 基于角色的授权

基于角色的授权是一种常见的授权方式,不同角色(如管理员、普通用户)具有不同的权限。

4.1.1 数据库模型扩展

在前面的用户模型基础上,添加角色字段:

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import UserMixin

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] ='sqlite:///users.db'
db = SQLAlchemy(app)


class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    password = db.Column(db.String(120), nullable=False)
    role = db.Column(db.String(50), nullable=False, default='user')


db.create_all()

4.1.2 授权代码示例

from flask import Flask, render_template, request, redirect, url_for
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] ='sqlite:///users.db'
app.secret_key = 'your_secret_key'
db = SQLAlchemy(app)
login_manager = LoginManager()
login_manager.init_app(app)


class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    password = db.Column(db.String(120), nullable=False)
    role = db.Column(db.String(50), nullable=False, default='user')


@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))


@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        user = User.query.filter_by(username=username, password=password).first()
        if user:
            login_user(user)
            return redirect(url_for('protected'))
        else:
            return 'Invalid credentials'
    return render_template('login.html')


@app.route('/logout')
@login_required
def logout():
    logout_user()
    return redirect(url_for('login'))


@app.route('/admin - only')
@login_required
def admin_only():
    user = current_user
    if user.role!= 'admin':
        return 'Access denied'
    return 'This is an admin - only page'


@app.route('/protected')
@login_required
def protected():
    return 'This is a protected page'


if __name__ == '__main__':
    db.create_all()
    app.run()

在上述代码中,/admin - only 路由是一个仅管理员可访问的页面。在访问该路由时,首先获取当前用户 current_user,检查其角色是否为 admin,如果不是则返回 Access denied,否则显示页面内容。

4.2 基于权限的授权

基于权限的授权比基于角色的授权更细粒度,每个操作都可以对应一个权限。

4.2.1 数据库模型设计

需要创建权限表和用户权限关联表:

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import UserMixin

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] ='sqlite:///users.db'
db = SQLAlchemy(app)


class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    password = db.Column(db.String(120), nullable=False)


class Permission(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50), unique=True, nullable=False)


class UserPermission(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
    permission_id = db.Column(db.Integer, db.ForeignKey('permission.id'))


db.create_all()

4.2.2 授权代码示例

from flask import Flask, render_template, request, redirect, url_for
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] ='sqlite:///users.db'
app.secret_key = 'your_secret_key'
db = SQLAlchemy(app)
login_manager = LoginManager()
login_manager.init_app(app)


class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    password = db.Column(db.String(120), nullable=False)


class Permission(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50), unique=True, nullable=False)


class UserPermission(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
    permission_id = db.Column(db.Integer, db.ForeignKey('permission.id'))


@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))


@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        user = User.query.filter_by(username=username, password=password).first()
        if user:
            login_user(user)
            return redirect(url_for('protected'))
        else:
            return 'Invalid credentials'
    return render_template('login.html')


@app.route('/logout')
@login_required
def logout():
    logout_user()
    return redirect(url_for('login'))


@app.route('/create - post', methods=['GET', 'POST'])
@login_required
def create_post():
    has_permission = Permission.query.join(UserPermission).filter(
        UserPermission.user_id == current_user.id,
        Permission.name == 'create_post'
    ).first()
    if not has_permission:
        return 'Access denied'
    # 处理创建文章的逻辑
    return 'Create post page'


@app.route('/protected')
@login_required
def protected():
    return 'This is a protected page'


if __name__ == '__main__':
    db.create_all()
    app.run()

在上述代码中,/create - post 路由检查当前用户是否具有 create_post 权限。通过查询 PermissionUserPermission 表的关联来判断用户是否有此权限,如果没有则返回 Access denied

五、安全考虑

5.1 密码存储安全

在处理用户认证时,密码存储的安全性至关重要。绝不能以明文形式存储密码,应该使用密码哈希函数。在 Python 中,可以使用 bcrypt 库。

5.1.1 安装 bcrypt

pip install bcrypt

5.1.2 密码哈希存储示例

import bcrypt
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import UserMixin

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] ='sqlite:///users.db'
db = SQLAlchemy(app)


class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    password_hash = db.Column(db.String(120), nullable=False)

    def set_password(self, password):
        self.password_hash = bcrypt.hashpw(password.encode('utf - 8'), bcrypt.gensalt())

    def check_password(self, password):
        return bcrypt.checkpw(password.encode('utf - 8'), self.password_hash)


db.create_all()

在上述代码中,User 类增加了 set_passwordcheck_password 方法。set_password 方法使用 bcrypt.hashpw 对密码进行哈希处理并存储哈希值,check_password 方法使用 bcrypt.checkpw 验证密码。

5.2 防止 CSRF 攻击

跨站请求伪造(CSRF)是一种常见的 Web 攻击,攻击者通过伪装成用户的合法请求来执行恶意操作。Flask - WTF 扩展可以帮助防止 CSRF 攻击。

5.2.1 安装 Flask - WTF

pip install flask - wtf

5.2.2 使用 CSRF 保护示例

from flask import Flask, render_template, request, redirect, url_for
from flask_wtf.csrf import CSRFProtect
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] ='sqlite:///users.db'
app.secret_key = 'your_secret_key'
csrf = CSRFProtect(app)
db = SQLAlchemy(app)
login_manager = LoginManager()
login_manager.init_app(app)


class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    password = db.Column(db.String(120), nullable=False)


@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))


@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        user = User.query.filter_by(username=username, password=password).first()
        if user:
            login_user(user)
            return redirect(url_for('protected'))
        else:
            return 'Invalid credentials'
    return render_template('login.html')


@app.route('/logout')
@login_required
def logout():
    logout_user()
    return redirect(url_for('login'))


@app.route('/protected')
@login_required
def protected():
    return 'This is a protected page'


if __name__ == '__main__':
    db.create_all()
    app.run()

在上述代码中,CSRFProtect 实例 csrf 被初始化并关联到应用。在模板中,需要包含 CSRF 令牌,Flask - WTF 会在请求处理时验证令牌,防止 CSRF 攻击。

六、结合前端实现认证与授权

在实际应用中,通常需要结合前端来实现用户认证与授权的交互。以下以使用 HTML 和 JavaScript 与 Flask 后端交互为例。

6.1 前端登录表单

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF - 8">
    <title>Login</title>
</head>

<body>
    <form action="{{ url_for('login') }}" method="post">
        <label for="username">Username:</label><br>
        <input type="text" id="username" name="username"><br>
        <label for="password">Password:</label><br>
        <input type="password" id="password" name="password"><br><br>
        <input type="submit" value="Login">
    </form>
</body>

</html>

上述 HTML 代码创建了一个简单的登录表单,表单提交到 Flask 后端的 login 路由。

6.2 使用 AJAX 进行认证

如果是前后端分离的应用,可以使用 AJAX 来处理认证。以下是一个简单的示例,使用 fetch API:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF - 8">
    <title>Login</title>
</head>

<body>
    <label for="username">Username:</label><br>
    <input type="text" id="username"><br>
    <label for="password">Password:</label><br>
    <input type="password" id="password"><br><br>
    <button onclick="login()">Login</button>
    <script>
        function login() {
            const username = document.getElementById('username').value;
            const password = document.getElementById('password').value;
            fetch('/login', {
                method: 'POST',
                headers: {
                    'Content - Type': 'application/json'
                },
                body: JSON.stringify({
                    username: username,
                    password: password
                })
            })
              .then(response => response.text())
              .then(data => {
                    if (data === 'Invalid credentials') {
                        alert('Invalid credentials');
                    } else {
                        alert('Login successful');
                    }
                });
        }
    </script>
</body>

</html>

在上述代码中,当点击登录按钮时,login 函数使用 fetch 发送一个 POST 请求到 /login 路由,携带用户名和密码。根据后端返回的响应进行相应的提示。

6.3 前端授权控制

在前端,可以根据用户的认证状态和授权信息来控制页面元素的显示。例如,只有管理员用户才能看到删除按钮:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF - 8">
    <title>Admin Page</title>
</head>

<body>
    {% if current_user.role == 'admin' %}
    <button onclick="deleteUser()">Delete User</button>
    {% endif %}
    <script>
        function deleteUser() {
            // 处理删除用户的 AJAX 请求逻辑
        }
    </script>
</body>

</html>

在上述代码中,使用 Flask 的 Jinja2 模板语法,根据 current_user.role 判断是否为管理员,如果是则显示删除按钮。

通过以上内容,详细介绍了如何在 Flask 应用中实现用户认证与授权,涵盖了基本概念、多种认证与授权方式、安全考虑以及与前端的结合等方面,希望能帮助开发者构建安全可靠的 Web 应用。