开发者

flask库中sessions.py的使用小结

目录
  • 1. Flask Session 的基本使用
    • (1) 启用 Session
    • (2) 存储和读取 Session
    • (3) Session 的持久性(Permanent Session)
  • 2. Flask Session 的工作原理
    • (1) 默认实现(SecureCookieSession)
    • (2) 如何修改 Session 存储方式?
  • 3. Session 的安全配置
    • (1) 安全相关的 Cookie 设置
    • (2) 防止 Session 劫持
  • 4. 常见问题
    • (1)RuntimeError: The session is unavailable because no secret key was set.
    • (2) Session 数据不持久
    • (3) 如何强制 Session 过期?
  • 5. 总结
    • Sessions.py
      • 1. 会话(Session)的核心类
        • (1)SessionMixin
        • (2)SecureCookieSession
      • 2. 会话接口(SessionInterface)
        • (1) 核心方法
        • (2) Cookie 配置方法
      • 3. 默认实现(SecureCookieSessionInterface)
        • (1) 安全特性
        • (2) 工作流程
        • (3) 配置示例
      • 4. 设计思想
        • 常见问题

        在 Flask 中,Session(会话) 是一种用于在不同请求之间存储用户数据的机制。Flask 的 Session 默认是基于 客户端 Cookie 的,但数据会经过加密签名,防止篡改(但内容本身是可读的,除非额外加密)。

        1. Flask Session 的基本使用

        (1) 启用 Session

        Flask 的 Session 基于 flask.session,它是一个类似字典的对象。使用时需要设置 SECRET_KEY(用于签名 Session 数据,防止篡改):

        from flask import Flask, session
        
        app = Flask(__name__)
        app.secret_key = 'your-secret-key-here'  # 必须设置,否则 Session 无法工作

        (2) 存储和读取 Session

        from flask import Flask, session, request, redirect, url_for
        
        app = Flask(__name__)
        app.secret_key = 'your-secret-key'
        
        @app.route('/login')
        def login():
            session['username'] = 'admin'  # 存储 Session
            session['logged_in'] = True    # Session 可以存储任意可序列化的数据
            return "Logged in!"
        
        @app.route('/dashboard')
        def dashboard():
            if session.get('logged_in'):
                return f"Welcome, {session['username']}!"
            else:
                return redirect(url_for('login'))
        
        @app.route('/logout')
        def logout():
            session.pop('username', None)  # 移除 Session 中的某个键
            session.clear()                # 清空整个 Session
            return "Logged out!"

        (3) Session 的持久性(Permanent Session)

        默认情况下,Session 在浏览器关闭后失效。如果要让 Session 长期有效(基于 PERMANENT_SESSION_LIFETIME 配置):

        from datetime import timedelta
        
        app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7)  # 7天后过期
        
        @app.route('/login')
        def login():
            session.permanent = True  # 启用持久 Session
            session['username'] = 'admin'
            return "Logged in (persistent)!"

        2. Flask Session 的工作原理

        (1) 默认实现(SecureCookieSession)

        Flask 的 Session 默认使用 SecureCookieSession,数据存储在 客户端 Cookie 中,但经过签名(防止篡改)。

        数据格式

        {
            "username": "admin",
            "_fresh": True,
            "_id": "abc123...",
            "_permanent": True
        }

        签名机制:使用 itsdangerous 库进行签名,确保数据不被篡改(但内容可读)。

        (2) 如何修改 Session 存储方式?

        默认的 Cookie Session 有大小限制(通常 4KB),如果需要存储大量数据,可以改用 服务端 Session(如 Redis、数据库):

        from flask_session import Session  # 需要安装 flask-session
        
        app.config['SESSION_TYPE'] = 'redis'  # 可选 编程客栈redis, memcached, filesystem 等
        app.config['SESSION_PERMANENT'] = True
        app.config['SESSION_USE_SIGNER'] = True  # 是否签名 Cookie
        app.config['SESSION_KEY_PREFIX'] = 'myapp:'  # Redis 键前缀
        
        Session(app)  # 替换默认的 Session 实现

        然后 flask.session 会自动使用 Redis 存储数据,而不是 Cookie。

        3. Session 的安全配置

        (1) 安全相关的 Cookie 设置

        app.config.update(
            SESSION_COOKIE_NAME='my_session',       # Cookie 名称
            SESSION_COOKIE_HTTPONLY=True,          # 禁止 JavaScript 访问
            SESSION_COOKIE_SECURE=True,            # 仅 HTTPS 传输
            SESSION_COOKIE_SAMESITE='Lax',         # 防 CSRF(Strict/Lax/None)
            SESSION_COOKIE_PARTITIONED=True,       # 跨站 Cookie 隔离(CHIPS)
        )

        (2) 防止 Session 劫持

        • 使用 SESSION_USE_SIGNER=True(默认启用)防止篡改。
        • 避免在 Session 中存储敏感信息(如密码),因为 Cookie 内容可被用户读取(即使签名)。

        4. 常见问题

        (1)RuntimeError: The session is unavailable because no secret key was set.

        原因:未设置 SECRET_KEY

        解决

        app.secret_key = 'your-secret-key' # 必须设置

        (2) Session 数据不持久

        原因:默认 Session 在浏览器关闭后失效。

        解决

        session.permanent = True
        app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=30)

        (3) 如何强制 Session 过期?

        @app.route('/clear_session')
        def clear_session():
            session.clear()  # 清空 Session
            return "Session cleared!"

        5. 总结

        特性说明
        默认存储客户端 Cookie(签名防篡改)
        持久性session.permanent = True + PERMANENT_SESSION_LIFETIME
        安全配置HTTPOnly、Secure、SameSite
        扩展存储使用 flask-session 支持 Redis、数据库等
        适用场景小型数据(如用户登录状态),大数据需用服务端存储

        如果需要更安全的 Session 方案,建议:

        • 使用 服务端 Session(Redis) 替代 Cookie。
        • 避免存储敏感数据在 Session 中。
        • 启用 Secure + SameSite 防 CSRF。

        Sessions.py

        from __future__ import annotations
        
        import collections.abc as c
        import hashlib
        import typing as t
        from collections.abc import MutableMapping
        from datetime import datetime
        from datetime import timezone
        
        from itsdangerous import BadSignature
        from itsdangerous import URLSafeTimedSerializer
        from werkzeug.datastructures import CallbackDict
        
        from .json.tag import TaggedJSONSerializer
        
        if t.TYPE_CHECKING:  # pragma: no cover
            import typing_extensions as te
        
            from .app import Flask
            from .wrappers import Request
            from .wrappers import Response
        
        
        class SessionMixin(MutableMapping[str, t.Any]):
            """Expands a basic dictionary with session attributes."""
        
            @property
            def permanent(self) -> bool:
                """This reflects the ``'_permanent'`` key in the dict."""
                return self.get("_permanent", False)
        
            @permanent.setter
            def permanent(self, value: bool) -> None:
                self["_permanent"] = bool(value)
        
            #: Some implementations can detect whether a session is newly
            #: created, but that is not guaranteed. Use with caution. The mixin
            # default is hard-coded ``False``.
            new = False
        
            #: Some implementations can detect changes to the session and set
            #: this when that happens. The mixin default is hard coded to
            #: ``True``.
            modified = True
        
            #: Some implementations can detect when session data is read or
            #: written and set this when that happens. The mixin default is hard
            #: coded to ``True``.
            Accessed = True
        
        
        class SecureCookieSession(CallbackDict[str, t.Any], SessionMixin):
            """Base class for sessions based on signed cookies.
        
            This session backend will set t编程客栈he :attr:`modified` and
            :attr:`accessed` attributes. It cannot reliably track whether a
            session is new (vs. empty), so :attr:`new` remains hard coded to
            ``False``.
            """
        
            #: When data is changed, this is set to ``True``. Only the session
            #: dictionary itself is tracked; if the session contains mutable
            #: data (for example a nested dict) then this must be set to
            #: ``True`` manually when modifying that data. The session cookie
            #: will only be written to the response if this is ``True``.
            modified = False
        
            #: When data is read or written, this is set to ``True``. Used by
            # :class:`.SecureCookieSessionInterface` to add a ``Vary: Cookie``
            #: header, which allows caching proxies to cache different pages for
            #: different users.
            accessed = False
        
            def __init__(
                self,
                initial: c.Mapping[str, t.Any] | c.Iterable[tuple[str, t.Any]] | None = None,
            ) -> None:
                def on_update(self: te.Self) -> None:
                    self.modified = True
                    self.accessed = True
        
                super().__init__(initial, on_update)
        
            def __getitem__(self, key: str) -> t.Any:
                self.accessed = True
                return super().__getitem__(key)
        
            def get(self, key: str, default: t.Any = None) -> t.Any:
                self.accessed = True
                return super().get(key, default)
        
            def setdefault(self, key: str, default: t.Any = None) -> t.Any:
                self.accessed = True
                return super().setdefault(key, default)
        
        
        class NullSession(SecureCookieSession):
            """Class used to generate nicer error messages if sessions are not
            available.  Will still allow read-only access to the empty session
            but fail on setting.
            """
        
            def _fail(self, *args: t.Any, **kwargs: t.Any) -> t.NoReturn:
                raise RuntimeError(
                    "The session is unavailable because no secret "
                    "key was set.  Set the secret_key on the "
                    "application to something unique and secret."
                )
        
            __setitem__ = __delitem__ = clear = pop = popitem = update = setdefault = _fail  # noqa: B950
            del _fail
        
        
        class SessionInterface:
            """The basic interface you have to implement in order to replace the
            default session interface which uses werkzeug's securecookie
            implementation.  The only methods you have to implement are
            :meth:`open_session` and :meth:`save_sessijavascripton`, the others have
            useful defaults which you don't need to change.
        
            The session object returned by the :meth:`open_session` method has to
            provide a dictionary like interface plus the properties and methods
            from the :class:`SessionMixin`.  We recommend just subclassing a dict
            and adding that mixin::
        
                class Session(dict, SessionMixin):
                    pass
        
            If :meth:`open_session` returns ``None`` Flask will call into
            :meth:`make_null_session` to create a session that acts as replacement
            if the session support cannot work because some requirement is not
            fulfilled.  The default :class:`NullSession` class that is created
            will complain that the secret key was not set.
        
            To replace the session interface on an application all you have to do
            is to assign :attr:`flask.Flask.session_interface`::
        
                app = Flask(__name__)
                app.session_interface = MySessionInterface()
        
            Multiple requests with the same session may be sent and handled
            concurrently. When implementing a new session interface, consider
            whether reads or writes to the backing store must be synchronized.
            There is no guarantee on the order in which the session for each
            request is opened or saved, it will occur in the order that requests
            begin and end processing.
        
            .. versionadded:: 0.8
            """
        
            #: :meth:`make_null_session` will look here for the class that should
            #: be created when a null session is requested.  Likewise the
            #: :meth:`is_null_session` method will perform a typecheck against
            #: this type.
            null_session_class = NullSession
        
            #: A flag that indicates if the session interface is pickle based.
            #: This can be used by Flask extensions to make a decision in regards
            #: to how to deal with the session object.
            #:
            #: .. versionadded:: 0.10
            pickle_based = False
        
            def make_null_session(self, app: Flask) -> NullSession:
                """Creates a null session which acts as a replacement object if the
                real session support could not be loaded due to a configuration
                error.  This mainly aids the user experience because the job of the
                null session is to still support lookup without complaining but
                modifications are answered with a helpful error message of what
                failed.
        
                This creates an instance of :attr:`null_session_class` by default.
                """
                return self.null_session_class()
        
            def is_null_session(self, obj: object) -> bool:
                """Checks if a given object is a null session.  Null sessions are
                not asked to be saved.
        
                This checks if the object is an instance of :attr:`null_session_class`
                by default.
                """
                return isinstance(obj, self.null_session_class)
        
            def get_cookie_name(self, app: Flask) -> str:
                """The name of the session cookie. Uses``app.config["SESSION_COOKIE_NAME"]``."""
                return app.config["SESSION_COOKIE_NAME"]  # type: ignore[no-any-return]
        
            def get_cookie_domain(self, app: Flask) -> str | None:
                """The value of the ``Domain`` parameter on the session cookie. If not set,
                browsers will only send the cookie to the exact domain it was set from.
                Otherwise, they will send it to any subdomain of the given value as well.
        
                Uses the :data:`SESSION_COOKIE_DOMAIN` config.
        
                .. versionchanged:: 2.3
                    Not set by default, does not fall back to ``SERVER_NAME``.
                """
                return app.config["SESSION_COOKIE_DOMAIN"]  # type: ignore[no-any-return]
        
            def get_cookie_path(self, app: Flask) -> str:
                """Returns the path for which the cookie should be valid.  The
                default implementation uses the value from the ``SESSION_COOKIE_PATH``
                config var if it's set, and falls back to ``APPLICATION_ROOT`` or
                uses ``/`` if it's ``None``.
                """
                return app.config["SESSION_COOKIE_PATH"] or app.config["APPLICATION_ROOT"]  # type: ignore[no-any-return]
        
            def get_cookie_httponly(self, app: Flask) -> bool:
                """Returns True if the session cookie should be httponly.  This
                currently just returns the value of the ``SESSION_COOKIE_HTTPONLY``
                config var.
                """
                return app.config["SESSION_COOKIE_HTTPONLY"]  # type: ignore[no-any-return]
        
            def get_cookie_secure(self, app: Flask) -> bool:
                """Returns True if the cookie should be secure.  This currently
                just returns the value of the ``SESSION_COOKIE_SECURE`` setting.
                """
                return app.config["SESSION_COOKIE_SECURE"]  # type: ignore[no-any-return]
        
            def get_cookie_samesite(self, app: Flask) -> str | None:
                """Return ``'Strict'`` or ``'Lax'`` if the cookie should use the
                ``SameSite`` attribute. This currently just returns the value of
                the :data:`SESSION_COOKIE_SAMESITE` setting.
                """
                return app.config["SESSION_COOKIE_SAMESITE"]  # type: ignore[no-any-return]
        
            def get_cookie_partitioned(self, app: Flask) -> bool:
                """Returns True if the cookie should be partitioned. By default, uses
                the value of :data:`SESSION_COOKIE_PARTITIONED`.
        
                .. versionadded:: 3.1
                """
                return app.config["SESSION_COOKIE_PARTITIONED"]  # type: ignore[no-any-return]
        
            def get_expiration_time(self, app: Flask, session: SessionMixin) -> datetime | None:
                """A helper method that returns an expiration date for the session
                or ``None`` if the session is linked to the browser session.  The
                default implementation returns now + the permanent session
                lifetime configured on the application.
                """
                if session.permanent:
                    return datetime.now(timezone.utc) + app.permanent_session_lifetime
                return None
        
            def should_set_cookie(self, app: Flask, session: SessionMixin) -> bool:
                """Used by session backends to determine if a ``Set-Cookie`` header
                should be set for this session cookie for this response. If the session
                has been modified, the cookie is set. If the session is permanent and
                the ``SESSION_REFRESH_EACH_REQUEST`` config is true, the cookie is
                always set.
        
                This check is usually skipped if the session was deleted.
        
                .. versionadded:: 0.11
                """
        
                return session.modified or (
                    session.permanent and app.config["SESSION_REFRESH_EACH_REQUEST"]
                )
        
            def open_session(self, app: Flask, request: Request) -> SessionMixin | None:
                """This is called at the beginning of each request, after
                pushing the request context, before matching the URL.
        
                This must return an object which implements a dictionary-like
                interface as well as the :class:`SessionMixin` interface.
        
                This will return ``None`` to indicate that loading failed in
                some way that is not immediately an error. The request
                context will fall back to using :meth:`make_njsull_session`
                in this case.
                """
                raise NotImplementedError()
        
            def save_session(
                self, app: Flask, session: SessionMixin, response: Response
            ) -> None:
                """This is called at the end of each request, after generating
                a response, before removing the request context. It is skipped
                if :meth:`is_null_session` returns ``True``.
                """
                raise NotImplementedError()
        
        
        session_json_serializer = TaggedJSONSerializer()
        
        
        def _lazy_sha1(string: bytes = b"") -> t.Any:
            """Don't access ``hashlib.sha1`` until runtime. FIPS builds may not include
            SHA-1, in which case the import and use as a default would fail before the
            developer can configure something else.
            """
            return hashlib.sha1(string)
        
        
        class SecureCookieSessionInterface(SessionInterface):
            """The default session interface that stores sessions in signed cookies
            through the :mod:`itsdangerous` module.
            """
        
            #: the salt that should be applied on top of the secret key for the
            #: signing of cookie based sessions.
            salt = "cookie-session"
            #: the hash function to use for the signature.  The default is sha1
            digest_method = staticmethod(_lazy_sha1)
            #: the name of the itsdangerous supported key derivation.  The default
            #: is hMAC.
            key_derivation = "hmac"
            #: A python serializer for the payload.  The default is a compact
            #: JSON derived serializer with support for some extra Python types
            #: such as datetime objects or tuples.
            serializer = session_json_serializer
            session_class = SecureCookieSession
        
            def get_signing_serializer(self, app: Flask) -> URLSafeTimedSerializer | None:
                if not app.secret_key:
                    return None
        
                keys: list[str | bytes] = []
        
                if fallbacks := app.config["SECRET_KEY_FALLBACKS"]:
                    keys.extend(fallbacks)
        
                keys.append(app.secret_key)  # itsdangerous expects current key at top
                return URLSafeTimedSerializer(
                    keys,  # type: ignore[arg-type]
                    salt=self.salt,
                    serializer=self.serializer,
                    signer_kwargs={
                        "key_derivation": self.key_derivation,
                        "digjavascriptest_method": self.digest_method,
                    },
                )
        
            def open_session(self, app: Flask, request: Request) -> SecureCookieSession | None:
                s = self.get_signing_serializer(app)
                if s is None:
                    return None
                val = request.cookies.get(self.get_cookie_name(app))
                if not val:
                    return self.session_class()
                max_age = int(app.permanent_session_lifetime.total_seconds())
                try:
                    data = s.loads(val, max_age=max_age)
                    return self.session_class(data)
                except BadSignature:
                    return self.session_class()
        
            def save_session(
                self, app: Flask, session: SessionMixin, response: Response
            ) -> None:
                name = self.get_cookie_name(app)
                domain = self.get_cookie_domain(app)
                path = self.get_cookie_path(app)
                secure = self.get_cookie_secure(app)
                partitioned = self.get_cookie_partitioned(app)
                samesite = self.get_cookie_samesite(app)
                httponly = self.get_cookie_httponly(app)
        
                # Add a "Vary: Cookie" header if the session was accessed at all.
                if session.accessed:
                    response.vary.add("Cookie")
        
                # If the session is modified to be empty, remove the cookie.
                # If the session is empty, return without setting the cookie.
                if not session:
                    if session.modified:
                        response.delete_cookie(
                            name,
                            domain=domain,
                            path=path,
                            secure=secure,
                            partitioned=partitioned,
                            samesite=samesite,
                            httponly=httponly,
                        )
                        response.vary.add("Cookie")
        
                    return
        
                if not self.should_set_cookie(app, session):
                    return
        
                expires = self.get_expiration_time(app, session)
                val = self.get_signing_serializer(app).dumps(dict(session))  # type: ignore[union-attr]
                response.set_cookie(
                    name,
                    val,
                    expires=expires,
                    httponly=httponly,
                    domain=domain,
                    path=path,
                    secure=secure,
                    partitioned=partitioned,
                    samesite=samesite,
                )
                response.vary.add("Cookie")
        

        1. 会话(Session)的核心类

        (1)SessionMixin

        作用:为字典形式的 Session 提供扩展属性。

        关键属性

        • permanent:标记 Session 是否为持久会话(基于 _permanent 键值)。
        • new:标记 Session 是否为新创建的(默认为 False)。
        • modified:标记 Session 是否被修改(默认为 True)。
        • accessed:标记 Session 是否被访问(默认为 True)。

        (2)SecureCookieSession

        作用:基于签名 Cookie 的默认 Session 实现(继承自 CallbackDict 和 SessionMixin)。

        特点

        • 自动跟踪修改状态(modified)和访问状态(accessed)。
        • 数据变更时会触发回调,确保状态同步。
        (3)NullSession

        作用:当未配置密钥时,提供一个安全的空 Session 替代品。

        行为

        • 允许读取操作(返回空值)。
        • 拒绝写入操作(直接抛出 RuntimeError)。

        2. 会话接口(SessionInterface)

        (1) 核心方法

        • open_session(app, request)

          功能:在请求开始时加载 Session。

          返回:需实现类字典接口 + SessionMixin 的对象,失败时返回 None(触发 NullSession)。

        • save_session(app, session, response)

          功能:在请求结束时保存 Session 到响应。

          注意:若 is_null_session(session) 为 True 则跳过。

        (2) Cookie 配置方法

        • get_cookie_name():从配置 SESSION_COOKIE_NAME 获取 Cookie 名称。
        • get_cookie_domain():控制 Cookie 的 Domain 属性(默认无子域名共享)。
        • get_cookie_secure():根据 SESSION_COOKIE_SECURE 决定是否仅 HTTPS 传输。
        • get_cookie_samesite():配置 SameSite 防 CSRF(Strict/Lax)。
        • get_cookie_partitioned():是否启用 Partitioned 属性(用于跨站 Cookie 隔离)。
        (3) 辅助逻辑
        • should_set_cookie():决定是否设置 Set-Cookie 头(基于修改状态或 SESSION_REFRESH_EACH_REQUEST)。
        • make_null_session():生成 NullSession 实例。
        • is_null_session():检查对象是否为无效 Session。

        3. 默认实现(SecureCookieSessionInterface)

        (1) 安全特性

        签名机制:使用 itsdangerous.URLSafeTimedSerializer

        • 支持密钥轮换(SECRET_KEY_FALLBACKS)。
        • 可配置盐值(salt)、哈希算法(如 sha1)和密钥派生方式(如 hmac)。

        数据序列化:通过 TaggedJSONSerializer 处理 Python 特殊类型(如 datetime)。

        (2) 工作流程

        读取 Session

        • 从请求的 Cookie 中加载数据。
        • 验证签名和时间戳(防止篡改和过期会话)。
        • 失败时返回空 Session。

        保存 Session

        • 若 Session 为空且被修改 → 删除 Cookie。
        • 若需更新(should_set_cookie 为 True)→ 写入签名后的数据到 Cookie。
        • 自动设置 Vary: Cookie 头以适配缓存。

        (3) 配置示例

        app.config.update(
            SECRET_KEY="your-secret-key",
            SESSION_COOKIE_NAME="my_session",
            SESSION_COOKIE_SECURE=True,
            SESSION_COOKIE_SAMESITE="Lax",
            PERMANENT_SESSION_LIFETIME=timedelta(days=7),
        )

        4. 设计思想

        • 客户端存储:Session 数据默认通过 Cookie 存储(节省服务端资源)。
        • 安全优先:所有数据签名防篡改,且支持安全相关的 Cookie 属性。
        • 灵活扩展:通过实现 SessionInterface 可轻松切换为服务端 Session(如 Redis 存储)。

        常见问题

        • 密钥缺失:未设置 SECRET_KEY 时,所有 Session 操作将触发 NullSession 的异常。
        • 性能注意:Cookie 大小受限(通常不超过 4KB),复杂数据需考虑服务端存储方案。

        如果需要进一步分析特定部分(如自定义 Session 存储),可以深入探讨具体实现方式。

        到此这篇关于flask库中sessions.py的使用小结的文章就介绍到这了,更多相关flask sessions.py内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!

        0

        上一篇:

        下一篇:

        精彩评论

        暂无评论...
        验证码 换一张
        取 消

        最新开发

        开发排行榜