用戶管理是絕大部分Web網站都需要解決的問題。用戶管理涉及到用戶注冊和登錄。
用戶注冊相對簡單,我們可以先通過API把用戶注冊這個功能實現了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
_RE_MD5 = re. compile (r '^[0-9a-f]{32}$' ) @api @post ( '/api/users' ) def register_user(): i = ctx.request. input (name = ' ', email=' ', password=' ') name = i.name.strip() email = i.email.strip().lower() password = i.password if not name: raise APIValueError( 'name' ) if not email or not _RE_EMAIL.match(email): raise APIValueError( 'email' ) if not password or not _RE_MD5.match(password): raise APIValueError( 'password' ) user = User.find_first( 'where email=?' , email) if user: raise APIError( 'register:failed' , 'email' , 'Email is already in use.' ) user = User(name = name, email = email, password = password, image = 'http://www.gravatar.com/avatar/%s?d=mm&s=120' % hashlib.md5(email).hexdigest()) user.insert() return user |
注意用戶口令是客戶端傳遞的經過MD5計算后的32位Hash字符串,所以服務器端并不知道用戶的原始口令。
接下來可以創建一個注冊頁面,讓用戶填寫注冊表單,然后,提交數據到注冊用戶的API:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
{ % extends '__base__.html' % } { % block title % }注冊{ % endblock % } { % block beforehead % } <script> function check_form() { $( '#password' ).val(CryptoJS.MD5($( '#password1' ).val()).toString()); return true; } < / script> { % endblock % } { % block content % } <div class = "uk-width-2-3" > <h1>歡迎注冊!< / h1> <form id = "form-register" class = "uk-form uk-form-stacked" onsubmit = "return check_form()" > <div class = "uk-alert uk-alert-danger uk-hidden" >< / div> <div class = "uk-form-row" > <label class = "uk-form-label" >名字:< / label> <div class = "uk-form-controls" > < input name = "name" type = "text" class = "uk-width-1-1" > < / div> < / div> <div class = "uk-form-row" > <label class = "uk-form-label" >電子郵件:< / label> <div class = "uk-form-controls" > < input name = "email" type = "text" class = "uk-width-1-1" > < / div> < / div> <div class = "uk-form-row" > <label class = "uk-form-label" >輸入口令:< / label> <div class = "uk-form-controls" > < input id = "password1" type = "password" class = "uk-width-1-1" > < input id = "password" name = "password" type = "hidden" > < / div> < / div> <div class = "uk-form-row" > <label class = "uk-form-label" >重復口令:< / label> <div class = "uk-form-controls" > < input name = "password2" type = "password" maxlength = "50" placeholder = "重復口令" class = "uk-width-1-1" > < / div> < / div> <div class = "uk-form-row" > <button type = "submit" class = "uk-button uk-button-primary" ><i class = "uk-icon-user" >< / i> 注冊< / button> < / div> < / form> < / div> { % endblock % } Try |
這樣我們就把用戶注冊的功能完成了:
用戶登錄比用戶注冊復雜。由于HTTP協議是一種無狀態協議,而服務器要跟蹤用戶狀態,就只能通過cookie實現。大多數Web框架提供了Session功能來封裝保存用戶狀態的cookie。
Session的優點是簡單易用,可以直接從Session中取出用戶登錄信息。
Session的缺點是服務器需要在內存中維護一個映射表來存儲用戶登錄信息,如果有兩臺以上服務器,就需要對Session做集群,因此,使用Session的Web App很難擴展。
我們采用直接讀取cookie的方式來驗證用戶登錄,每次用戶訪問任意URL,都會對cookie進行驗證,這種方式的好處是保證服務器處理任意的URL都是無狀態的,可以擴展到多臺服務器。
由于登錄成功后是由服務器生成一個cookie發送給瀏覽器,所以,要保證這個cookie不會被客戶端偽造出來。
實現防偽造cookie的關鍵是通過一個單向算法(例如MD5),舉例如下:
當用戶輸入了正確的口令登錄成功后,服務器可以從數據庫取到用戶的id,并按照如下方式計算出一個字符串:
"用戶id" + "過期時間" + MD5("用戶id" + "用戶口令" + "過期時間" + "SecretKey")
當瀏覽器發送cookie到服務器端后,服務器可以拿到的信息包括:
- 用戶id
- 過期時間
- MD5值
如果未到過期時間,服務器就根據用戶id查找用戶口令,并計算:
MD5("用戶id" + "用戶口令" + "過期時間" + "SecretKey")
并與瀏覽器cookie中的MD5進行比較,如果相等,則說明用戶已登錄,否則,cookie就是偽造的。
這個算法的關鍵在于MD5是一種單向算法,即可以通過原始字符串計算出MD5,但無法通過MD5反推出原始字符串。
所以登錄API可以實現如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
@api @post ( '/api/authenticate' ) def authenticate(): i = ctx.request. input () email = i.email.strip().lower() password = i.password user = User.find_first( 'where email=?' , email) if user is None : raise APIError( 'auth:failed' , 'email' , 'Invalid email.' ) elif user.password ! = password: raise APIError( 'auth:failed' , 'password' , 'Invalid password.' ) max_age = 604800 cookie = make_signed_cookie(user. id , user.password, max_age) ctx.response.set_cookie(_COOKIE_NAME, cookie, max_age = max_age) user.password = '******' return user # 計算加密cookie: def make_signed_cookie( id , password, max_age): expires = str ( int (time.time() + max_age)) L = [ id , expires, hashlib.md5( '%s-%s-%s-%s' % ( id , password, expires, _COOKIE_KEY)).hexdigest()] return '-' .join(L) 對于每個URL處理函數,如果我們都去寫解析cookie的代碼,那會導致代碼重復很多次。 利用攔截器在處理URL之前,把cookie解析出來,并將登錄用戶綁定到ctx.request對象上,這樣,后續的URL處理函數就可以直接拿到登錄用戶: @interceptor ( '/' ) def user_interceptor( next ): user = None cookie = ctx.request.cookies.get(_COOKIE_NAME) if cookie: user = parse_signed_cookie(cookie) ctx.request.user = user return next () # 解密cookie: def parse_signed_cookie(cookie_str): try : L = cookie_str.split( '-' ) if len (L) ! = 3 : return None id , expires, md5 = L if int (expires) < time.time(): return None user = User.get( id ) if user is None : return None if md5 ! = hashlib.md5( '%s-%s-%s-%s' % ( id , user.password, expires, _COOKIE_KEY)).hexdigest(): return None return user except : return None Try |
這樣,我們就完成了用戶注冊和登錄的功能。