此可插拔身份验证服务(PAS)插件将在预定次数的错误尝试后锁定登录。锁定后,用户将看到一个页面,告知他们联系管理员解锁。
项目描述
LoginLockout
此可插拔身份验证服务(PAS)插件将在预定次数的错误尝试后锁定登录。锁定后,用户将看到一个页面,告知他们联系管理员解锁。
需要
PluggableAuthService 及其依赖项
(可选) PlonePAS 及其依赖项
(可选) Plone 4.1.x-6.0.x
功能
在锁定之前可配置的允许的错误尝试次数
在可配置的时间(“重置周期”)后,账户将再次可用。如果重置周期后的第一次登录尝试无效,则无效登录计数器设置为1。
用户会看到一个消息,说明账户已被锁定以及锁定时间长度。(不显示剩余时间,仅显示总锁定时间。)
您可以限制用户只能来自特定的IP网络。您不需要使用错误登录尝试来使用此功能。
配置
注意 如果您从0.4.0升级,您将需要运行升级或手动重置PAS插件的顺序,如下所示,因为这已更改。
您可以使用此插件与Zope一起使用而无需Plone,或者与Plone一起使用。当您使用Plone时,您将通过Plone注册表(plone 5+)或通过portal_properties(plone 4)进行配置。
转到Plone控制面板 -> 登录锁定设置,在那里您可以更改这些默认值
>>> admin_browser = make_admin_browser('/') >>> admin_browser.getLink('Site Setup').click() >>> admin_browser.getLink('LoginLockout').click() >>> admin_browser.getLink('Lockout Settings').click()
允许的错误尝试次数:3
重置周期:24小时
whitelist_ips: [] # 允许任何来源IP
伪造客户端IP:false
>>> admin_browser.getControl("Max Attempts").value '3' >>> admin_browser.getControl("Reset Period (hours)").value '24.0' >>> admin_browser.getControl('Lock logins to IP Ranges').value '' >>> admin_browser.getControl('Fake Client IP').selected False
确保设置确实已更改
>>> admin_browser.getControl('Fake Client IP').selected = True >>> get_loginlockout_settings().fake_client_ip False >>> admin_browser.getControl('Save').click() >>> 'Changes saved.' in admin_browser.contents True >>> get_loginlockout_settings().fake_client_ip True
详情
LoginLockout可以作为Plone插件使用,也可以与zope和PAS单独使用。首先,我们将向您展示它如何与Plone一起工作。
安装
通过添加/删除产品将此产品安装到Plone中。如果您要将此产品安装到没有Plone的Zope中,则您需要遵循以下手动安装步骤。
这将安装并激活两个PAS插件。
手动安装
此插件需要安装两个位置,实例PAS(登录发生的位置)和根acl_users。
1. 将产品目录“LoginLockout”放置在您的“Products/”目录中。重启Zope。
2. 在您的实例PAS“acl_users”中,从添加列表中选择“LoginLockout”。给它一个ID和标题,然后按添加按钮。
3. 在添加后屏幕中启用“Authentication”和“Update Credentials”插件接口。
对您的根PAS重复上述步骤,但将其作为插件添加到
Anonymoususerfactory
并确保LoginLockout是第一个Anonymoususerfactory插件
以下步骤2至4将由Plone安装程序为您完成。
就是这样!试试看。
Plone LoginLockout PAS插件
非常重要,插件必须是激活插件列表中的第一个认证插件。这确保我们防止尝试登录到已锁定的账户并显示状态消息。这也记录了用户名和登录,如果被锁定将防止登录。
>>> plone_pas = portal.acl_users.plugins >>> IAuthenticationPlugin = plone_pas._getInterfaceFromName('IAuthenticationPlugin') >>> plone_pas.listPlugins(IAuthenticationPlugin) [('login_lockout_plugin', <LoginLockout at /plone/acl_users/login_lockout_plugin>)...]
和ICredentialsUpdatePlugin。这记录登录成功的时间以重置尝试数据。
>>> ICredentialsUpdatePlugin = plone_pas._getInterfaceFromName('ICredentialsUpdatePlugin') >>> 'login_lockout_plugin' in [p[0] for p in plone_pas.listPlugins(ICredentialsUpdatePlugin)] True
根Zope LoginLockout PAS插件
它还会在zope实例的根目录安装一个插件。
重要的是,这应该是第一个IAnonymousUserFactoryPlugin。在一个普通的Zope实例中,它将是唯一的。这确保它收集失败的登录尝试的数据。
>>> root_pas = portal.getPhysicalRoot().acl_users.plugins >>> IAnonymousUserFactoryPlugin = plone_pas._getInterfaceFromName('IAnonymousUserFactoryPlugin') >>> root_pas.listPlugins(IAnonymousUserFactoryPlugin) [('login_lockout_plugin', <LoginLockout at /acl_users/login_lockout_plugin>)]
锁定不正确的密码尝试
首次登录为管理员
现在我们将打开一个新的浏览器并尝试登录
>>> anon_browser = make_anon_browser('/login_form') >>> anon_browser.getControl('Login Name').value = user_id >>> anon_browser.getControl('Password').value = user_password >>> anon_browser.getControl('Log in').click() >>> 'Login failed' in anon_browser.contents False >>> print(anon_browser.contents) <BLANKLINE> ...You are now logged in... >>> anon_browser.open(portal.absolute_url()+'/logout')
让我们尝试使用另一个密码再次尝试
>>> anon_browser = make_anon_browser('/login_form') >>> anon_browser.getControl('Login Name').value = user_id >>> anon_browser.getControl('Password').value = 'notpassword' >>> anon_browser.getControl('Log in').click() >>> print(anon_browser.contents) <BLANKLINE> ...Login failed... >>> print(anon_browser.contents) <BLANKLINE> ...You have 2 attempts left before this account is locked...
这次不正确的尝试将在日志中显示
我们已安装了一个控制面板来监控登录尝试
>>> admin_browser = make_admin_browser('/loginlockout_settings') >>> print(admin_browser.contents) <BLANKLINE> ...<td>test-user</td>... ...<td>1</td>...
如果我们再尝试两次,我们将被锁定
>>> anon_browser = make_anon_browser('/login_form') >>> anon_browser.getControl('Login Name').value = user_id >>> anon_browser.getControl('Password').value = 'notpassword2' >>> anon_browser.getControl('Log in').click() >>> 'Login failed' in anon_browser.contents True >>> print(anon_browser.contents) <BLANKLINE> ...You have 1 attempts left before this account is locked... >>> anon_browser.getControl('Login Name').value = user_id >>> anon_browser.getControl('Password').value = 'notpassword3' >>> anon_browser.getControl('Log in').click() >>> 'Login failed' in anon_browser.contents True >>> 'attempts left' not in anon_browser.contents True >>> print(anon_browser.contents) <... ...This account has now been locked for security purposes...
现在即使正确的密码也不会起作用
>>> anon_browser = make_anon_browser('/login_form') >>> anon_browser.getControl('Login Name').value = user_id >>> anon_browser.getControl('Password').value = user_password >>> anon_browser.getControl('Log in').click() Not logged in >>> print(anon_browser.contents) <... ...This account has now been locked for security purposes... ... >>> "now logged in" not in anon_browser.contents True >>> anon_browser.getLink("Home").click() >>> anon_browser.getLink('Log in') <Link...>
管理员可以重置此人的账户
>>> admin_browser = make_admin_browser('/loginlockout_settings') >>> print(admin_browser.contents) <BLANKLINE> ...<td>test-user</td>... ...<td>3</td>... >>> admin_browser.getControl(name='reset_nonploneusers:list').value = ['test-user'] >>> admin_browser.getControl('Reset selected accounts').click() >>> print(admin_browser.contents) <BLANKLINE> ...Accounts were reset for these login names: test-user...
现在他们可以再次登录
>>> anon_browser = make_anon_browser('/login_form') >>> anon_browser.getControl('Login Name').value = user_id >>> anon_browser.getControl('Password').value = user_password >>> anon_browser.getControl('Log in').click() >>> print(anon_browser.contents) <BLANKLINE> ...You are now logged in...
IP锁定
您可以可选地确保只有特定的IP地址范围可以进行登录。
默认情况下,IP锁定是禁用的。
注意:如果您在代理后面使用Zope,则必须在每个代理上启用X-Forward-For标题,否则此插件将错误地使用REMOTE_ADDR,这将是一个本地IP。
要启用此功能,请进入ZMI并在whitelist_ips属性中输入范围
>>> config_property( whitelist_ips = u'10.1.1.1' )
如果zope前面有代理,您必须确保它们设置了`X-Forwarded-For`标题。注意只有第一个转发的IP将被使用。
>>> anon_browser = make_anon_browser('/login_form') >>> anon_browser.addHeader('X-Forwarded-For', '10.1.1.1, 192.168.1.1') >>> anon_browser.getControl('Login Name').value = user_id >>> anon_browser.getControl('Password').value = user_password >>> anon_browser.getControl('Log in').click() >>> print(anon_browser.contents) <BLANKLINE> ...You are now logged in... >>> anon_browser.open(portal.absolute_url()+'/logout')
如果不是有效的IP,则登录将失败
>>> anon_browser = make_anon_browser('/login_form') >>> anon_browser.addHeader('X-Forwarded-For', '2.2.2.2') >>> anon_browser.open(portal.absolute_url()+'/login_form') >>> anon_browser.getControl('Login Name').value = user_id >>> anon_browser.getControl('Password').value = user_password >>> anon_browser.getControl('Log in').click() >>> print(anon_browser.contents) <BLANKLINE> ...Login currently unavailable... >>> anon_browser.getLink('Log in') <Link text='Log in'...>
基本认证与正确的IP一起工作
>>> anon_browser = make_anon_browser() >>> anon_browser.addHeader('Authorization', 'Basic %s:%s' % (user_id,user_password)) >>> anon_browser.addHeader('X-Forwarded-For', '10.1.1.1') >>> anon_browser.open(portal.absolute_url()) >>> anon_browser.getLink('Log out') <Link text='Log out'...>
基本认证与错误的IP一起失败
>>> anon_browser = make_anon_browser() >>> anon_browser.addHeader('Authorization', 'Basic %s:%s' % (user_id,user_password)) >>> anon_browser.addHeader('X-Forwarded-For', '2.2.2.2') >>> anon_browser.open(portal.absolute_url()) >>> print(anon_browser.contents) <BLANKLINE> ...Login currently unavailable... >>> anon_browser.getLink('Log in') <Link text='Log in'...>
我们仍然可以在根目录使用root登录
>>> anon_browser = make_anon_browser() >>> anon_browser.addHeader('Authorization', 'Basic %s:%s' % (base_id, base_password)) >>> anon_browser.addHeader('X-Forwarded-For', '2.2.2.2') >>> anon_browser.open(portal.absolute_url()+'/../manage_main') >>> print(anon_browser.contents) <BLANKLINE> ...manage_workspace...
但我们不能再使用root ID进入plone站点了
>>> anon_browser.open(portal.absolute_url()+'/manage_main') Traceback (most recent call last): ... Unauthorized: You are not authorized to access this resource.
您还可以设置IP范围,例如。
>>> config_property( whitelist_ips = u"""10.1.1.1 ... 10.1.0.0/16 # range 1 ... 2.2.0.0/16 # range 2 ... """) >>> anon_browser = make_anon_browser('/login_form') >>> anon_browser.addHeader('X-Forwarded-For', '2.2.2.2') >>> anon_browser.getControl('Login Name').value = user_id >>> anon_browser.getControl('Password').value = user_password >>> anon_browser.getControl('Log in').click() >>> print(anon_browser.contents) <BLANKLINE> ...You are now logged in... >>> anon_browser.open(portal.absolute_url()+'/logout')
您还可以设置一个环境变量LOGINLOCKOUT_IP_WHITELIST,它与配置合并。这允许那些具有文件系统访问权限的人如果他们的配置设置错误,则有一种方法进入。它还允许为任何已安装loginlockout的Plone多站点设置中的任何站点设置IP范围。只要站点已安装loginlockout。
>>> anon_browser = make_anon_browser('/login_form') >>> anon_browser.getLink('Log in') <Link text='Log in'... >>> import os; os.environ["LOGINLOCKOUT_IP_WHITELIST"] = "3.3.3.3" >>> anon_browser.addHeader('Authorization', 'Basic %s:%s' % (user_id,user_password)) >>> anon_browser.addHeader('X-Forwarded-For', '3.3.3.3') >>> anon_browser.open(portal.absolute_url()) >>> anon_browser.getLink('Log out') <Link text='Log out'...>
注意,您仍然必须设置IP锁定配置,否则即使设置了环境变量,登录也允许从任何地方进行
>>> config_property( whitelist_ips = u""" ... """) >>> anon_browser = make_anon_browser() >>> anon_browser.addHeader('Authorization', 'Basic %s:%s' % (user_id,user_password)) >>> anon_browser.addHeader('X-Forwarded-For', '4.4.4.4') >>> anon_browser.open(portal.absolute_url()) >>> anon_browser.getLink('Log out') <Link text='Log out'...> >>> del os.environ["LOGINLOCKOUT_IP_WHITELIST"]
如果您不确定当前客户端IP被检测为什么,您可以在控制面板中查看
>>> admin_browser = make_admin_browser('/') >>> admin_browser.addHeader('X-Forwarded-For', '10.1.1.1, 192.168.1.1') >>> admin_browser.getLink('Site Setup').click() >>> admin_browser.getLink('LoginLockout').click() >>> print(admin_browser.contents) <BLANKLINE> ...Current detected Client IP: <span>10.1.1.1</span>...
登录历史
您还可以查看特定用户的成功登录历史记录。注意这是用户ID而不是用户登录,它们可以不同。用户test_user_1_有4次成功的登录。
>>> admin_browser = make_admin_browser('/loginlockout_settings') >>> admin_browser.getLink('Login history').click() >>> admin_browser.getControl('Username pattern').value = 'test_user_1_' >>> admin_browser.getControl('Search records').click() >>> print(admin_browser.contents) <BLANKLINE> ... <td valign="top">test_user_1_</td> <td valign="top"> <ul> <li> ... () </li> <li> ... () </li> <li> ... (10.1.1.1) </li> <li> ... (2.2.2.2) </li> </ul> ...
密码重置历史
当用户更改他们的密码时
>>> anon_browser = make_anon_browser('/login_form') >>> anon_browser.getControl('Login Name').value = user_id >>> anon_browser.getControl('Password').value = user_password >>> anon_browser.getControl('Log in').click() >>> anon_browser.getLink("Preferences").click() >>> anon_browser.getLink("Password").click() >>> anon_browser.getControl('Current password').value = user_password >>> anon_browser.getControl('New password').value = '12345678' >>> anon_browser.getControl('Confirm password').value = '12345678' >>> anon_browser.getControl('Change Password').click() >>> print(anon_browser.contents) <... ...Password changed... ...
这更改了密码
>>> anon_browser = make_anon_browser('/login_form') >>> anon_browser.getControl('Login Name').value = user_id >>> anon_browser.getControl('Password').value = '12345678' >>> anon_browser.getControl('Log in').click() >>> anon_browser.getLink("Preferences").click()
然后管理员可以看到密码已被更改
>>> admin_browser = make_admin_browser('/loginlockout_settings') >>> admin_browser.getLink('History password changes').click() >>> print(admin_browser.contents) <... ... <tr class="even"> <td>test_user_1_</td> <td>...</td> </tr> ...
其他支持
root用户也可以被锁定,并且还有基本认证
>>> def try_base_login(pw): ... anon_browser = make_anon_browser() # Can't redefine header in older testbrowser ... anon_browser.addHeader('Authorization', 'Basic %s:%s' % (base_id, pw)) ... anon_browser.open(portal.absolute_url()) ... print(anon_browser.contents) >>> try_base_login("attempt1") <... ...You have 2 attempts left before this account is locked... >>> try_base_login("attempt2") <... ...You have 1 attempts left before this account is locked... >>> try_base_login("attempt3") <... ...This account has now been locked for security purposes... ... >>> try_base_login(base_password) <... ...This account has now been locked for security purposes... ...
实现
如果在认证插件激活后激活了root匿名用户工厂插件,则这是不成功的登录尝试。如果密码与上一次不成功的尝试不同,则我们在root插件中持久存储的数据中增加计数器。
如果实例插件尝试验证已被标记为有太多尝试的用户,则会引发未授权错误。这将激活挑战插件,显示锁定信息而不是另一个登录表单。
当登录成功时,会调用updateCredentials,在这种情况下,我们将重置不成功的登录计数。
故障排除
AttributeError: manage_addLoginLockout
如果在运行测试时出现AttributeError: manage_addLoginLockout,这可能是由于在测试设置期间没有运行__init__.py中的initialize()方法。
要解决这个问题,请显式调用
z2.installProduct(portal, 'Products.LoginLockout')
开发
您想要帮助推进这个插件真是太好了!
要开始开发
git clone git@github.com:collective/Products.LoginLockout.git cd Products.LoginLockout virtualenv . ./bin/python bootstrap.py ./bin/buildout ./bin/test
请遵守以下要求
只有在测试当前通过时才开始工作。如果不通过,请修复它们,或向某人(*)寻求帮助。
在分支中完成工作,并在GitHub上创建一个拉取请求。请求某人(*)合并它。
请遵守指南:PEP8。我们使用plone.recipe.codeanalysis强制执行其中一些。
- (*) 可能会帮助您的人
khink, djay, ajung, macagua
TODO
在LoginLockout产品上可以做的事情
将皮肤移动到浏览器视图
移除重置密码的覆盖。应该在PAS或使用事件中完成
可选的路径以存储尝试数据库,以便可以将其存储在无历史数据库中。
可能有一个短暂的锁定或验证码来防止快速尝试,而不是完全锁定
仅限制某些组到某些IP网络,例如管理员。也许还有角色?
鸣谢
Dylan Jay,原始代码。
贡献者
Kees Hink
Andreas Jung
Leonardo J. Caballero G.
Wolfgang Thomas
Peter Uittenbroek
Ovidiu Miron
Ludolf Takens
Maarten Kling
感谢Daniel Nouri和BlueDynamics,他们的NoDuplicateLogin是该项目的基石。
变更
0.5.0 (2024-03-08)
使Python 3和Plone 5.2兼容[HybridAU]
进行了更改,以便在5.2和6.0(经典)中正常工作[djay]
将插件操作更改为重置凭据而不是引发UnAuthorised,以便状态消息正常工作,并切换到使用状态消息。这需要PAS插件的不同顺序[djay]
添加了剩余尝试次数的警告[djay]
包括限制来自某些IP网络请求的能力。配置页面显示当前客户端IP[djay]
将尝试存储移动到Plone站点,以防止站点之间的数据泄露[djay]
Plone 5+现在使用注册表进行配置[djay]
删除“选择全部”按钮。[ivanteoh]
纠正了私有方法错误。添加了法语翻译。纠正了翻译域。添加了一些翻译。纠正了控制面板图标。[sgeulette]
纠正了卸载配置文件[sgeulette]
修复了4.1-6.0版本中的更改密码历史记录[djay]
删除了4.1的测试[djay]
0.4.0 (2015-11-25)
修复了皮肤模板Python脚本中的flake8错误[khink]
0.3.9 (2015-11-18)
重置密码时,在门户消息中不出现Unicode错误[maartenkling]
0.3.8 (2015-10-17)
包括Travis构建徽章。修复了测试设置,使代码分析工作,更新README以包含开发信息。(khink)
0.3.7 (2015-06-08)
在重置期间后重置计数器。(ltakens)
0.3.6 (2015-04-08)
在站点布局中渲染锁定消息。在锁定消息中显示重置期间,以便人们不必再次联系站点管理员。[khink]
0.3.5 (2015-04-02)
通过ZMI配置可配置允许尝试的数量[khink]
0.3.4 (2015-04-01)
通过ZMI配置可配置重置期间[khink]
为此包添加了更多字符串分类器项[macagua]
为gif图标添加了plone_deprecated皮肤[macagua]
添加了对Configlet和GenericSetup配置文件的支持[macagua]
添加了西班牙语翻译[macagua]
添加了i18n支持[macagua]
- LoginLockout接口更新如下(omiron)
将用户锁定组与虚假信息分开
链接到用户个人资料页面
提供完整的用户名和电子邮件地址,以便简化“页面查找”
在配置中引入“选择全部”选项(thepjot)
在重置期间过期后重新启用“reset_period”,用户再次获得机会(thepjot)
0.3.3 (2013-11-20)
以更防御性的方式检查fake_client_ip(pysailor)
0.3.2 (2012-03-12)
修复了弃用警告(Andreas Jung)
0.3.1 (2012-02-13)
修复了文档中的某些reStructuredText错误(Andreas Jung)
0.3 (2011-03-04)
内部清理
尽可能使用GenericSetup
添加了对成功登录尝试记录的支持
添加了对密码更改记录的支持
(Andreas Jung)
0.2 (2009-04-20)
合并了configlet版本Eggified
开始doctest
(Dylan Jay)
(2009-03-10)
新增配置组件,用于在Plone控制面板中查看失败的尝试和重置账户。
很可能不再支持纯Zope使用。
(Kees Hink)
(2008-12-18)
新增安装程序,使用Extensions/Install.py。 (不幸的是,Generic Setup似乎还不支持卸载,但setuphandlers.py和导入配置文件(profiles/default)中的方法可供使用。只需取消注释相关的zcml指令即可。)
(Kees Hink)
0.1 (未知)
初始版本 (Dylan Jay)
项目详情
下载文件
下载适合您平台的文件。如果您不确定选择哪个,请了解更多关于安装包的信息。
源代码分发
构建分发
哈希值 for Products.LoginLockout-0.5.0-py2-none-any.whl
算法 | 哈希摘要 | |
---|---|---|
SHA256 | 9f930ca9dc803764a07f6f322a7982532ed07e5c08413d3e29364b531772ee56 |
|
MD5 | 7201e1b4f34622186c516ff09550ffbc |
|
BLAKE2b-256 | 9146751f0702358bb02521a93707beffe2fe37c853bb872a84470783290ae3ad |