有興趣並做了一個令人費解的例子:定義了一個LoginManager
類來處理使用外部文檔上的列表創建和記錄用戶。這個類應該保存在它自己的文件中,然後將其導入創建新用戶的代碼文件和驗證它們的代碼文件中(請參閱代碼示例)。
類啓動程序獲取文件路徑作爲參數,只要它指向兩個代碼文件中的同一個文件,它應該可以。
我用argon2
作爲散列算法,雖然我必須承認我是密碼學的新手,所以你應該用一粒鹽來看待我的選擇。
import argon2
from pathlib import Path
import re
class LoginManager(object):
"""
Parameters
----------
database : str
File (and path to it) that holds the users and corresponding hashed
passwords.
Examples
--------
Simple `LoginManager`:
>>> temp_database = 'temp_database.txt'
>>> Login1 = LoginManager(temp_database)
>>> Login1.add_user('super_user', 's3kr3tp4ssw0rd')
>>> Login1.login('super_user', 's3kr3tp4ssw0rd')
True
>>> Login1.login('super_user', 't0t411ywr0ng') # wrong password
False
>>> Login1.login('LameUser', 's3kr3tp4ssw0rd') # not existing user
False
New `LoginManager` with the same database:
>>> Login2 = LoginManager(temp_database)
>>> Login2.login('super_user', 's3kr3tp4ssw0rd') # was on Login1
True
>>> Login2.login('super_user', 't0t411ywr0ng') # wrong password
False
>>> Login2.login('LameUser', 't0t411ywr0ng') # not existing user
False
Clean `temp_database` if testing with `doctest`:
>>> import os
>>> os.remove(temp_database)
Notes
-----
* Notice that adding new users to either LoginManager in the example will
not share the new ones between them! Only the users existing at the time
of the creation will be in the LoginManager, so colisions must be must be
avoided by not having two pointing to the same file at the same.
* See more details on `argon2` here:
https://argon2-cffi.readthedocs.io/en/stable/api.html#argon2.PasswordHasher
(also where I saw 's3kr3tp4ssw0rd' and 't0t411ywr0ng')
"""
def __init__(self, database=None):
"""
Create new `LoginManager`.
Notes
-----
Should have better verification whether given path points to a file or
a folder!
"""
if database is None:
database = 'database.txt'
self.database = Path(database)
# create file if it is not there:
if not self.database.is_file():
self.database.touch()
self._reload_database()
self.hasher = argon2.PasswordHasher(time_cost=2,
memory_cost=512,
parallelism=2,
hash_len=16,
salt_len=16,
encoding='utf-8')
self._hash_example = self.hasher.hash('default')
self._max_name_size = 10
def add_user(self, user, password):
"""
Add new user to the `LoginManager` and update `database` file.
Raises `ValueError` if the user already exists or is invalid.
Examples
--------
>>> temp_database = 'temp_database_user.txt'
>>> Login1 = LoginManager(temp_database)
>>> Login1.add_user('super*user', 's3kr3tp4ssw0rd')
Traceback (most recent call last):
ValueError: Invalid characters in user name!
Use only A-Z, a-z, 0-9 and `.`, `-` or `_`.
>>> Login1.add_user('UltraLongUserName', 's3kr3tp4ssw0rd')
Traceback (most recent call last):
ValueError: User name "UltraLongUs..." too long!
Clean `temp_database` if testing with `doctest`:
>>> import os
>>> os.remove(temp_database)
Notes
-----
Does not check users added to the databse file after the `LoginManager`
was created!
"""
self._is_username_valid(user) # raises ValueError if it's not
hashed = self.hasher.hash(password)
self.users[user] = hashed
with self.database.open('ba') as database:
database.write('\t'.join([user.ljust(self._max_name_size),
hashed,
'\r\n']))
# The '\r\n' at the end ensures there's always a new empty line
# after the lattest password, and eases the split of user name and
# password afterwards using '\t' when importing the list.
# The .ljust(self._max_name_size) pads the username with spaces.
# The binary mode is used to ensure future changes are valid (namely
# to allow the use of seek with negative values whithin the file.
def login(self, user, password):
"""
Return `True` if the user/password pair is valid, `False` otherwise.
"""
try:
return self.hasher.verify(self.users.get(user, self._hash_example),
password)
except argon2.exceptions.VerifyMismatchError:
return False
def change_password(self, user, old_password, new_password):
"""
Change password of existing user.
Examples
--------
>>> temp_database = 'temp_database_change_pass.txt'
>>> Login1 = LoginManager(temp_database)
>>> Login1.add_user('super_user', 's3kr3tp4ssw0rd')
>>> Login1.add_user('LameUser', 't0t411ywr0ng')
>>> Login1.add_user('banana', '1234567890')
Test changes to first user:
>>> Login1.login('super_user', 's3kr3tp4ssw0rd')
True
>>> Login1.change_password('super_user',
... 's3kr3tp4ssw0rd',
... 'n3ws3kr3tp4ssw0rd')
>>> Login1.login('super_user', 's3kr3tp4ssw0rd')
False
>>> Login1.login('super_user', 'n3ws3kr3tp4ssw0rd')
True
Test changes to last user:
>>> Login1.change_password('banana',
... '1234567890',
... 'n3ws3kr3tp4ssw0rd')
>>> Login1.login('banana', '1234567890')
False
>>> Login1.login('banana', 'n3ws3kr3tp4ssw0rd')
True
Test changes to a middle user:
>>> Login1.change_password('LameUser',
... 't0t411ywr0ng',
... 'n3ws3kr3tp4ssw0rd')
>>> Login1.login('banana', 't0t411ywr0ng')
False
>>> Login1.login('banana', 'n3ws3kr3tp4ssw0rd')
True
Make sure changes to file are valid:
>>> Login2 = LoginManager(temp_database)
>>> Login2.login('super_user', 'n3ws3kr3tp4ssw0rd')
True
Clean `temp_database` if testing with `doctest`:
>>> import os
>>> os.remove(temp_database)
Notes
-----
If a contact is available to reach the user, it should be used to warn
him of an attempt to change the password.
"""
if self.login(user, old_password):
with self.database.open('br+') as database:
# find user name
line = ' '
while line != '':
line = database.readline()
if line[:self._max_name_size].rstrip() == user:
new_hash = self.hasher.hash(new_password)
database.seek(-len(self._hash_example)-3, 1)
database.write('\t'.join([new_hash, '\r\n']))
self.users[user] = new_hash
break
def _reload_database(self):
self.users = {} # could use a more intuitive name
with self.database.open('r') as database:
lines = database.readlines()
for user_pass in lines:
user, pass_hash, new_line = user_pass.split('\t')
self.users[user.rstrip()] = pass_hash
def _is_username_valid(self, user_name):
"""
Check if user name is valid, raise `ValueError` if it's not.
"""
if user_name in self.users.keys():
raise ValueError('User "%s" already exists!' % (user_name,))
if len(user_name) > self._max_name_size:
raise ValueError('User name "%s..." too long!' %
(user_name[:self._max_name_size+1],))
# From https://stackoverflow.com/questions/1323364/
search = re.compile(r'[^A-Za-z0-9._-]').search
if bool(search(user_name)):
raise ValueError('Invalid characters in user name!\n'
'Use only A-Z, a-z, 0-9 and `.`, `-` or `_`.')
def _test():
"""
Test functions using the doctext examples.
"""
import doctest
doctest.testmod(verbose=False)
if __name__ == "__main__":
# use the examples in documentation to test the code.
_test()
你(和任何人)是自由活動,使用和改變,但不要把這個作爲一個很好的做法的例子 - 只是一個業餘愛好者對這個問題的看法。
您可以將用戶/密碼列表寫入文件並從該文件中讀取該列表,但這[不是應該如何完成的](https://www.youtube.com/watch?v=8ZtInClXe1Q) 。應該保留用戶列表,以及指向巫婆文件/文件夾屬於該用戶的指針。該文件/文件夾的內容應使用第一種情況下給出的密碼進行加密,如果提供了正確的密碼,解密應該是成功的。 – berna1111
順便說一下,我相信加密模塊是[hashlib](https://docs.python.org/2/library/hashlib.html#module-hashlib) - 我從來沒有使用它,但就我而言從第一眼就明白它是用來散列給定的密鑰(不像我在之前的評論中所說的那樣是文件或文件夾)。您保留密碼的結果散列,並且您在嘗試用於登錄的任何密碼上使用相同的散列函數。 – berna1111
好的感謝您的幫助,可能聽起來像一個愚蠢的問題,但我只是進入編程。但再次感謝回覆 – Jake