【发布时间】:2015-03-29 11:34:34
【问题描述】:
我正在编写一个相当简单的内部应用程序(目前原型在 Bottle),它将事件和变更管理事件的通知发送到内部邮件列表,同时强制这些通知符合几个标准模板(并确保,通过Python Jira API 表明所需的问题引用存在并且处于适当的状态)。
当然,我们要求用户在我发送消息之前对我的应用程序进行身份验证,这些消息将归于他们。我们使用 LDAP,所有密码哈希都以 {SSHA} 格式存储。
我发现了至少三种不同的身份验证方式:
- 使用具有足够 LDAP ACI 的服务帐户绑定到 LDAP 以获取密码哈希(与我们的 /etc/sssd/sssd.conf 系统级身份验证相同),使用它来查找 dn 并提取“userPassword”属性;然后使用 hashlib 验证建议的密码
- 使用具有有限搜索权限的服务帐户绑定到 LDAP,使用该帐户查找用户的 dn,然后尝试使用该 dn 绑定到 LDAP,用户建议密码
- 使用 simplepam(纯 Python/ctypes 包装器)或 python-pam 模块中的 authenticate() 函数
这里的代码似乎正确地实现了其中的第一个:
#!python
import hashlib
import ConfigParser, os
from base64 import encodestring as encode
from base64 import decodestring as decode
import ldap
config = ConfigParser.ConfigParser()
config.read(os.path.expanduser('~/.creds.ini'))
uid = config.get('LDAP', 'uid')
pwd = config.get('LDAP', 'pwd')
svr = config.get('LDAP', 'svr')
bdn = config.get('LDAP', 'bdn')
ld = ldap.initialize(svr)
ld.protocol_version = ldap.VERSION3
ld.simple_bind_s(uid, pwd)
def chk(prop, pw):
pw=decode(pw[6:]) # Base64 decode after stripping off {SSHA}
digest = pw[:20] # Split digest/hash of PW from salt
salt = pw[20:] # Extract salt
chk = hashlib.sha1(prop) # Hash the string presented
chk.update(salt) # Salt to taste:
return chk.digest() == digest
if __name__ == '__main__':
import sys
from getpass import getpass
max_attempts = 3
if len(sys.argv) < 2:
print 'Must supply username against which to authenticate'
sys.exit(127)
name = sys.argv[1]
user_dn = ld.search_s(bdn, ldap.SCOPE_SUBTREE, '(uid=%s)' % name)
if len(user_dn) < 1:
print 'No DN found for %s' % name
sys.exit(126)
pw = user_dn[0][1].get('userPassword', [''])[0]
exit_value = 1
attempts = 0
while attempts < max_attempts:
prop = getpass('Password: ')
if chk(prop, pw):
print 'Authentication successful'
exit_value = 0
break
else:
print 'Authentication failed'
attempts += 1
else:
print 'Maximum retries exceeded'
sys.exit(exit_value)
这似乎可行(假设我们在 .creds.ini 中有适当的值)。
下面是实现第二个选项的一些代码:
#!python
# ...
### Same ConfigParser and LDAP initialization as before
# ...
def chk(prop, dn):
chk = ldap.initialize(svr)
chk.protocol_version = ldap.VERSION3
try:
chk.simple_bind_s(dn, prop)
except ldap.INVALID_CREDENTIALS:
return False
chk.unbind()
return True
if __name__ == '__main__':
import sys
from getpass import getpass
max_attempts = 3
if len(sys.argv) < 2:
print 'Must supply username against which to authenticate'
sys.exit(127)
name = sys.argv[1]
user_dn = ld.search_s(bdn, ldap.SCOPE_SUBTREE, '(uid=%s)' % name)
if len(user_dn) < 1:
print 'No distinguished name (DN) found for %s' % name
sys.exit(126)
dn = user_dn[0][0]
exit_value = 1
attempts = 0
while attempts < max_attempts:
prop = getpass('Password: ')
if chk(prop, dn):
print 'Authentication successful'
exit_value = 0
break
else:
print 'Authentication failed'
attempts += 1
else:
print 'Maximum retries exceeded'
sys.exit(exit_value)
此处未显示,但我还测试了我可以继续使用 ld LDAP 连接,而独立于瞬态 chk LDAP 对象。所以我长期运行的网络服务可以继续重复使用一个连接。
无论我使用两个 PAM 模块中的哪一个,最后一个选项几乎相同。这是一个使用 python-pam 的示例:
#!/usr/bin/env python
import pam
pam_conn = pam.pam()
def chk(prop, name):
return pam_conn.authenticate(name, prop)
if __name__ == '__main__':
import sys
from getpass import getpass
max_attempts = 3
if len(sys.argv) < 2:
print 'Must supply username against which to authenticate'
sys.exit(127)
name = sys.argv[1]
exit_value = 1
attempts = 0
while attempts < max_attempts:
prop = getpass('Password: ')
if chk(prop, name):
print 'Authentication successful'
exit_value = 0
break
else:
print 'Authentication failed'
attempts += 1
else:
print 'Maximum retries exceeded'
sys.exit(exit_value)
我的问题是:我应该使用哪一个。他们中的任何一个是否比其他人特别不安全?对此“最佳实践”是否有任何共识?
【问题讨论】:
-
最后:你是用哪种方式实现的?
-
第二种方法(使用 userDN 绑定到 LDAP),如下@jwilleke 推荐。