Flask-Login的信号和应用

本文介绍了如何安装并使用Flask-Login及其依赖binker来处理用户登录的信号。通过订阅user_logged_in信号,可以在用户登录时执行特定操作,并利用user_loader检查用户的登录状态。

安装blinker、flask_login

 pip install -i https://pypi.tuna.tsinghua.edu.cn/simple flask_login

 pip install -i https://pypi.tuna.tsinghua.edu.cn/simple blinker 

user_logged_in是signal实例

创建login_manager,继承lask_login.UserMixin的一些属性

app = Flask(__name__)
app.secret_key = 'super secret string'
app.config['SQLALCHEMY_DATABASE_URI'] = SQLALCHEMY_DATABASE_URI
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False


db = SQLAlchemy(app)

login_manager = flask_login.LoginManager()
login_manager.init_app(app)


password = '123'


class User(flask_login.UserMixin, db.Model):
    __tablename__ = 'login_users'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(128), nullable=False)
    login_count = db.Column(db.Integer, default=0)
    last_login_ip = db.Column(db.String(128), default='unknown')


db.create_all()

 

订阅信号

@flask_login.user_logged_in.connect_via(app)#信号订阅user_logged_in.connect(fn)
def _track_logins(sender, user, **extra):
    print("_track_logins")
    user.login_count += 1
    user.last_login_ip = request.remote_addr
    db.session.add(user)
    db.session.commit()

 

发送信号并且记录用户用于后续判断用户状态

@app.route('/login', methods=['GET', 'POST'])
def login():
    print("login")
    if request.method == 'GET':
        return '''
<form action='login' method='POST'>
    <input type='text' name='name' id='name' placeholder='name'></input>
    <input type='password' name='pw' id='pw' placeholder='password'></input>
    <input type='submit' name='submit'></input>
</form>
               '''

    name = request.form.get('name')
    if request.form.get('pw') == password:
        user = User.query.filter_by(name=name).first()
        if not user:
            user = User(name=name)
            db.session.add(user)
            db.session.commit()
        # 信号发送user_logged_in.send
        # 提供给user_loader判断用户是否在登陆状态
        flask_login.login_user(user)
        return redirect(url_for('protected'))

内部实现发送信号的代码:

def login_user(user, remember=False, duration=None, force=False, fresh=True):
    '''
    Logs a user in. You should pass the actual user object to this. If the
    user's `is_active` property is ``False``, they will not be logged in
    unless `force` is ``True``.

    This will return ``True`` if the log in attempt succeeds, and ``False`` if
    it fails (i.e. because the user is inactive).

    :param user: The user object to log in.
    :type user: object
    :param remember: Whether to remember the user after their session expires.
        Defaults to ``False``.
    :type remember: bool
    :param duration: The amount of time before the remember cookie expires. If
        ``None`` the value set in the settings is used. Defaults to ``None``.
    :type duration: :class:`datetime.timedelta`
    :param force: If the user is inactive, setting this to ``True`` will log
        them in regardless. Defaults to ``False``.
    :type force: bool
    :param fresh: setting this to ``False`` will log in the user with a session
        marked as not "fresh". Defaults to ``True``.
    :type fresh: bool
    '''
    if not force and not user.is_active:
        return False

    user_id = getattr(user, current_app.login_manager.id_attribute)()
    session['user_id'] = user_id
    session['_fresh'] = fresh
    session['_id'] = current_app.login_manager._session_identifier_generator()

    if remember:
        session['remember'] = 'set'
        if duration is not None:
            try:
                # equal to timedelta.total_seconds() but works with Python 2.6
                session['remember_seconds'] = (duration.microseconds +
                                               (duration.seconds +
                                                duration.days * 24 * 3600) *
                                               10**6) / 10.0**6
            except AttributeError:
                raise Exception('duration must be a datetime.timedelta, '
                                'instead got: {0}'.format(duration))

    _request_ctx_stack.top.user = user
    user_logged_in.send(current_app._get_current_object(), user=_get_user())
    return True

user_loader根据login_user记录的用户检查相关登陆状态等信息

@login_manager.user_loader
def user_loader(id):
    print("user_loader")
    user = User.query.filter_by(id=id).first()
    return user

完整的代码:

# coding=utf-8
from flask import Flask, request, redirect, url_for
import flask_login
from flask_sqlalchemy import SQLAlchemy

DB_HOSTNAME = 'localhost'
DATABASE_NAME = 'r'
DB_USERNAME = 'web'
DB_PASSWORD = 'web'
DB_PORT = 3306
# 'mysql://{}:{}@{}/{}'.format(
#     USERNAME, PASSWORD, HOSTNAME, DATABASE)
SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://{}:{}@{}:{}/{}?charset=utf8'.format(DB_USERNAME, DB_PASSWORD, DB_HOSTNAME, DB_PORT, DATABASE_NAME)

app = Flask(__name__)
app.secret_key = 'super secret string'
app.config['SQLALCHEMY_DATABASE_URI'] = SQLALCHEMY_DATABASE_URI
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False


db = SQLAlchemy(app)

login_manager = flask_login.LoginManager()
login_manager.init_app(app)


password = '123'


class User(flask_login.UserMixin, db.Model):
    __tablename__ = 'login_users'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(128), nullable=False)
    login_count = db.Column(db.Integer, default=0)
    last_login_ip = db.Column(db.String(128), default='unknown')


db.create_all()


@flask_login.user_logged_in.connect_via(app)#信号订阅user_logged_in.connect(fn)
def _track_logins(sender, user, **extra):
    print("_track_logins")
    user.login_count += 1
    user.last_login_ip = request.remote_addr
    db.session.add(user)
    db.session.commit()


@login_manager.user_loader
def user_loader(id):
    print("user_loader")
    user = User.query.filter_by(id=id).first()
    return user


@app.route('/login', methods=['GET', 'POST'])
def login():
    print("login")
    if request.method == 'GET':
        return '''
<form action='login' method='POST'>
    <input type='text' name='name' id='name' placeholder='name'></input>
    <input type='password' name='pw' id='pw' placeholder='password'></input>
    <input type='submit' name='submit'></input>
</form>
               '''

    name = request.form.get('name')
    if request.form.get('pw') == password:
        user = User.query.filter_by(name=name).first()
        if not user:
            user = User(name=name)
            db.session.add(user)
            db.session.commit()
        # 信号发送user_logged_in.send
        # 提供给user_loader判断用户是否在登陆状态
        flask_login.login_user(user)
        return redirect(url_for('protected'))

    return 'Bad login!'


@app.route('/protected')
@flask_login.login_required
def protected():
    print("protected")
    user = flask_login.current_user
    return 'Logged in as: {}| Login_count: {}|IP: {}'.format(
        user.name, user.login_count, user.last_login_ip)


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=9001, debug=True)

 

<think>我们参考引用[4]中提到:在登录成功后进行socket连接,监听异地登录提示消息。结合flask-login,我们需要在用户登录时记录登录信息,并在检测到异地登录时通过WebSocket推送消息给客户端。 步骤: 1. 在用户登录时,记录用户的登录信息(如IP地址、设备信息、登录时间等)。 2. 当用户再次登录时,比对当前登录信息与上次记录的信息,如果发现异地登录(如IP变化),则触发通知。 3. 使用WebSocket(例如Flask-SocketIO)在用户登录后建立连接,并监听异地登录事件。 4. 当检测到异地登录时,向后端发送通知,后端通过WebSocket向客户端推送消息。 具体实现: 1. 扩展用户模型,存储会话信息(如最近登录的IP会话标识)。 2. 在登录视图函数中,检查当前登录的IP是否与上次登录的IP不同(或根据需求检查其他信息),如果是异地登录,则记录并触发事件。 3. 利用WebSocket,在用户登录后建立连接,并在异地登录事件发生时,向该用户的所有会话(除了新登录的这个)发送退出通知。 注意:flask-login本身不提供异地登录检测,我们需要自己实现。同时,为了实时通知,我们需要使用WebSocket。 参考引用[5]中提到了Flask信号,我们可以利用信号机制在用户登录时触发事件,但这里我们更需要的是在检测到异地登录时主动通知之前登录的会话。 实现流程: a. 用户A在设备1登录,记录设备1的IP生成的会话标识(可以用token或session id),并将这个会话标识与用户A关联(可以存储在数据库或Redis中)。 b. 用户A在设备2登录,此时检测到设备2的IP与设备1不同(或者其他条件满足异地登录),则触发异地登录事件。 c. 在异地登录事件中,我们需要向设备1发送通知(通过WebSocket),要求设备1退出登录。 d. 设备1收到通知后,前端提示用户并跳转到登录页。 代码结构示例: 1. 用户登录视图: - 验证用户 - 生成access_token(用于JWT)或者使用session - 检查当前登录IP是否在已有的活跃会话中(从存储的会话信息中查找) - 如果不在,则认为是异地登录,触发事件(同时记录新的会话信息) 2. 使用Flask-SocketIO建立WebSocket连接: - 在用户登录后,前端建立WebSocket连接,并监听特定事件(如'force_logout') - 后端在触发异地登录事件时,向该用户的其他所有会话发送'force_logout'事件 3. 存储会话信息:可以使用Redis或数据库。每个用户对应多个会话(每个设备一个会话)。存储结构例如: user_id: { session_id1: {ip: 'xxx', login_time: ..., device_info: ...}, session_id2: {ip: 'yyy', login_time: ..., device_info: ...} } 4. 当用户退出时,删除对应的会话信息。 考虑到引用[3]中的注册登录示例,我们可以扩展登录接口: 示例代码(关键部分): ```python from flask import Flask, request, jsonify, session from flask_socketio import SocketIO, emit from flask_jwt_extended import JWTManager, create_access_token, jwt_required, get_jwt_identity from flask_login import LoginManager, UserMixin, login_user, current_user, logout_user app = Flask(__name__) app.config['SECRET_KEY'] = 'your_secret_key' socketio = SocketIO(app) login_manager = LoginManager(app) # 模拟用户存储 users_db = {} # 存储用户的活跃会话,格式:{user_id: [session_info1, session_info2]} active_sessions = {} class User(UserMixin): pass @login_manager.user_loader def load_user(user_id): if user_id in users_db: user = User() user.id = user_id return user return None @app.route('/login', methods=['POST']) def login(): data = request.get_json() username = data.get('username') password = data.get('password') # 用户验证 if username in users_db and check_password_hash(users_db[username]['password_hash'], password): user = User() user.id = username # 获取客户端IP client_ip = request.remote_addr # 设备信息(简单示例,实际可以从User-Agent解析) user_agent = request.headers.get('User-Agent', 'unknown') # 创建新会话信息 new_session = { 'ip': client_ip, 'user_agent': user_agent, 'login_time': datetime.now(), 'session_id': None # 如果是使用session,可以使用session.sid,如果是JWT,可以用token的jti } # 检查是否为异地登录:这里简单比较IP,实际可以更复杂(比如城市) if username in active_sessions: # 检查是否有其他会话 for sess in active_sessions[username]: if sess['ip'] != client_ip: # 触发异地登录事件:通知之前的会话 # 这里需要能够定位到之前的WebSocket连接,可以通过为每个会话存储一个sid(SocketIO的会话ID)来实现 # 实际应用中,我们需要在用户建立WebSocket连接时记录该用户的socketio会话ID(sid)与其用户会话的关联 socketio.emit('force_logout', {'reason': '异地登录'}, room=sess['socketio_sid']) # 记录新会话 # 注意:我们还没有socketio_sid,这个需要在建立WebSocket连接时添加 # 所以这里先记录,在WebSocket连接建立后再更新该会话的socketio_sid if username not in active_sessions: active_sessions[username] = [] active_sessions[username].append(new_session) # 登录用户 login_user(user) # 生成token或session(这里使用session) # 如果是JWT,可以生成token,并将会话信息与jti关联 access_token = create_access_token(identity=username) return jsonify(access_token=access_token), 200 else: return jsonify({"msg": "Bad username or password"}), 401 # WebSocket连接建立 @socketio.on('connect') def handle_connect(): if current_user.is_authenticated: # 将当前WebSocket连接的sid与用户的会话关联 # 我们需要找到用户最近添加的那个还没有sid的会话(或者通过其他方式匹配) # 这里简单处理:将用户当前所有会话中最后一个没有sid的会话设置为当前sid user_id = current_user.id if user_id in active_sessions: for session_info in active_sessions[user_id]: if 'socketio_sid' not in session_info: session_info['socketio_sid'] = request.sid break # 将当前socketio连接加入房间(可以按用户分组,方便广播) # 也可以不需要房间,因为我们存储了每个会话的sid # 这里我们直接存储了每个会话的sid,所以通知时可以直接指定sid # 当客户端断开连接时,移除会话 @socketio.on('disconnect') def handle_disconnect(): if current_user.is_authenticated: user_id = current_user.id # 从活跃会话中移除该sid对应的会话 if user_id in active_sessions: # 找到该sid对应的会话 active_sessions[user_id] = [sess for sess in active_sessions[user_id] if sess.get('socketio_sid') != request.sid] if not active_sessions[user_id]: del active_sessions[user_id] # 当用户主动退出时,清除会话 @app.route('/logout') @jwt_required() # 如果使用JWT def logout(): user_id = get_jwt_identity() # 清除当前会话:需要知道当前会话的sid(在WebSocket连接中存储了,但这里没有直接关联) # 另一种做法:在登录时生成一个会话ID(token jti)并存储,退出时删除。 # 由于我们存储了活跃会话,并且在WebSocket连接时绑定了sid,我们可以通过当前请求无法直接获得WebSocket sid,所以需要在退出时通知前端断开WebSocket,然后在前端断开时触发清除。 # 或者我们可以在用户退出时,从active_sessions中删除该用户所有会话(这样会退出所有设备),或者只删除当前会话? # 这里我们简化:只删除当前会话(需要知道当前会话标识) # 由于我们使用JWT,可以在token中存储一个会话ID(jti),并在存储活跃会话时使用这个jti作为键。然后在退出时删除该jti对应的会话。 # 但上面的代码并没有存储jti,所以需要调整。 # 由于时间关系,这里不再展开,实际实现需要根据认证方式仔细设计。 return jsonify({"msg": "Logged out"}), 200 ``` 注意:上述代码是一个简化的示例,实际应用中需要更严谨的设计,例如: - 会话存储的线程安全(使用数据库或Redis) - 会话的唯一标识生命周期管理 - 更精确的异地登录判断(如地理位置) - WebSocket连接的安全性认证 另外,异地登录提示也可以不强制退出,而是发送警告消息,由用户决定是否退出其他会话。 相关问题: §§相关问题§§ 1. 如何安全地存储用户会话信息? 2. 除了IP地址,还有哪些方法可以检测异地登录? 3. 如何在前端使用WebSocket接收异地登录通知并处理? 4. Flask-SocketIO如何与Flask-Login集成实现认证? </think>### 使用Flask-Login实现异地登录提示的方法 要实现异地登录提示,核心思路是**记录用户登录设备/位置信息,并通过WebSocket实时推送通知**。以下是具体实现步骤: #### 1. **记录登录信息** 在用户登录时,记录设备/IP信息到数据库: ```python from flask_login import current_user from datetime import datetime @app.route('/login', methods=['POST']) def login(): # ...验证逻辑... if user_valid: login_user(user) # 记录登录信息 new_login = UserLogin( user_id=user.id, ip_address=request.remote_addr, # 获取客户端IP user_agent=request.headers.get('User-Agent'), # 获取设备信息 login_time=datetime.utcnow() ) db.session.add(new_login) db.session.commit() ``` #### 2. **异地登录检测** 在登录验证时对比历史记录: ```python @app.route('/login', methods=['POST']) def login(): # ...验证逻辑... if user_valid: # 检查最近登录设备 recent_login = UserLogin.query.filter_by(user_id=user.id).order_by(UserLogin.login_time.desc()).first() if recent_login and (recent_login.ip_address != request.remote_addr): # 触发异地登录通知 emit_force_logout(user.id, request.remote_addr) # 见步骤3 ``` #### 3. **WebSocket实时通知** 使用Flask-SocketIO推送通知: ```python from flask_socketio import SocketIO, emit socketio = SocketIO(app) # 用户连接时绑定session @socketio.on('connect') def handle_connect(): if current_user.is_authenticated: join_room(current_user.id) # 按用户ID分组 # 推送异地登录警告 def emit_force_logout(user_id, new_ip): message = f"您的账号在{new_ip}登录,被迫下线!" socketio.emit('force_logout', {'reason': message}, room=str(user_id)) # 推送给该用户所有会话 ``` #### 4. **前端处理**(Vue示例) ```javascript // 建立WebSocket连接 const socket = io.connect('https://your-domain.com') // 监听强制退出事件 socket.on('force_logout', (data) => { alert(data.reason) router.push('/login') // 跳转到登录页 }) ``` #### 5. **会话管理优化** ```python from flask_login import user_logged_in @user_logged_in.connect_via(app) def track_logins(sender, user, **extra): # 限制最大会话数 (例如最多3个设备) active_sessions = UserLogin.query.filter_by(user_id=user.id).count() if active_sessions > 3: oldest_session = UserLogin.query.filter_by(user_id=user.id).order_by(UserLogin.login_time).first() db.session.delete(oldest_session) ``` ### 关键技术点说明: 1. **设备识别**:通过`request.remote_addr`获取IP,结合`User-Agent`识别设备[^1][^3] 2. **实时通信**:使用SocketIO实现服务端主动推送[^4] 3. **会话管理**: - 登录时生成唯一会话ID - 数据库存储设备/IP/登录时间 - 登出时清理会话记录 4. **安全性**: - 敏感操作需二次验证 - WebSocket连接使用JWT认证 > **注意**:生产环境需添加IP地理定位服务(如GeoIP2),提高异地判断准确性[^3]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值