lua-resty-session

OpenResty 的会话库 - 灵活且安全

$ opm get bungle/lua-resty-session

lua-resty-session

lua-resty-session 是一个安全且灵活的 OpenResty 会话库。

概括来说;

  • 会话是不可变的(每次保存都会生成一个新的会话),并且是无锁的。

  • 会话数据使用 HKDF-SHA256 派生的密钥进行 AES-256-GCM 加密。

  • 会话具有一个固定大小的头部,该头部使用 HKDF-SHA256 派生的密钥进行 HMAC-SHA256 MAC 保护。

  • 会话数据可以存储在无状态 Cookie 中或各种后端存储中。

  • 单个会话 Cookie 可以跨不同受众维护多个会话。

注意:版本 4.0.0 是对该库的重写,其中包含多年来积累的大量经验教训。如果您仍然使用旧版本,请参考 旧版文档

状态

该库被认为已准备好用于生产环境。

摘要

    worker_processes  1;
    
    events {
      worker_connections 1024;
    }
    
    http {
      init_by_lua_block {
        require "resty.session".init({
          remember = true,
          audience = "demo",
          secret   = "RaJKp8UQW1",
          storage  = "cookie",
        })
      }
      
      server {
        listen       8080;
        server_name  localhost;
        default_type text/html;
    
        location / {
          content_by_lua_block {
            ngx.say([[
              <html>
              <body>
                <a href=/start>Start the test</a>
              </body>
              </html>
            ]])
          }
        }
    
        location /start {
          content_by_lua_block {
            local session = require "resty.session".new()
            session:set_subject("OpenResty Fan")
            session:set("quote", "The quick brown fox jumps over the lazy dog")
            local ok, err = session:save()
           
            ngx.say(string.format([[
              <html>
              <body>
                <p>Session started (%s)</p>
                <p><a href=/started>Check if it really was</a></p>
              </body>
              </html>
            ]], err or "no error"))
          }
        }
    
        location /started {
          content_by_lua_block {
            local session, err = require "resty.session".start()
            
            ngx.say(string.format([[
              <html>
              <body>
                <p>Session was started by %s (%s)</p>
                <p><blockquote>%s</blockquote></p>
                <p><a href=/modify>Modify the session</a></p>
              </body>
              </html>
            ]],
              session:get_subject() or "Anonymous",
              err or "no error",
              session:get("quote") or "no quote"
            ))
          }
        }
        
        location /modify {
          content_by_lua_block {
            local session, err = require "resty.session".start()
            session:set_subject("Lua Fan")
            session:set("quote", "Lorem ipsum dolor sit amet")
            local _, err_save = session:save()
            
            ngx.say(string.format([[
              <html>
              <body>
                <p>Session was modified (%s)</p>
                <p><a href=/modified>Check if it is modified</a></p>
              </body>
              </html>
            ]], err or err_save or "no error"))
          }
        }
        
        location /modified {
          content_by_lua_block {
            local session, err = require "resty.session".start()
    
            ngx.say(string.format([[
              <html>
              <body>
                <p>Session was started by %s (%s)</p>
                <p><blockquote>%s</blockquote></p>
                <p><a href=/destroy>Destroy the session</a></p>
              </body>
              </html>
            ]],
              session:get_subject() or "Anonymous",
              err or "no error",
              session:get("quote")  or "no quote"
            ))
          }
        }
        
        location /destroy {
          content_by_lua_block {
            local ok, err = require "resty.session".destroy()
    
            ngx.say(string.format([[
              <html>
              <body>
                <p>Session was destroyed (%s)</p>
                <p><a href=/destroyed>Check that it really was?</a></p>
              </body>
              </html>
            ]], err or "no error"))
          }
        }
        
        location /destroyed {
          content_by_lua_block {
            local session, err = require "resty.session".open()
    
            ngx.say(string.format([[
              <html>
              <body>
                <p>Session was really destroyed, you are known as %s (%s)</p>
                <p><a href=/>Start again</a></p>
              </body>
              </html>
            ]],
              session:get_subject() or "Anonymous",
              err or "no error"
            ))
          }
        }    
      }
    }  

安装

使用 OpenResty 包管理器 (opm)

    ❯ opm get bungle/lua-resty-session

lua-resty-session 的 OPM 仓库位于 https://opm.openresty.org.cn/package/bungle/lua-resty-session/。

还要检查每个存储的依赖项(可能存在其他依赖项)。

使用 LuaRocks

    ❯ luarocks install lua-resty-session

lua-resty-session 的 LuaRocks 仓库位于 https://luarocks.org/modules/bungle/lua-resty-session。

还要检查每个存储的依赖项(可能存在其他依赖项)。

配置

配置可以分为通用会话配置和服务器端存储配置。

以下是一个示例

    init_by_lua_block {
      require "resty.session".init({
        remember = true,
        store_metadata = true,
        secret = "RaJKp8UQW1",
        secret_fallbacks = {
          "X88FuG1AkY",
          "fxWNymIpbb",
        },
        storage = "postgres",
        postgres = {
          username = "my-service",
          password = "kVgIXCE5Hg",
          database = "sessions",
        },
      })
    }

会话配置

会话配置可以传递给 初始化构造函数辅助函数

以下是可能的会话配置选项

| 选项 | 默认值 | 描述 | |-----------------------------|:------------:|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | secret | nil | 用于密钥派生的密钥。密钥在使用前会使用 SHA-256 进行哈希。例如:"RaJKp8UQW1"。 | | secret_fallbacks | nil | 可以用作备用密钥的密钥数组(进行密钥轮换时),例如:{ "6RfrAYYzYq", "MkbTkkyF9C" }。 | | ikm | (随机) | 可以直接指定初始密钥材料 (或 ikm)(不使用密钥),数据长度正好为 32 字节。例如:"5ixIW4QVMk0dPtoIhn41Eh1I9enP2060" | | ikm_fallbacks | nil | 可以用作备用密钥的初始密钥材料数组(进行密钥轮换时),例如:{ "QvPtlPKxOKdP5MCu1oI3lOEXIVuDckp7" }。 | | cookie_prefix | nil | Cookie 前缀,使用 nil"__Host-""__Secure-"。 | | cookie_name | "session" | 会话 Cookie 名称,例如:"session"。 | | cookie_path | "/" | Cookie 路径,例如:"/"。 | | cookie_http_only | true | 将 Cookie 标记为 HTTP Only,使用 truefalse。 | | cookie_secure | nil | 将 Cookie 标记为安全,使用 niltruefalse。 | | cookie_priority | nil | Cookie 优先级,使用 nil"Low""Medium""High"。 | | cookie_same_site | "Lax" | Cookie 同站点策略,使用 nil"Lax""Strict""None""Default" | | cookie_same_party | nil | 使用同一方标记标记 Cookie,使用 niltruefalse。 | | cookie_partitioned | nil | 使用分区标记标记 Cookie,使用 niltruefalse。 | | remember | false | 启用或禁用持久会话,使用 niltruefalse。 | | remember_safety | "Medium" | 记住 Cookie 密钥派生的复杂性,使用 nil"None"(快速)、"Low""Medium""High""Very High"(缓慢)。 | | remember_cookie_name | "remember" | 持久会话 Cookie 名称,例如:"remember"。 | | audience | "default" | 会话受众,例如:"my-application"。 | | subject | nil | 会话主体,例如:"[email protected]"。 | | enforce_same_subject | false | 当设置为 true 时,受众需要共享相同的主题。库在保存时会删除与主题不匹配的受众数据。 | | stale_ttl | 10 | 保存会话时会创建一个新会话,过期时间指定旧会话还可以使用多长时间,例如:10(秒)。 | | idling_timeout | 900 | 空闲超时指定会话在被视为无效之前可以处于非活动状态多长时间,例如:900(15 分钟)(秒),0 禁用检查和触碰。 | | rolling_timeout | 3600 | 轮询超时指定会话可以使用多长时间,直到需要续订,例如:3600(一小时)(秒),0 禁用检查和轮询。 | | absolute_timeout | 86400 | 绝对超时限制会话可以续订多长时间,直到需要重新身份验证,例如:86400(一天)(秒),0 禁用检查。 | | remember_rolling_timeout | 604800 | 记住超时指定持久会话被视为有效多长时间,例如:604800(一周)(秒),0 禁用检查和轮询。 | | remember_absolute_timeout | 2592000 | 记住绝对超时限制持久会话可以续订多长时间,直到需要重新身份验证,例如:2592000(30 天)(秒),0 禁用检查。 | | hash_storage_key | false | 是否对存储密钥进行哈希。如果存储密钥被哈希,则在没有 Cookie 的情况下,服务器端无法解密数据,使用 niltruefalse。 | | hash_subject | false | 当启用 store_metadata 时,是否对主题进行哈希,例如:出于 PII 原因。 | | store_metadata | false | 是否也存储会话的元数据,例如:收集特定受众属于特定主题的会话数据。 | | touch_threshold | 60 | 触碰阈值控制 session:refresh 多久触碰一次 Cookie,例如:60(一分钟)(秒) | | compression_threshold | 1024 | 压缩阈值控制何时对数据进行压缩,例如:1024(千字节)(字节),0 禁用压缩。 | | request_headers | nil | 要发送到上游的一组标头,使用 idaudiencesubjecttimeoutidling-timeoutrolling-timeoutabsolute-timeout。例如:{ "id", "timeout" } 将在调用 set_headers 时设置 Session-IdSession-Timeout 请求标头。 | | response_headers | nil | 要发送到下游的一组标头,使用 idaudiencesubjecttimeoutidling-timeoutrolling-timeoutabsolute-timeout。例如:{ "id", "timeout" } 将在调用 set_headers 时设置 Session-IdSession-Timeout 响应标头。 | | storage | nil | 存储负责存储会话数据,使用 nil"cookie"(数据存储在 Cookie 中)、"dshm""file""memcached""mysql""postgres""redis""shm",或者提供自定义模块的名称("custom-storage"),或者一个实现会话存储接口的 table。 | | dshm | nil | dshm 存储的配置,例如:{ prefix = "sessions" }(见下文) | | file | nil | 文件存储的配置,例如:{ path = "/tmp", suffix = "session" }(见下文) | | memcached | nil | Memcached 存储的配置,例如:{ prefix = "sessions" }(见下文) | | mysql | nil | MySQL / MariaDB 存储的配置,例如:{ database = "sessions" }(见下文) | | postgres | nil | Postgres 存储的配置,例如:{ database = "sessions" }(见下文) | | redis | nil | Redis / Redis Sentinel / Redis Cluster 存储的配置,例如:{ prefix = "sessions" }(见下文) | | shm | nil | 共享内存存储的配置,例如:{ zone = "sessions" } | | ["custom-storage"] | nil | 自定义存储(使用 require "custom-storage" 加载)配置。 |

将数据存储到 Cookie 时,不需要额外的配置,只需将 storage 设置为 nil"cookie"

DSHM 存储配置

使用 DHSM 存储,您可以使用以下设置(将 storage 设置为 "dshm"

| 选项 | 默认值 | 描述 | |---------------------|:-------------:|----------------------------------------------------------------------------------------------| | prefix | nil | 存储在 DSHM 中的键的前缀。 | | suffix | nil | 存储在 DSHM 中的键的后缀。 | | host | "127.0.0.1" | 要连接的主机。 | | port | 4321 | 要连接的端口。 | | connect_timeout | nil | 控制 TCP/Unix 域套接字对象 connect 方法中使用的默认超时值。 | | send_timeout | nil | 控制 TCP/Unix 域套接字对象 send 方法中使用的默认超时值。 | | read_timeout | nil | 控制 TCP/Unix 域套接字对象 receive 方法中使用的默认超时值。 | | keepalive_timeout | nil | 控制连接池中连接的默认最大空闲时间。 | | pool | nil | 正在使用的连接池的自定义名称。 | | pool_size | nil | 连接池的大小。 | | backlog | nil | 当连接池已满时使用的队列大小(使用 pool_size 配置)。 | | ssl | nil | 启用 SSL。 | | ssl_verify | nil | 验证服务器证书。 | | server_name | nil | 新 TLS 扩展服务器名称指示 (SNI) 的服务器名称。 |

请参考 ngx-distributed-shm 以获取安装必要的依赖项。

文件存储配置

使用文件存储,您可以使用以下设置(将 storage 设置为 "file"

| 选项 | 默认值 | 描述 | |---------------------|:---------------:|-------------------------------------------------------------------------------------| | prefix | nil | 会话文件的名称前缀。 | | suffix | nil | 会话文件的名称后缀(或扩展名,不带 .)。 | | pool | nil | 文件写入所在的线程池名称(仅适用于 Linux)。 | | path | (临时目录) | 创建会话文件的路径(或目录)。 |

实现需要 LuaFileSystem,您可以使用 LuaRocks 安装它。

    ❯ luarocks install LuaFileSystem

Memcached 存储配置

使用 Memcached,您可以使用以下设置(将 storage 设置为 "memcached"

| 选项 | 默认值 | 描述 | |---------------------|:-----------:|----------------------------------------------------------------------------------------------| | prefix | nil | 存储在 memcached 中的键的前缀。 | | suffix | nil | 存储在 memcached 中的键的后缀。 | | host | 127.0.0.1 | 要连接的主机。 | | port | 11211 | 要连接的端口。 | | socket | nil | 要连接的套接字文件。 | | connect_timeout | nil | 控制 TCP/unix-domain 套接字对象 connect 方法使用的默认超时值。 | | send_timeout | nil | 控制 TCP/unix-domain 套接字对象 send 方法使用的默认超时值。 | | read_timeout | nil | 控制 TCP/unix-domain 套接字对象 receive 方法使用的默认超时值。 | | keepalive_timeout | nil | 控制连接池中连接的默认最大空闲时间。 | | pool | nil | 使用的连接池的自定义名称。 | | pool_size | nil | 连接池的大小。 | | backlog | nil | 当连接池已满时(使用 pool_size 配置)要使用的队列大小。 | | ssl | false | 是否启用 SSL | | ssl_verify | nil | 是否验证服务器证书 | | server_name | nil | 新 TLS 扩展服务器名称指示 (SNI) 的服务器名称。 |

MySQL / MariaDB 存储配置

使用文件 MySQL / MariaDB,您可以使用以下设置(将 storage 设置为 "mysql"

| 选项 | 默认值 | 描述 | |---------------------|:-----------------:|----------------------------------------------------------------------------------------------| | host | "127.0.0.1" | 要连接的主机。 | | port | 3306 | 要连接的端口。 | | socket | nil | 要连接的套接字文件。 | | username | nil | 用于身份验证的数据库用户名。 | | password | nil | 用于身份验证的密码,根据服务器配置可能需要。 | | charset | "ascii" | MySQL 连接上使用的字符集。 | | database | nil | 要连接的数据库名称。 | | table_name | "sessions" | 存储会话数据的数据库表名称。 | | table_name_meta | "sessions_meta" | 存储会话元数据的数据库元数据表名称。 | | max_packet_size | 1048576 | 从 MySQL 服务器发送的回复数据包的上限(以字节为单位)。 | | connect_timeout | nil | 控制 TCP/unix-domain 套接字对象 connect 方法使用的默认超时值。 | | send_timeout | nil | 控制 TCP/unix-domain 套接字对象 send 方法使用的默认超时值。 | | read_timeout | nil | 控制 TCP/unix-domain 套接字对象 receive 方法使用的默认超时值。 | | keepalive_timeout | nil | 控制连接池中连接的默认最大空闲时间。 | | pool | nil | 使用的连接池的自定义名称。 | | pool_size | nil | 连接池的大小。 | | backlog | nil | 当连接池已满时(使用 pool_size 配置)要使用的队列大小。 | | ssl | false | 是否启用 SSL。 | | ssl_verify | nil | 是否验证服务器证书。 |

您还需要在您的数据库中创建以下表格

`sql

-- 存储会话数据的数据库表。

CREATE TABLE IF NOT EXISTS sessions ( sid CHAR(43) PRIMARY KEY, name VARCHAR(255), data MEDIUMTEXT, exp DATETIME, INDEX (exp)

) CHARACTER SET ascii;

-- 会话元数据表。

-- 只有在您想存储会话元数据时才需要。

CREATE TABLE IF NOT EXISTS sessions_meta ( aud VARCHAR(255), sub VARCHAR(255), sid CHAR(43), PRIMARY KEY (aud, sub, sid), CONSTRAINT FOREIGN KEY (sid) REFERENCES sessions(sid) ON DELETE CASCADE ON UPDATE CASCADE ) CHARACTER SET ascii;

    ## Postgres Configuration
    
    With file Postgres you can use the following settings (set the `storage` to `"postgres"`):
    
    | Option              |      Default      | Description                                                                                               |
    |---------------------|:-----------------:|-----------------------------------------------------------------------------------------------------------|
    | `host`              |   `"127.0.0.1"`   | The host to connect.                                                                                      |
    | `port`              |      `5432`       | The port to connect.                                                                                      |
    | `application`       |      `5432`       | Set the name of the connection as displayed in pg_stat_activity (defaults to `"pgmoon"`).                 |
    | `username`          |   `"postgres"`    | The database username to authenticate.                                                                    |
    | `password`          |       `nil`       | Password for authentication, may be required depending on server configuration.                           |
    | `database`          |       `nil`       | The database name to connect.                                                                             |
    | `table_name`        |   `"sessions"`    | Name of database table to which to store session data (can be `database schema` prefixed).                |
    | `table_name_meta`   | `"sessions_meta"` | Name of database meta data table to which to store session meta data (can be `database schema` prefixed). |
    | `connect_timeout`   |       `nil`       | Controls the default timeout value used in TCP/unix-domain socket object's `connect` method.              |
    | `send_timeout`      |       `nil`       | Controls the default timeout value used in TCP/unix-domain socket object's `send` method.                 |
    | `read_timeout`      |       `nil`       | Controls the default timeout value used in TCP/unix-domain socket object's `receive` method.              |
    | `keepalive_timeout` |       `nil`       | Controls the default maximal idle time of the connections in the connection pool.                         |
    | `pool`              |       `nil`       | A custom name for the connection pool being used.                                                         |
    | `pool_size`         |       `nil`       | The size of the connection pool.                                                                          |
    | `backlog`           |       `nil`       | A queue size to use when the connection pool is full (configured with pool_size).                         |
    | `ssl`               |      `false`      | Enable SSL.                                                                                               |
    | `ssl_verify`        |       `nil`       | Verify server certificate.                                                                                |
    | `ssl_required`      |       `nil`       | Abort the connection if the server does not support SSL connections.                                      |
    
    You also need to create following tables in your database:
    
    ```sql
    --
    -- Database table that stores session data.
    --
    CREATE TABLE IF NOT EXISTS sessions (
      sid  TEXT PRIMARY KEY,
      name TEXT,
      data TEXT,
      exp  TIMESTAMP WITH TIME ZONE
    );
    CREATE INDEX ON sessions (exp);
    
    --
    -- Sessions metadata table.
    --
    -- This is only needed if you want to store session metadata.
    --
    CREATE TABLE IF NOT EXISTS sessions_meta (
      aud TEXT,
      sub TEXT,
      sid TEXT REFERENCES sessions (sid) ON DELETE CASCADE ON UPDATE CASCADE,
      PRIMARY KEY (aud, sub, sid)
    );

此实现需要 pgmoon,您可以使用 LuaRocks 安装它。

    ❯ luarocks install pgmoon

Redis 配置

会话库支持单个 Redis、Redis Sentinel 和 Redis Cluster 连接。它们之间通用的配置设置

| 选项 | 默认值 | 描述 | |---------------------|:-------:|----------------------------------------------------------------------------------------------| | prefix | nil | 存储在 Redis 中的键的前缀。 | | suffix | nil | 存储在 Redis 中的键的后缀。 | | username | nil | 用于身份验证的数据库用户名。 | | password | nil | 用于身份验证的密码。 | | connect_timeout | nil | 控制 TCP/unix-domain 套接字对象 connect 方法使用的默认超时值。 | | send_timeout | nil | 控制 TCP/unix-domain 套接字对象 send 方法使用的默认超时值。 | | read_timeout | nil | 控制 TCP/unix-domain 套接字对象 receive 方法使用的默认超时值。 | | keepalive_timeout | nil | 控制连接池中连接的默认最大空闲时间。 | | pool | nil | 使用的连接池的自定义名称。 | | pool_size | nil | 连接池的大小。 | | backlog | nil | 当连接池已满时(使用 pool_size 配置)要使用的队列大小。 | | ssl | false | 是否启用 SSL | | ssl_verify | nil | 是否验证服务器证书 | | server_name | nil | 新 TLS 扩展服务器名称指示 (SNI) 的服务器名称。 |

当您不传递 sentinelsnodes 时,将选择 single redis 实现,这将导致选择 sentinelcluster 实现。

单个 Redis 配置

单个 Redis 具有以下其他配置选项(将 storage 设置为 "redis"

| 选项 | 默认值 | 描述 | |-------------|:---------------:|--------------------------------| | host | "127.0.0.1" | 要连接的主机。 | | port | 6379 | 要连接的端口。 | | socket | nil | 要连接的套接字文件。 | | database | nil | 要连接的数据库。 |

Redis Sentinels 配置

Redis Sentinel 具有以下其他配置选项(将 storage 设置为 "redis" 并配置 sentinels

| 选项 | 默认值 | 描述 | |---------------------|:--------:|--------------------------------| | master | nil | 主服务器的名称。 | | role | nil | "master""slave"。 | | socket | nil | 要连接的套接字文件。 | | sentinels | nil | Redis Sentinels。 | | sentinel_username | nil | 可选的 Sentinel 用户名。 | | sentinel_password | nil | 可选的 Sentinel 密码。 | | database | nil | 要连接的数据库。 |

sentinels 是 Sentinel 记录的数组

| 选项 | 默认值 | 描述 | |--------|:-------:|----------------------| | host | nil | 要连接的主机。 | | port | nil | 要连接的端口。 |

当您将 sentinels 作为 redis 配置的一部分传递时(并且不传递 nodes,这将选择 cluster 实现),将选择 sentinel 实现。

此实现需要 lua-resty-redis-connector,您可以使用 LuaRocks 安装它。

    ❯ luarocks install lua-resty-redis-connector

Redis Cluster 配置

Redis Cluster 具有以下其他配置选项(将 storage 设置为 "redis" 并配置 nodes

| 选项 | 默认值 | 描述 | |---------------------------|:-------:|--------------------------------------------------------| | name | nil | Redis 集群名称。 | | nodes | nil | Redis 集群节点。 | | lock_zone | nil | 锁的共享字典名称。 | | lock_prefix | nil | 锁的共享字典名称前缀。 | | max_redirections | nil | 重定向的最大重试次数。 | | max_connection_attempts | nil | 连接的最大重试次数。 | | max_connection_timeout | nil | 所有重试中连接的最大超时时间。 |

nodes 是集群节点记录的数组

| 选项 | 默认值 | 描述 | |--------|:-------------:|----------------------------| | ip | "127.0.0.1" | 要连接的 IP 地址。 | | port | 6379 | 要连接的端口。 |

当您将 nodes 作为 redis 配置的一部分传递时,将选择 cluster 实现。

为了使 cluster 正常工作,您需要配置 lock_zone,因此也将其添加到您的 Nginx 配置中

    lua_shared_dict redis_cluster_locks 100k;

并将 lock_zone 设置为 "redis_cluster_locks"

此实现需要 resty-redis-clusterkong-redis-cluster,您可以使用 LuaRocks 安装它。

    ❯ luarocks install resty-redis-cluster
    # or
    ❯ luarocks install kong-redis-cluster

SHM 配置

使用 SHM 存储,您可以使用以下设置(将 storage 设置为 "shm"

| 选项 | 默认值 | 描述 | |----------|:------------:|------------------------------------| | prefix | nil | 存储在 SHM 中的键的前缀。 | | suffix | nil | 存储在 SHM 中的键的后缀。 | | zone | "sessions" | 共享内存区域的名称。 |

您还需要在 Nginx 中创建一个共享字典 zone

    lua_shared_dict sessions 10m;

注意:您可能需要根据您的需要调整共享内存区域的大小。

API

LDoc 生成的 API 文档也可以在 bungle.github.io/lua-resty-session 查看。

初始化

session.init

语法: session.init(configuration)

初始化会话库。

此函数可以在 OpenResty 的 initinit_worker 阶段调用,以将全局默认配置设置为该库创建的所有会话实例。

    require "resty.session".init({
      audience = "my-application",
      storage = "redis",
      redis = {
        username = "session",
        password = "storage",
      },
    })

有关可能的配置设置,请参阅 configuration

构造函数

session.new

语法: session = session.new(configuration)

创建一个新的会话实例。

    local session = require "resty.session".new()
    -- OR
    local session = require "resty.session".new({
      audience = "my-application",
    })

有关可能的配置设置,请参阅 configuration

辅助函数

session.open

语法: session, err, exists = session.open(configuration)

这可以用来打开一个会话,它将返回一个现有的会话或一个新的会话。exists(布尔值)返回值参数指示它是返回的现有会话还是新会话。err(字符串)包含打开可能失败的原因的消息(该函数也将返回 session)。

    local session = require "resty.session".open()
    -- OR
    local session, err, exists = require "resty.session".open({
      audience = "my-application",
    })

有关可能的配置设置,请参阅 configuration

session.start

语法: session, err, exists, refreshed = session.start(configuration)

这可以用来启动一个会话,它将返回一个现有的会话或一个新的会话。如果存在现有会话,则会话也将刷新(根据需要)。exists(布尔值)返回值参数指示它是返回的现有会话还是新会话。refreshed(布尔值)指示对 refresh 的调用是否成功。err(字符串)包含打开或刷新可能失败的原因的消息(该函数也将返回 session)。

    local session = require "resty.session".start()
    -- OR
    local session, err, exists, refreshed = require "resty.session".start({
      audience = "my-application",
    })

有关可能的配置设置,请参阅 configuration

session.logout

语法: ok, err, exists, logged_out = session.logout(configuration)

它注销特定受众。

单个会话 Cookie 可能会在多个受众(或应用程序)之间共享,因此需要能够仅从单个受众注销,同时保留其他受众的会话。exists(布尔值)返回值参数指示会话是否存在。logged_out(布尔值)返回值参数表示会话是否存在以及是否已注销。err(字符串)包含会话不存在或注销失败的原因。当会话存在并成功注销时,ok(真值)将为true

当只有一个受众时,这可以被认为等同于session.destroy

当最后一个受众注销时,Cookie 也将被销毁并在客户端上失效。

    require "resty.session".logout()
    -- OR
    local ok, err, exists, logged_out = require "resty.session".logout({
      audience = "my-application",
    })

有关可能的配置设置,请参阅 configuration

session.destroy

语法: ok, err, exists, destroyed = session.destroy(configuration)

它销毁整个会话并清除 Cookie。

单个会话 Cookie 可能会在多个受众(或应用程序)之间共享,因此需要能够仅从单个受众注销,同时保留其他受众的会话。exists(布尔值)返回值参数指示会话是否存在。destroyed(布尔值)返回值参数表示会话是否存在以及是否已销毁。err(字符串)包含会话不存在或注销失败的原因。当会话存在并成功注销时,ok(真值)将为true

    require "resty.session".destroy()
    -- OR
    local ok, err, exists, destroyed = require "resty.session".destroy({
      cookie_name = "auth",
    })

有关可能的配置设置,请参阅 configuration

实例方法

session:open

语法: ok, err = session:open()

这可用于打开会话。当会话打开并验证时,它返回true。否则,它返回nil和错误消息。

    local session = require "resty.session".new()
    local ok, err = session:open()
    if ok then
      -- session exists
      
    else
      -- session did not exists or was invalid
    end

session:save

语法: ok, err = session:save()

保存会话数据并使用新的会话 ID 发出新的会话 Cookie。当启用remember时,它还会发出新的持久 Cookie,并可能将数据保存在后端存储中。当会话保存时,它返回true。否则,它返回nil和错误消息。

    local session = require "resty.session".new()
    session:set_subject("john")
    local ok, err = session:save()
    if not ok then
      -- error when saving session
    end

session:touch

语法: ok, err = session:touch()

通过发送更新的会话 Cookie 更新会话的空闲偏移量。它仅发送客户端 Cookie,从不调用任何后端会话存储 API。通常使用session:refresh间接调用此方法。在错误情况下,它返回nil和错误消息,否则返回true

    local session, err, exists = require "resty.session".open()
    if exists then
      ok, err = session:touch()
    end

session:refresh

语法: ok, err = session:refresh()

根据滚动超时是否越来越近,要么保存会话(创建新的会话 ID),要么触碰会话,这意味着默认情况下,当滚动超时的 3/4 已用完时,即默认滚动超时为一小时时的 45 分钟。触碰有一个阈值,默认为一分钟,因此在某些情况下可能会跳过(您可以调用session:touch()强制执行)。在错误情况下,它返回nil和错误消息,否则返回true

    local session, err, exists = require "resty.session".open()
    if exists then
      local ok, err = session:refresh()
    end

以上代码看起来有点像session.start()帮助器。

session:logout

语法: ok, err = session:logout()

注销要么销毁会话,要么只清除当前受众的数据,并保存它(从当前受众注销)。在错误情况下,它返回nil和错误消息,否则返回true

    local session, err, exists = require "resty.session".open()
    if exists then
      local ok, err = session:logout()
    end

session:destroy

语法: ok, err = session:destroy()

销毁会话并清除 Cookie。在错误情况下,它返回nil和错误消息,否则返回true

    local session, err, exists = require "resty.session".open()
    if exists then
      local ok, err = session:destroy()
    end

session:close

语法: session:close()

只关闭会话实例,以便它不能再被使用。

    local session = require "resty.session".new()
    session:set_subject("john")
    local ok, err = session:save()
    if not ok then
      -- error when saving session
    end
    session:close()

session:set_data

语法: session:set_data(data)

设置会话数据。data需要是一个table

    local session, err, exists = require "resty.session".open()
    if not exists then
       session:set_data({
         cart = {},
       })
      session:save()
    end

session:get_data

语法: data = session:get_data()

获取会话数据。

    local session, err, exists = require "resty.session".open()
    if exists then
      local data = session:get_data()
      ngx.req.set_header("Authorization", "Bearer " .. data.access_token)
    end

session:set

语法: session:set(key, value)

在会话中设置一个值。

    local session, err, exists = require "resty.session".open()
    if not exists then
      session:set("access-token", "eyJ...")
      session:save()
    end

session:get

语法: value = session:get(key)

从会话中获取一个值。

    local session, err, exists = require "resty.session".open()
    if exists then
      local access_token = session:get("access-token")
      ngx.req.set_header("Authorization", "Bearer " .. access_token)
    end

session:set_audience

语法: session:set_audience(audience)

设置会话受众。

    local session = require "resty.session".new()
    session.set_audience("my-service")

session:get_audience

语法: audience = session:get_audience()

设置会话主体。

session:set_subject

语法: session:set_subject(subject)

设置会话受众。

    local session = require "resty.session".new()
    session.set_subject("[email protected]")

session:get_subject

语法: subject = session:get_subject()

获取会话主体。

    local session, err, exists = require "resty.session".open()
    if exists then
      local subject = session.get_subject()
    end

session:get_property

语法: value = session:get_property(name)

获取会话属性。可能的属性名称

  • "id":43 字节会话 ID(与 nonce 相同,但为 base64 url 编码)

  • "nonce":32 字节 nonce(与会话 ID 相同,但为原始字节)

  • "audience":当前会话受众

  • "subject":当前会话主体

  • "timeout":最接近的超时(以秒为单位)(剩余时间)

  • "idling-timeout"`:会话空闲超时(以秒为单位)(剩余时间)

  • "rolling-timeout"`:会话滚动超时(以秒为单位)(剩余时间)

  • "absolute-timeout"`:会话绝对超时(以秒为单位)(剩余时间)

注意:返回的值可能是nil

    local session, err, exists = require "resty.session".open()
    if exists then
      local timeout = session.get_property("timeout")
    end

session:set_remember

语法: session:set_remember(value)

设置持久会话的开启/关闭。

在许多登录表单中,用户可以选择“记住我”。您可以根据用户选择调用此函数。

    local session = require "resty.session".new()
    if ngx.var.args.remember then
      session:set_remember(true)
    end
    session:set_subject(ngx.var.args.username)
    session:save()

session:get_remember

语法: remember = session:get_remember()

获取持久会话的状态。

    local session, err, exists = require "resty.session".open()
    if exists then
      local remember = session.get_remember()
    end

语法: session:clear_request_cookie()

通过删除与会话相关的 Cookie 来修改请求头。当您在代理服务器上使用会话库并且不希望会话 Cookie 被转发到上游服务时,这很有用。

    local session, err, exists = require "resty.session".open()
    if exists then
      session:clear_request_cookie()
    end

session:set_headers

语法: session:set_headers(arg1, arg2, ...)

根据配置设置请求和响应头。

    local session, err, exists = require "resty.session".open({
      request_headers = { "audience", "subject", "id" },
      response_headers = { "timeout", "idling-timeout", "rolling-timeout", "absolute-timeout" },
    })
    if exists then
      session:set_headers()
    end

在不带参数调用时,它将设置使用request_headers配置的请求头和使用response_headers配置的响应头。

有关可能的标题名称,请参阅配置

session:set_request_headers

语法: session:set_request_headers(arg1, arg2, ...)

设置请求头。

    local session, err, exists = require "resty.session".open()
    if exists then
      session:set_request_headers("audience", "subject", "id")
    end

在不带参数调用时,它将设置使用request_headers配置的请求头。

有关可能的标题名称,请参阅配置

session:set_response_headers

语法: session:set_response_headers(arg1, arg2, ...)

设置请求头。

    local session, err, exists = require "resty.session".open()
    if exists then
      session:set_response_headers("timeout", "idling-timeout", "rolling-timeout", "absolute-timeout")
    end

在不带参数调用时,它将设置使用response_headers配置的请求头。

有关可能的标题名称,请参阅配置

session.info:set

语法: session.info:set(key, value)

在会话信息存储中设置一个值。会话信息存储可用于您希望在服务器端存储中存储数据但不想创建新会话并发送新会话 Cookie 的场景。在检查身份验证标签或消息身份验证代码时,不会考虑信息存储数据。因此,如果您想将此用于需要加密的数据,则需要在将其传递给此函数之前加密值。

    local session, err, exists = require "resty.session".open()
    if exists then
      session.info:set("last-access", ngx.now())
      session.info:save()
    end

使用 Cookie 存储,这仍然有效,但它几乎与session:set相同。

session.info:get

语法: value = session.info:get(key)

从会话信息存储中获取一个值。

    local session, err, exists = require "resty.session".open()
    if exists then
      local last_access = session.info:get("last-access")
    end

session.info:save

语法: value = session.info:save()

保存信息。仅更新后端存储。不发送新 Cookie(除了使用 Cookie 存储)。

    local session = require "resty.session".new()
    session.info:set("last-access", ngx.now())
    local ok, err = session.info:save()

Cookie 格式

    [ HEADER -------------------------------------------------------------------------------------]
    [ Type || Flags || SID || Created at || Rolling Offset || Size || Tag || Idling Offset || Mac ]
    [ 1B   || 2B    || 32B || 5B         || 4B             || 3B   || 16B || 3B            || 16B ]

    [ PAYLOAD --]
    [ Data  *B  ]   

在放入Set-Cookie头之前,HEADERPAYLOAD都经过 base64 url 编码。当使用服务器端存储时,PAYLOAD不会放入 Cookie 中。使用 Cookie 存储时,base64 url 编码的头与 base64 url 编码的有效负载连接。

HEADER是固定大小的 82 字节二进制数据或 base64 url 编码形式的 110 字节。

头字段说明

  • 类型:数字1以小端字节序打包在一个字节中(目前唯一支持的type)。

  • 标志:以小端字节序打包的二进制标志(短整型),占用两个字节。

  • SID:32字节的加密随机数据(会话 ID)。

  • 创建时间:以小端字节序打包的从纪元开始的秒数,截断为 5 个字节。

  • 滚动偏移量:以小端字节序打包的从创建时间开始的秒数(整数)。

  • 大小:以小端字节序打包的数据大小(短整型),占用两个字节。

  • 标签:16字节的数据 AES-256-GCM 加密的身份验证标签。

  • 空闲偏移量:以小端字节序打包的从创建时间 + 滚动偏移量开始的秒数,截断为 3 个字节。

  • Mac:16字节报文鉴别码,对头的报文鉴别码。

数据加密

  1. 初始密钥材料 (IKM)

    1. 通过对secret使用 SHA-256 哈希来派生secret的 IKM,或者

    2. 使用 32 字节 IKM,当使用ikm传递给库时

  2. 生成 32 字节的加密随机会话 ID (sid)

  3. 使用 HKDF 和 SHA-256 派生 32 字节的加密密钥和 12 字节的初始化向量(在 FIPS 模式下,它使用 PBKDF2 和 SHA-256 代替)

    1. 使用 HKDF extract 从ikm派生一个新密钥以获取key(此步骤每个ikm可以只执行一次)

      • 输出长度:32

      • 摘要:"sha256"

      • 密钥:<ikm>

      • 模式:仅提取

      • 信息:""

      • 盐:""

    2. 使用 HKDF expand 派生44字节的output

      • 输出长度:44

      • 摘要:"sha256"

      • 密钥:<key>

      • 模式:仅扩展

      • 信息:"encryption:<sid>"

      • 盐:""

    3. output的前 32 个字节是加密密钥 (aes-key),后 12 个字节是初始化向量 (iv)

  4. 使用 AES-256-GCM 加密plaintext(JSON 编码并可选地解压缩)以获取ciphertexttag

    1. 密码:"aes-256-gcm"

    2. 密钥:<aes-key>

    3. iv:<iv>

    4. 明文:<plaintext>

    5. aad:使用header的前 47 个字节作为aad,包括

      1. 类型

      2. 标志

      3. 会话 ID

      4. 创建时间

      5. 滚动偏移量

      6. 数据大小

步骤 3 中的remember Cookie 有一个变体,我们可能会使用PBKDF2而不是HKDF,具体取决于remember_safety设置(我们还在 FIPS 模式下使用它)。PBKDF2设置

  • 输出长度:44

  • 摘要:"sha256"

  • 密码:<key>

  • 盐:"encryption:<sid>"

  • 迭代次数:<1000|10000|100000|1000000>

迭代次数基于remember_safety设置("Low""Medium""High""Very High"),如果remember_safety设置为"None",我们将使用上述 HDKF。

Cookie 头部身份验证

  1. 使用 HKDF 和 SHA-256 派生 32 字节的身份验证密钥 (mac_key)(在 FIPS 模式下,它使用 PBKDF2 和 SHA-256 代替)

    1. 使用 HKDF extract 从ikm派生一个新密钥以获取key(此步骤每个ikm可以只执行一次并与加密密钥生成重复使用)

      • 输出长度:32

      • 摘要:"sha256"

      • 密钥:<ikm>

      • 模式:仅提取

      • 信息:""

      • 盐:""

    2. 使用 HKDF expand 派生32字节的mac-key

      • 输出长度:32

      • 摘要:"sha256"

      • 密钥:<key>

      • 模式:仅扩展

      • 信息:"authentication:<sid>"

      • 盐:""

  2. 使用 HMAC-SHA256 计算消息身份验证代码

    • 摘要:"sha256"

    • 密钥:<mac-key>

    • 消息:使用header的前 66 个字节,包括

      1. 类型

      2. 标志

      3. 会话 ID

      4. 创建时间

      5. 滚动偏移量

      6. 数据大小

      7. 标签

      8. 空闲偏移量

自定义存储接口

如果您想实现自定义存储,则需要实现以下接口

`lua

-- <custom> 会话库的后端

-- @module <custom>

-- 存储 -- @section instance

local metatable = {}

metatable.__index = metatable

function metatable.__newindex() error("尝试更新只读表", 2)

end

-- 存储会话数据。

-- @function instance:set -- @tparam string name cookie名称 -- @tparam string key 会话键 -- @tparam string value 会话值 -- @tparam number ttl 会话ttl -- @tparam number current_time 当前时间 -- @tparam[opt] string old_key 旧会话ID -- @tparam string stale_ttl 过期ttl -- @tparam[opt] table metadata 元数据表 -- @tparam boolean remember 是否存储持久会话 -- @treturn true|nil ok -- @treturn string 错误消息 function metatable:set(name, key, value, ttl, current_time, old_key, stale_ttl, metadata, remember) -- NYI

end

-- 检索会话数据。

-- @function instance:get -- @tparam string name cookie名称 -- @tparam string key 会话键 -- @treturn string|nil 会话数据 -- @treturn string 错误消息 function metatable:get(name, key) -- NYI

end

-- 删除会话数据。

-- @function instance:delete -- @tparam string name cookie名称 -- @tparam string key 会话键 -- @tparam[opt] table metadata 会话元数据 -- @treturn boolean|nil 会话数据 -- @treturn string 错误消息 function metatable:delete(name, key, current_time, metadata) -- NYI end

local storage = {}

-- 构造函数

-- @section constructors

-- 配置

-- @section configuration

-- <custom> 存储后端配置 -- @field <field-name> 待定

-- @table configuration

-- 创建一个 <custom> 存储。

-- 这将创建一个新的共享内存存储实例。

-- @function module.new -- @tparam[opt] table configuration <custom> 存储 @{configuration} -- @treturn table <custom> 存储实例 function storage.new(configuration) -- NYI -- return setmetatable({}, metatable) end

return storage

    Please check the existing implementations for the defails. And please
    make a pull-request so that we can integrate it directly to library
    for other users as well.
    
    
    # License
    
    `lua-resty-session` uses two clause BSD license.
    

版权所有 (c) 2014 – 2023 Aapo Talvensaari,2022 – 2023 Samuele Illuminati 保留所有权利。

在满足以下条件的情况下,允许以源代码和二进制形式重新分发和使用,无论是否修改。

  • 源代码的再分发必须保留上述版权声明、此条件列表以及以下免责声明。

  • 二进制形式的再分发必须在随分发提供的文档和/或其他材料中复制上述版权声明、此条件列表以及以下免责声明。

本软件由版权持有人和贡献者“按原样”提供,并且不提供任何明示或暗示的担保,包括但不限于适销性和特定用途适用性的暗示担保。在任何情况下,版权持有人或贡献者均不对任何直接、间接、附带、特殊、惩戒性或后果性损害负责

作者

Aapo Talvensaari (@bungle),Samuele Illuminati (@samugi)

许可证

2bsd

依赖项

hamishforbes/lua-ffi-zlib >= 0.5,fffonion/lua-resty-openssl >= 0.8.0,luajit,nginx,ngx_http_lua

版本