跳转到主要内容

此可插拔身份验证服务(PAS)插件将在预定次数的错误尝试后锁定登录。锁定后,用户将看到一个页面,告知他们联系管理员解锁。

项目描述

LoginLockout

https://github.com/collective/Products.LoginLockout/workflows/CI/badge.svg https://coveralls.io/repos/collective/Products.LoginLockout/badge.svg?branch=master&service=github

此可插拔身份验证服务(PAS)插件将在预定次数的错误尝试后锁定登录。锁定后,用户将看到一个页面,告知他们联系管理员解锁。

需要

  • PluggableAuthService 及其依赖项

  • (可选) PlonePAS 及其依赖项

  • (可选) Plone 4.1.x-6.0.x

http://github-actions.40ants.com/collective/Products.LoginLockout/matrix.svg

功能

  • 在锁定之前可配置的允许的错误尝试次数

  • 在可配置的时间(“重置周期”)后,账户将再次可用。如果重置周期后的第一次登录尝试无效,则无效登录计数器设置为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”插件接口。

  1. 对您的根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)

项目详情


下载文件

下载适合您平台的文件。如果您不确定选择哪个,请了解更多关于安装包的信息。

源代码分发

Products.LoginLockout-0.5.0.tar.gz (57.5 kB 查看哈希值)

上传时间 源代码

构建分发

Products.LoginLockout-0.5.0-py2-none-any.whl (52.5 kB 查看哈希值)

上传时间 Python 2

由以下赞助

AWS AWS 云计算和安全赞助商 Datadog Datadog 监控 Fastly Fastly CDN Google Google 下载分析 Microsoft Microsoft PSF赞助商 Pingdom Pingdom 监控 Sentry Sentry 错误记录 StatusPage StatusPage 状态页面