lucinda/security

实现常见Web安全模式(例如:身份验证、授权)的API,适用于基于OWASP指南的PHP应用程序

v4.1.4 2022-12-25 18:03 UTC

README

关于

该API是一个 骨架(需要开发者的绑定),它根据OWASP指南实现了Web安全(身份验证、授权、状态持久化、CSRF预防)的常见问题,提供了多个绑定点,开发者必须使用其原型插件组件以完成流程(例如:数据库身份验证)。

diagram

它通过以下步骤实现

  • 配置:设置一个XML文件以配置Web安全
  • 绑定点:将XML/代码中定义的用户自定义组件绑定到API原型
  • 执行:创建一个Wrapper实例以进行身份验证和授权,然后使用它获取登录用户ID、访问令牌(无状态应用程序)或CSRF令牌(表单登录)

API完全遵循PSR-4规范,仅需要PHP 8.1+解释器和SimpleXML + OpenSSL扩展。要快速了解其工作原理,请查看

  • 安装:描述了如何在计算机上安装API,考虑到上述步骤
  • 单元测试:API具有100%的单元测试覆盖率,使用UnitTest API代替PHPUnit以提供更大的灵活性
  • 示例:基于单元测试展示了Wrapper的功能

内部所有类都属于 Lucinda\WebSecurity 命名空间!

配置

要配置此API,您必须有一个包含以下标签的XML文件

  • 安全:配置API(必需)
  • 用户:如果通过XML进行身份验证,则为可选(仅当使用访问控制列表时需要)
  • 路由:如果通过XML进行授权,则为可选(仅当使用访问控制列表时需要)

安全

此标签的最大语法为

<security>
    <csrf secret="..." expiration="..."/>
    <persistence>
        <session parameter_name="..." expiration="..." is_http_only="..." is_https_only="..." ignore_ip="..." handler="..."/>
        <remember_me secret="..." parameter_name="..."  expiration="..." is_http_only="..." is_https_only="..."/>
        <synchronizer_token secret="..." expiration="..." regeneration="..."/>
        <json_web_token secret="..." expiration="..." regeneration="..."/>
    </persistence>
    <authentication>
        <form dao="..." throttler="...">
            <login page="..." target="..." parameter_username="..." parameter_password="..."  parameter_rememberMe="..." />
            <logout page="..." target="..."/>
        </form>
        <oauth2 dao="..." target="..." login="..." logout="..."/>
    </authentication>
    <authorization>
        <by_dao page_dao="..." user_dao="..." logged_in_callback="..." logged_out_callback="..."/>
        <by_route logged_in_callback="..." logged_out_callback="..."/>
    </authorization>
</security>

其中

  • 安全:包含全局Web安全策略(必需)。
    • csrf:包含生成反CSRF令牌所需的设置(用于签名身份验证很有用)
      • secret:用于加密CSRF令牌的密码(使用:Token\SaltGenerator
      • 过期时间:可选。令牌过期的秒数。如果未设置,则令牌将在10分钟后过期。
    • 持久性:包含一个或多个用于在请求之间保留登录状态的机制(至少需要有一个!)
      • session:(可选)通过HTTP会话配置登录状态的持久性
        • parameter_name:(可选)用于存储登录状态的 $_SESSION 参数的名称。如果未设置,则默认为 "uid"。
        • expiration:(可选)会话到期前的秒数。如果未设置,则根据服务器的默认值到期。
        • is_http_only:(可选)是否将会话cookie设置为HttpOnly(可以是0或1;默认为0)。
        • is_https_only:(可选)是否将会话cookie设置为仅HTTPS(可以是0或1;默认为0)。
        • handler:(可选)实现 SessionHandlerInterface 的类的名称(包括命名空间或相对路径),会将会话处理委托给该类。
      • remember_me:(可选)通过HTTP remember me cookie配置登录状态的持久性
        • secret:(必选)用于加密cookie的密码(使用:Token\SaltGenerator
        • parameter_name:(可选)用于存储登录状态的 $_COOKIE 参数的名称。如果未设置,则默认为 "uid"。
        • expiration:(可选)cookie到期前的秒数。如果未设置,则cookie将在一天后到期。
        • is_http_only:(可选)是否将cookie设置为HttpOnly(可以是0或1;默认为0)。
        • is_https_only:(可选)是否将cookie设置为仅HTTPS(可以是0或1;默认为0)。
      • synchronizer_token:(可选)通过在每个请求上签名同步器令牌配置登录状态的持久性
        • secret:(必选)用于加密令牌的密码(使用:Token\SaltGenerator
        • expiration:(可选)令牌到期前的秒数。如果未设置,则令牌将在1小时内到期。
        • regeneration:(可选)从令牌创建时刻起,直到需要因连续使用而重新生成令牌的秒数。如果未设置,则令牌将在1分钟后重新生成。
      • json_web_token:(可选)通过在每个请求上签名JSON Web Token配置登录状态的持久性
        • secret:(必选)用于加密令牌的密码(使用:Token\SaltGenerator
        • expiration:(可选)令牌到期前的秒数。如果未设置,则令牌将在1小时内到期。
        • regeneration:(可选)从令牌创建时刻起,直到需要因连续使用而重新生成令牌的秒数。如果未设置,则令牌将在1分钟后重新生成。
    • authentication:(必选)包含一个或多个用于认证的机制(至少需要一个!)
      • form:(可选)通过表单配置认证。如果没有设置 dao 属性,则通过XML和 users 标签进行认证!
        • dao:(可选)实现数据库中表单认证的 Authentication\DAO\UserAuthenticationDAO 的PSR-4自动加载兼容类的名称(包括命名空间)。[1]
        • throttler:(可选)扩展 Authentication\Form\LoginThrottler 以执行登录限制预防的PSR-4自动加载兼容类的名称(包括命名空间)
        • login:(可选)配置登录
          • page:(可选)执行登录操作的页面(所有发送到该页面的请求都将通过此过滤器),也用于登录失败后重定向的页面。如果没有设置,则默认使用 "login"。
          • target:(可选)登录成功后的目标页面。如果没有设置,则默认使用 "index"。
          • parameter_username:(可选)提交用户名的 $_POST 参数的名称。如果没有设置,则默认使用 "username"。
          • parameter_password:(可选)提交密码的 $_POST 参数的名称。如果没有设置,则默认使用 "password"。
          • parameter_rememberMe:(可选)激活 "记住我" 选项的 $_POST 参数的名称(值可以是0或1)。如果没有设置,则默认使用 "remember_me"。
        • logout:(可选)配置注销
          • page:(可选)执行注销操作的页面(所有发送到该页面的请求都将通过此过滤器)。如果没有设置,则默认使用 "logout"。
          • target:(可选)注销成功或失败后的目标页面。如果没有设置,则默认使用 "login"。
      • oauth2: (可选) 配置通过 oauth2 提供商进行身份验证
        • dao: (必填) PSR-4 自动加载符合规范类的名称(包括命名空间),该类实现 Authentication\OAuth2\VendorAuthenticationDAO 并将身份验证结果保存在数据库中
        • target:(可选)登录成功后的目标页面。如果没有设置,则默认使用 "index"。
        • login: (可选) 提供由提供者选项的通用登录页面。如果没有,则隐式使用“login”。
        • logout: (可选) 执行注销操作的页面。如果没有,则隐式使用“logout”。
    • authorization: (必填) 包含授权请求的单个机制(至少一个必填!)
      • by_dao: (可选) 配置通过数据库进行授权
        • page_dao: (必填) PSR-4 自动加载符合规范类的名称(包括命名空间),该类扩展 Authorization\DAO\PageAuthorizationDAO 并在数据库中检查用户权限
        • user_dao: (必填) PSR-4 自动加载符合规范类的名称(包括命名空间),该类扩展 Authorization\DAO\UserAuthorizationDAO 并在数据库中检查页面权限
        • logged_in_callback: (可选) 当授权失败时,已认证用户调用的回调页面。如果没有,则隐式使用“index”。
        • logged_out_callback: (可选) 当授权失败时,访客用户调用的回调页面。如果没有,则隐式使用“login”。
      • by_route: (可选) 配置通过 XML 进行授权,在这种情况下需要 routes 标签。[1]
        • logged_in_callback: (可选) 当授权失败时,已认证用户调用的回调页面。如果没有,则隐式使用“index”。
        • logged_out_callback: (可选) 当授权失败时,访客用户调用的回调页面。如果没有,则隐式使用“login”。

有关 XML 的示例,请检查 WrapperTest @ 单元测试!

注意:(1)如果授权是通过 by_route,并且 authenticationform(带有 dao 属性),则此处引用的类也必须实现 Authorization\UserRoles

用户

如果使用 XML 身份验证(存在 form 标签且没有 dao 属性)+ 授权(存在 by_route 标签),则此标签是必需的。语法是

<users roles="...">
    <user id="..." username="..." password="..." roles="..."/>
    ...
</security>

其中

  • users: (必填) 包含网站用户列表,每个用户通过一个 user 标签标识
    • roles: (必填) 包含访客(未登录用户)所属角色的列表,用逗号分隔
    • user: (必填) 包含单个用户的信息
      • id: (必填) 包含唯一的用户标识符(例如:1)
      • username: (可选) 包含用户名(例如:john_doe)。对于 XML 身份验证是必需的!
      • password: (可选) 包含使用 password_hash(例如:`php password_hash("doe", PASSWORD_BCRYPT)`)散列的用户密码。对于 XML 身份验证是必需的!
      • roles: (可选) 包含用户所属角色的列表,用逗号分隔(例如:USERS, ADMINISTRATORS)。对于 XML 身份验证+授权是必需的

如果在上面的列表中没有检测到用户,则自动假定 GUEST 角色!

路由

如果使用 XML 授权(存在 by_route 标签),则此标签是必需的。语法是

<routes roles="...">
    <route id="..." roles="..."/>
    ...
</routes>

其中

  • routes: (必填) 包含网站路由列表,每个路由通过一个 route 标签标识
    • roles: (必填) 包含默认假定所有页面所属角色的列表,用逗号分隔(例如:GUEST)
    • route: (必填) 包含特定路由的策略
      • id: (必填) 页面相对 URL(例如:administration)
      • roles: (必填) 包含与页面关联的角色的列表,用逗号分隔(例如:USERS, ADMINISTRATORS)

绑定点

为了保持灵活性和实现最高性能,API 仅做出绝对必要的假设!相反,它提供给开发者一种将绑定到其原型的能力,以获得某些功能。

声明式绑定

它为开发者提供了通过 XML 声明式绑定 到其原型类/接口的能力

程序式绑定

它为开发者提供了一种通过包装器构造函数程序化绑定到其原型的能力。

执行

一旦完成配置,就可以最终使用此API通过调用包装器进行认证和授权,它定义了以下公共方法

认证和授权都需要在事先设置以下对象并注入构造函数中

如果认证/授权达到需要重定向请求的点,构造函数会抛出SecurityPacket。它还可能抛出

处理SecurityPacket

非无状态应用程序的开发者应该使用类似以下内容处理此异常

try {
	// sets $xml and $request
	$object = new Lucinda\WebSecurity\Wrapper($xml, $request);
	// operate with $object to retrieve information
} catch (SecurityPacket $e) {
	header("Location: ".$e->getCallback()."?status=".$e->getStatus()."&penalty=".((integer) $e->getTimePenalty()));
	exit();
}

然而,无状态Web服务应用程序的开发者应该使用类似以下内容处理此异常

try {
	// sets $xml and $request
	$object = new Lucinda\WebSecurity\Wrapper($xml, $request);
	// use $object to produce a response
} catch (SecurityPacket $e) {
	echo json_encode(["status"=>$e->getStatus(), "callback"=>$e->getCallback(), "penalty"=>(integer) $e->getTimePenalty(), "access_token"=>$e->getAccessToken()]);
	exit();
	// front end will handle above code and make a redirection
}

处理其他异常

它们可以按照以下方式处理

use Lucinda\WebSecurity;

try {
	// sets $xml and $request
	$object = new Wrapper($xml, $request);
	// process $object
} catch (SecurityPacket $e) {
	// handle security packet as above
} catch (Authentication\Form\Exception $e) {
	// respond with a 400 Bad Request HTTP status (it's either foul play or misconfiguration)
} catch (PersistenceDrivers\Session\HijackException $e) {
	// respond with a 400 Bad Request HTTP status (it's always foul play)
} catch (Token\EncryptionException $e) {
	// respond with a 400 Bad Request HTTP status (it's always foul play)
} catch (Token\Exception $e) {
	// respond with a 400 Bad Request HTTP status (it's either foul play or misconfiguration)
} catch (ConfigurationException $e) {
	// show stack trace and exit (it's misconfiguration)
} catch (Authentication\OAuth2\Exception $e) {
	// handle as you want (error received from OAuth2 vendor usually from user's decision not to approve your access)
}

安装

首先选择一个文件夹,将其关联到域名,然后在该文件夹中使用控制台运行以下命令

composer require lucinda/security

然后创建一个包含配置设置的configuration.xml文件(见上文的配置)和一个index.php文件(见上文的获取结果),并在项目根目录中写入以下代码

$request = new Lucinda\WebSecurity\Request();
$request->setIpAddress($_SERVER["REMOTE_ADDR"]);
$request->setUri($_SERVER["REQUEST_URI"]!="/"?substr($_SERVER["REQUEST_URI"],1):"index");
$request->setMethod($_SERVER["REQUEST_METHOD"]);
$request->setParameters($_POST);
$request->setAccessToken(isset($_SERVER["HTTP_AUTHORIZATION"]) && stripos($_SERVER["HTTP_AUTHORIZATION"], "Bearer ")===0?trim(substr($_SERVER["HTTP_AUTHORIZATION"], 7)):"");

try {
	// sets $xml and $request
	$object = new Lucinda\WebSecurity\Wrapper(simplexml_load_file("configuration.xml"), $request);
	// operate with $object to retrieve information
} catch (Lucinda\WebSecurity\SecurityPacket $e) {
	header("Location: ".$e->getCallback()."?status=".$e->getStatus()."&penalty=".((integer) $e->getTimePenalty()));
	exit();
}

然后将此文件作为引导文件并开始开发MVC模式

RewriteEngine on
RewriteRule ^(.*)$ index.php

单元测试

有关测试和示例,请检查API源中的以下文件/文件夹

参考指南

类SecurityPacket

SecurityPacket类封装了响应认证/授权事件,通常需要重定向,并定义了以下与开发者相关的方法

getStatus的值取决于枚举参数的值

使用示例

https://github.com/aherne/lucinda-framework/blob/master/src/Controllers/SecurityPacket.php

类Request

Request 对象封装了用于认证和授权的请求信息,以下是通过以下公共方法实现的:

使用示例

https://github.com/aherne/lucinda-framework-engine/blob/master/src/RequestBinder.php

OAuth2 驱动接口

Authentication\OAuth2\Driver 接口封装了一个 OAuth2 服务提供商进行认证,并定义了以下方法:

使用示例

https://github.com/aherne/lucinda-framework-engine/blob/master/src/OAuth2/AbstractSecurityDriver.php

OAuth2 用户信息接口

Authentication\OAuth2\UserInformation 接口包含通过以下方法检索 OAuth2 提供商上登录用户信息的蓝图:

使用示例

https://github.com/aherne/lucinda-framework-engine/blob/master/src/OAuth2/AbstractUserInformation.php

OAuth2 服务提供商认证 DAO 接口

Authentication\OAuth2\VendorAuthenticationDAO 接口包含通过以下方法保存 OAuth2 提供商上登录用户信息的蓝图:

使用示例

https://github.com/aherne/lucinda-framework-configurer/blob/master/files/models/dao/UsersOAuth2Authentication.php

用户认证 DAO 接口

Authentication\DAO\UserAuthenticationDAO 接口包含通过以下方法实现基于表单的数据库认证的蓝图:

使用示例

https://github.com/aherne/lucinda-framework-configurer/blob/master/files/models/dao/UsersFormAuthentication1.php

登录限制器抽象类

Authentication\Form\LoginThrottler 抽象类封装了在数据源(SQL 或 NoSQL)上执行表单登录限制算法(针对暴力攻击)的以下公共方法

必须扩展此类以实现以下抽象受保护方法

使用示例

https://github.com/aherne/lucinda-framework-engine/blob/master/src/AbstractLoginThrottler.php https://github.com/aherne/lucinda-framework-configurer/blob/master/files/models/dao/SqlLoginThrottler.php

用户授权 DAO 抽象类

Authorization\DAO\UserAuthorizationDAO 抽象类封装了在数据库中检查用户账户的数据库授权,以下是通过以下公共方法实现的:

必须扩展此类以实现以下抽象方法

使用示例

https://github.com/aherne/lucinda-framework-configurer/blob/master/files/models/dao/UsersAuthorization1.php

页面授权 DAO 抽象类

Authorization\DAO\PageAuthorizationDAO 抽象类封装了在数据库中检查访问控制列表的数据库授权,以下是通过以下公共方法实现的:

必须扩展此类以实现以下抽象方法

使用示例

https://github.com/aherne/lucinda-framework-configurer/blob/master/files/models/dao/PagesAuthorization.php

用户角色接口

Authorization\UserRoles 接口定义了任何授权的蓝图,其中通过以下公共方法在数据库中检查用户角色:

使用示例

https://github.com/aherne/lucinda-framework-configurer/blob/master/files/models/dao/UsersFormAuthentication3.php