3

flask 中的 cookie 和 session

 3 years ago
source link: https://windard.com/blog/2017/10/17/Flask-Session
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

flask 中的 cookie 和 session

2017-10-17

session 和 cookie ,web 开发中必知必会必须掌握的两个概念。

  • cookie 小饼干,存储在用户浏览器,在用户发起HTTP请求时,一起发给 web 后端,标志用户身份
  • session 会话,在 web 后端记录用户的登录状态,保存方式有 cookie,内存,缓存,数据库等,用来区分不同用户的请求,和同一个用户的不同请求

flask 的原生 session 实现

session 能够区分不用的用户请求和同一个用户的不同请求,则需要将每一个请求的 session 数据隔离,即保证每一个请求的 session 数据不能相互混淆,每一个 session 的数据都只属于本次请求,其他请求不能拿到。在 flask 中,作为一次请求的的全局变量,session 数据是从本次请求的栈顶取出,详细可以查看 flask 的 LocalProxy

在每一个请求的时候,session 数据是栈顶取出,那么不在这次请求的时候,session 存储在哪里呢?

Flask 中默认是直接将 服务器端的 session 存储在客户端浏览器的 cookie 中,当然是加密过的,意思就是说,后端在 session 里面写入什么数据,就会被加密存储到客户端 cookie 里面,浏览器中那条叫做 session 的 cookie 就是加密数据,只要你明白算法,知道密钥,理论上就可以把 session 的数据都算出来,所以在 flask 中如果想要使用 session 就必须要设一个 SECRET_KEY ,这就是 session 加密的密钥。

将 session 放在 cookie 中有好处,也有坏处。好处是不受服务器端影响,flask 重启仍然可以使用,如果放在内存中,后端重启,前端全部需要再次登录。坏处是如果存很多数据在 session 里面,那么 cookie 就会非常大,而前端是不应该存储大量数据,而且容易被人破解,虽然即使是同样的 session 数据,每次加密生成 cookie 也不同,但是也还是不安全,怪不得 cookie 中的这条数据叫做 session,而不是session_id 或者 sid。

Session 可以看做是一个类字典对象,存储序列化的数据,它不能直接存储对象,需要将对象序列化之后存储,数组字典都可以,但是对象不行。

既然 session 是一个字典,那它是怎样保持登录状态的呢?那它是怎样知道两次请求时同一个用户的呢?

只要 session 在每一个请求中是隔离的,那只需要在 session 中存储 login=True 的键值对,那么只要 session 中有这样的键值对,即可以表明发起该次请求的用户是登录的。每一次请求的 session 是隔离的,那么在 session 中存储 user_id=xxx,如果某两次请求的 user_id 是相同的,那么这两次请求的用户即是相同的。

flask 使用 sid

阅读 flask 源代码 sessions.py 中的 SecureCookieSessionInterface,这是 flask 默认的 SessionInterface.

根据源代码可以发现 session 的导入和导出主要由 open_sessionsave_session 负责,前者从请求的 cookie 中取出 session ,然后解密反序列化,返回一个 session_class,如果没有找到 session ,则返回一个空的 session_class,后者负责将 session 序列化加密,放入返回响应头中。

那手动实现 session_id 的功能也非常的简单,在存入 cookie 的时候,不直接存储存储加密之后的 session ,而是使用 sha1 计算出 session 的哈希值,作为索引,然后将其存入 Redis 缓存中,然后将哈希值提供给前端。在取出 cookie 的时候,通过哈希值来查找到 session 的内容,这样在传给前端页面的 cookie 也能够保证一个好看的,长度固定的 sid ,无法猜测,无法破解。

将 session 放在 cookie 中,还有一个很大的安全隐患,虽然即使是相同的 session 数据,每一次加密之后的结果也会不同,因为引入了时间戳作为校验,但是这也就说明只要在 cookie 有效期之内,那每一次请求的 cookie 都可以继续使用,都可以自动登录,获得登录状态。

使用 sid的时候,每次的请求 sid 也会有所改变,为了安全,可以在每次从 Redis 缓存中取出 session 数据的时候,也同时删除缓存数据,就可以避免使用以前的 sid 登录,同时也是为了避免缓存中存储大量的无用数据,如果每次登录都是一条新的缓存数据,用户量大的情况下,Redis 也受不了吖。

而且使用 Redis 缓存将 sid 与 session 加密结果一起存储的方式,与 flask 无关,在后端重启之后照样可以正常拿到数据,存到 Redis 里面也可以比较稳。

所以总结就是即使是在 cookie 里面存储 session 的加密值,每一次刷新页面也会刷新 cookie,将 cookie 换上新的时间戳之类的,加密的结果是不一致的,虽然大部分数据段的内容是相同的。在使用sid的时候则会特别的明显,每次都是不一样的结果。

使用 sid 的话,如果之前的sid还在有效期之内的话,理论上说拿着之前的sid,也是可以使用的,所以需要将sid从缓存中每次取出后删除,使用sid,只取用一次,就从Redis中删除,避免Redis占用过大。

默认的 cookie 的有效期是当前会话,意思就是说,只要当前浏览器还没有关闭,就算当前页面关闭或者其他跳转到其他页面,再次回到该页面,cookie 还是保存着的,当前会话是指浏览器的当前会话,浏览器的当前会话只有在关闭浏览器之后才会被清除,如果想做一个记住我的功能,对浏览器的cookies 要做一个长期的存储的,保存时间就不能只是当前会话了。

接下来上大招,flask_login ,看一下高端的写法应该怎么玩。

flask-login 的 session

flask_login 还是万变不离其宗,更加直接,将 user_id 写入 session,session 里面找有 user_id 就是登陆的,flask_principal 权限控制管理则是将 identity_id 写入 session,然后加密放在 cookie 里面。

至于 remember_me 功能就更简单了,将 user_id 放到了另一条 cookie 里,即未加密的数据,然后放一个加密的数据作为签名,cookie 的有效期设置为一年,在一年内使用的时候直接做签名校验即可,如果签名通过,就使用该 user_id 登录。

这里使用的是单向签名算法,同样使用 SECRET_KEY 作为签名密钥,很奇怪为什么不适用加密算法,在使用的时候解密就好了,也不用直接存储明文数据。

奇怪,不知道为什么要直接放加密数据,这样感觉不太稳,虽然自己手动改了之后校验不通过,但是能够看到session的内容始终不太好。

flask_login 还有一个功能就是判断用户是否为密码登录的,还是使用保存的 cookie 登录的,就是在 session 中写入一个 _fresh 的键值对,如果是密码登录的,在使用 login_user 的时候在 session 中写入 _fresh=True,在使用 cookie 登录的时候会写入 _fresh=False

flask_login 在 session 中还写入了一个 _id 的字段,不知道有什么用,好像是没有看到使用的,只有在密码登录的时候才会有这个字段,好像是用户浏览器的唯一标志,即将浏览器的 user-agent 和 IP 做了一个 sha512 哈希之后的数据,保存在 session 中,使用 cookie 登录的就没有。

flask_login 无法控制 SessionInterface ,即不能控制 session 的形式和存储状态等,只能在 session 中存内容,在 login_user 时将 user_id ,_fresh, _id, remember 等写入,在当前请求的栈顶推入登入的用户 current_user,在当前请求中使用 current_user 可以获得当前登录的用户。在 flask_login 在 session 中写入数据之后,flask 的 process_handler 检测到 session 中有数据,调用 sessions.save_session 方法,将数据加密后存入 cookie 中。

所谓的得到当前session的意思只是得到该 session 中存储的用户数据而已,实际上这部分数据全部加密存储在 cookie 中。
 在写入 cookie 的时候,当前登录的用户信息作为一个 cookie 写入,用户的 remember 信息作为另一个 cookie 写入,flask_login 虽然不能控制 SessionInterface,在 save_session 时将session全部序列化写入cookie,但是可以在请求之后,注册一个 after_request 事件,在这个函数中将 session 中的 remember 信息单独作为一个 cookie 写入 response ,然后一起传给前端。这就说明 save_session 函数调用是在 after_request 之后。

app.after_request(self._update_remember_cookie)

def _update_remember_cookie(self, response):
    # Don't modify the session unless there's something to do.
    if 'remember' in session:
        operation = session.pop('remember', None)

        if operation == 'set' and 'user_id' in session:
            self._set_cookie(response)
        elif operation == 'clear':
            self._clear_cookie(response)

    return response

而且在 after_request 的函数中将 remember 的信息弹出了,怪不得后来使用登录时的 cookie 已经没有了 remember 字段。 在将remember信息存入 cookie 的方式非常的简单粗暴,就是将session中的user_id和 app.secret_key 作为密钥,sha512 哈希算法的 hmac 加密 user_id 一起存入cookie,确实不可解密,但是可以防篡改的验签。

在 flask_login 将 remember 信息存入 cookie 之后,flask 的 save_session 也要行动了,此时在 login_user 中被存入的 remember 字段已经被弹出了,只剩下 _fresh, _id, user_id 三个字段。

在 save_session 中,dumps 函数中一个 serializer ,一个 signature ,边序列化边签名,一起存入cookie,所以在 open_session 中 loads 一个反序列化,一个验签。

在请求到来之后,那 open_session 肯定也是 before_request 之前的了,将 cookie 中的 session 数据取出来,放到本次请求的全局变量 session 中去。

如果是请求受到 login_required 装饰的方法,就会去判断 current_user 是否授权,一般登录即存在,存在即授权,所以就是判断 current_user 是否存在,所以 current_user 是在什么时候在这次请求中被推入栈顶的呢?

有一个疑问,就像其他的 LocalProxy 导入的全局变量一样,current_user 都是只知道被引入了,但是不知道是在什么时候被引入的,在什么条件下触发的,是被谁调用的,最终反正就是得到了。在请求的开始的时候。

在 flask_login 中的 current_user 是将 _get_user 得到的 user 对象返回,而且它也能够在没有栈顶没有 user 对象的时候推入 user ,而这个 user 可以从 session 中取 user_id ,然后通过 login_manager.user_loader 来得到 user 对象,这是在用户登录的时候,也可以在用户使用 remember cookie 信息登录的时候从 cookie 中得到,或者是从 HTTP header 中得到,或者是从请求参数中得到, 这几种方式在得到 user 之后就会将 user 信息导入 session 中。

Session 中的 remember 字段默认是没有的,或者默认为空,当其为 set 的时候就表明要设置 cookie 了,这是在使用了 remember=True 的 login_user 的时候,在设置 cookie 之后即被弹出,当其为 clear 时就表明要删除 cookie 了,这是在使用了 logout_user 的时候被设置的,在 logout_user 时栈顶的 user 也被弹出,current_user 变为 AnonymousUserMixin 。同样的,在 after_request 中,既然可以设置 cookie ,那么也就可以删除 cookie,在删除 remember 的cookie之后,session 中的 remember 字段也被删除。在 logout_user 中 user_id, _fresh 被删除,remember 字段被设置然后删除,remember 的 cookie 被删除,当前请求的栈顶 user 被弹出,在剩下 session 中的 _id 还依旧倔强的留在session中,所以最后,session 依旧作为 cookie 被一直返回给前端。

最后写一点,flask_login 的 session_mode 挺有意思,当其为 strong 的时候,session 中的 _id 就会派上用场了,如果当前 session 被使用在不用的浏览器和IP上,就会删除当前session和用户前端的cookie,这是很正确的,因为如果是正常用户的话,cookie无论怎么变都是在自己的电脑上,user-agent 无论如何不会改变,IP 的改变倒是有可能,就是在登录之后,切换IP了,就会被自动退出登录,那只需要再登录一遍就好了吖。本来用户切换 IP 了,就应该警觉一些,而如果没有切换 IP ,没有切换浏览器,应该 _id 也都是不会改变的,确实是有一定的安全性。

flask-login 的 token

关于 flask_login 不知道是在进化还是在退化,竟然在 0.3.2 里面有一个很好用的 feature 在 0.4.0 里面去掉了,就是 token_loader,使用 token_loader 的话,需要在你的 UserMixin 的用户子类中实现 get_auth_token ,方法,这样就能在 remember 里面不要再直接放用户信息了,太危险了,就算是升级 session,将 session 变为 sid ,但是 remember 还是不能解决,因为是flask_login写入的,而不是我们能控制的,官方还非常贴心的提供了 make_secure_token 方法来安全的创建 token ,真的非常好用,来试一下,奇怪,为什么后面要去掉呢?

找来找去,终于在 changelog 里面找到 0.4.0 有提到,有老哥把 remember_me 当成 token 来使用,确实是人才吖,token 短小精悍,可长期持有,确实如果保留 token_loader 可以这样用,但是每次请求返回的时候会带上一个 cookie 的吖,但是这样用真的好么,明明有一个 request_loader 的吖,老哥。作者也是 real 耿直,直接就把这么好的 feature 给删了,让别人想用 remember 就必须存储那么丑的 session ,丑拒。

BREAKING: The `login_manager.token_handler` function, `get_auth_token` method
on the User class, and the `utils.make_secure_token` utility function have
been removed to prevent users from creating insecure auth implementations.
Use the `Alternative Tokens` example from the docs instead. #291

好像是这位老哥

一个 remember cookie 可以被大量的,多次的使用,但是有一个问题就是,无论被使用多少遍,它的过期时间都不会改变,即不会给你生成一个新的 remember 信息,也不会延长 remember 的过期时间,确实挺合理的,remember 信息在重新登录之后,才会改变,生成一个新的,但是原来的那个还是有效的,可以用的,也没有刻意的去清之前的cookie,只是拿一个新的去覆盖原来的旧的。

flask-login 的两种 token 实现方式

发现直接使用 session 加密当数据的一个重要缺陷,就是直接使用加密之后的 session ,无论有没有登出,或者后面做了什么操作,拿以前的有效期之内的 session 都是可以继续用的,包括 remember 信息。虽然在操作时都会将前端的cookie做一个清理,或者说每一次cookie都会改变,但是使用之前的 cookie 终究是不稳吖,但是使用 sid 就很稳了,因为每一次从缓存中读取之后,就会将前一个 sid 删掉,只读一次。这就很好的嘛。

实际上 session 保存的实质一个字典的信息,登入状态只是有没有 current_user ,当前有没有用户,在栈顶有用户,即登录,没有,则未登录。session 和 current_user 并不是绝对的相互对应,有 session,或许只是有保存信息,并不一定有用户,有用户,也并不一定需要一个 session 。

flask-login 的两种 token 实现方式,一种是 session_token ,在 SessionInterface 中的读取 session 和写入 session 的时候,不再是从 cookie 中读取,而是从 header 里面读取 session 。还有一种方式是使用 flask-login 提供的 request_loader ,可以从 header 或者请求参数中读取 token

session_token 和 login_token 都能够实现 token 的功能,在 session_token 中 token 还可以当做 sid 使用,但是当成 sid 只能使用一次,就会失效了,需要使用返回的新的 sid ,在是 login_token 中 token 不仅可以放在 header 中,还可以放在 URL 请求参数中,效果一致。

Token 是能够多次使用的,使用后不会变的,再次生成原来的不受影响,session_login 且能够登出,即 token 失效,在 login_session 中不能登出。session_token 的使用期限在每次登录后缓存刷新,理论可以一直长久的无限使用,login_token 的有效期是固定值的,缓存失效需要再次登录,获得新的 token。

多点登录,任意下线

Flask-redis 真实坑吖,flask-cache 设置缓存为 redis 的时候存进去str,取出来 str ,但是 flask-Redis,存进去 str ,取出来 bytes,python 3 中。

在 flask 的 cookie 中存储的 session 是只要在过期时间之内的都可以使用,虽然是每次都更新,但是以前的 cookie 也还是可以使用的,这一点在使用 session_id 如果每次只能使用一次的话,虽然保证了安全,但是在现代网站架构中并不实用。

现代网站中肯定是使用 session_id,不可能会在 cookie 中存大量的数据,即时是加密的也不行。那么将数据存在 Redis 中之后,如果每次使用将 原来的 session_id 删除,将新的 session_id 写入,那么 Redis 中间的同步都是一个问题,而且在首页等大型的站点,一般会有多个请求同时发出,如果 session_id 只能使用一次的话,那么岂不能发出大量的异步请求,只能同步一个一个轮流发出,会等待较长时间。

所以使用 session_id 之后,其实 session_id 是当做 token 来使用,能够在一段时间内有效,且保持不变。


本文固定链接:https://windard.com/blog/2017/10/17/Flask-Session
原创文章,转载请注明出处:flask 中的 cookie 和 session By Windard


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK