<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ??一站式輕松地調用各大LLM模型接口,支持GPT4、智譜、豆包、星火、月之暗面及文生圖、文生視頻 廣告
                ![用戶管理的規范](https://box.kancloud.cn/2016-04-15_57105526b9611.png) # 用戶管理的規范 用戶管理是現代Web應用都需要做的事情之一。一個僅有基本的賬戶功能的應用也需要處理一大堆諸如注冊,郵件確認,安全地存儲密碼,重置密碼,用戶驗證以及更多。考慮到許多安全問題都出現在管理用戶時,在這個領域最好遵循普遍的規范。 > **注意** > 在本章中我會假定你已經在用SQLAlchemy模型和WTForms來處理你的表單輸入。如果你不使用它們,你需要修改這些規范來適應你喜歡的方法。 ## 郵件確認 當一個新用戶給你他們的郵件地址,你通常需要確認該地址是否是正確的。一旦你完成了驗證,你就可以安心地發送密碼重置鏈接和其他敏感信息給該郵箱,不用擔心位于接收端的會是誰。 郵件確認的一個通常的規范是發送一個當前獨一無二的URL密碼重置鏈接,來確認用戶的電子郵件地址。舉個例子,john@gmail.com注冊了你的應用。你的應用把他登記在數據庫中,設置`email_confirmed`列為`False`并發送一封帶特定URL的郵件給john@gmail.com。這個URL通常包括一個獨一無二的token,比如<http://myapp.com/accounts/confirm/kj3kjhj3hj3>。當John收到那封郵件時,他點擊鏈接。你的應用看到了token,知道是哪封郵件并設置John的`email_confirmed`列為`True`。 那我們怎么知道給定的token對應的是哪封郵件?一個方法是在創建token時把它存儲到數據庫中,在我們收到一個確認請求時檢索數據庫來找到那個token。這需要做很多事情,而幸運的是,我們不必這么做。 我們將郵件地址編碼進token。它還包括一個時間戳,表示這個token的有效期。為了做到這一點,我們要使用`itsdangerous`包。這個包提供了在無法信賴的環境中發送敏感信息的工具。(比如發送郵件確認token給未驗證的郵件地址)。在這個例子里,我們將使用`URLSafeTimedSerializer`。 _myapp/util/security.py_ ```python from itsdangerous import URLSafeTimedSerializer from .. import app ts = URLSafeTimedSerializer(app.config["SECRET_KEY"]) ``` 現在當用戶給我們郵件地址時,我們可以使用這個序列器來生成驗證token。通過這種方式,我們來實現一個簡單的賬戶注冊流程。 _myapp/views.py_ ```python from flask import redirect, render_template, url_for from . import app, db from .forms import EmailPasswordForm from .util import ts, send_email @app.route('/accounts/create', methods=["GET", "POST"]) def create_account(): form = EmailPasswordForm() if form.validate_on_submit(): user = User( email = form.email.data, password = form.password.data ) db.session.add(user) db.session.commit() # Now we'll send the email confirmation link subject = "Confirm your email" token = ts.dumps(self.email, salt='email-confirm-key') confirm_url = url_for( 'confirm_email', token=token, _external=True) html = render_template( 'email/activate.html', confirm_url=confirm_url) # 假設在myapp/util.py中定義了send_mail send_email(user.email, subject, html) return redirect(url_for("index")) return render_template("accounts/create.html", form=form) ``` 這段視圖實現了創建用戶并發送郵件到給定的郵件地址。你可能注意到了,我們使用一個模板來給電子郵件生成HTML。我們來看看這個電子郵件模板的例子。 _myapp/templates/email/activate.html_ ``` 你的賬戶已經成功創建<br> 請點擊打開以下鏈接來激活你的郵箱: <p> <a href="{{ confirm_url }}">{{ confirm_url }}</a> </p> <p> --<br> 如果對本郵件有疑問或者有話想說,發郵件給hello@myapp.com. </p> ``` OK,所以現在我們只需要實現一個處理那個郵件中的驗證鏈接的視圖。 _myapp/views.py_ ```python @app.route('/confirm/<token>') def confirm_email(token): try: email = ts.loads(token, salt="email-confirm-key", max_age=86400) except: abort(404) user = User.query.filter_by(email=email).first_or_404() user.email_confirmed = True db.session.add(user) db.session.commit() return redirect(url_for('signin')) ``` 這個視圖只是一個簡單的表單視圖。我們僅僅在開頭添加了`try ... except`來檢查這個token是否有效。這個token包括一個時間戳,所以我們可以調用`ts.loads()`,如果它比`max_age`還大,就拋出一個異常。在這個例子,我們設置`max_age`為86400秒,也即24小時。 > **注意** > 你可以用差不多的方法實現一個郵件重置的功能。僅需要發送帶舊郵件地址和新地址的token的驗證鏈接到新的郵件地址。如果token是有效的,用新的地址更新舊地址。 ## 存儲密碼 用戶管理的第一條軍規是在存儲它們之前使用Bcrypt算法(或者scrypt,不過這里我們將使用Bcrypt)hash密碼。你絕不可明文存儲密碼。這會是嚴重的安全問題并且它損害了你的用戶。所有的繁重工作都已經有第三方的包來完成,所以沒有任何不遵循這個最佳實踐的理由。 > **參見** > OWASP是業界最值得信賴的關于Web應用安全的信息來源之一。看一下他們推薦的一些安全編程規范: > <https://www.owasp.org/index.php/Secure_Coding_Cheat_Sheet#Password_Storage> 我們將繼續前進,使用Flask-Bcrypt插件來實現應用中的bcrypt包。這個插件只是基于`py-bcypt`包的包裝,但是它幫我們處理了一些瑣碎的事(比如在比較hash結果之前檢查字符串編碼)。 myapp/\_\_init\_\_.py ```python from flask_bcrypt import Bcrypt bcrypt = Bcrypt(app) ``` Bcrypt算法之所以深受歡迎,其中一個原因是它的“未來拓展性”。這意味著隨著時間的遷移,當計算能力越來越廉價時,我們可以讓它越來越難通過暴力算法來測試成百上千萬密碼組合來破解。我們用于hash密碼的"rounds"越多,完成一次嘗試所花費的時間就越長。如果在存儲密碼前,我們把它hash了20次,駭客也不得不hash他們的每次猜測20次。 記住如果我們hash密碼20次,需要等到計算結束之后,我們的應用才會做出響應。這意味著,在選擇計算的次數時,我們要取得安全性和可用性的一個平衡點。在給定時間內你能計算的次數取決于你擁有的計算資源,所以最好測試不同的數字,找到能在0.25到0.5秒間完成一個密碼的hash的值。至少,先從12次(12 rounds)開始嘗試吧。 要想測試hash一個密碼的時間,你可以`time`一個簡單的,用于hash一個密碼的Python腳本看看。 _benchmark.py_ ```python from flask_bcrypt import generate_password_hash # 改變round的次數(第二個參數),直到運行時間在0.25到0.5之間。 generate_password_hash('password1', 12) ``` 現在我們可以用`time`命令測幾次看看。 ``` $ time python test.py real 0m0.496s user 0m0.464s sys 0m0.024s ``` 我曾在一個小服務器上做過快速的基準測試,發現12 rounds正好能花費恰當的時間,所以我在這個例子中這么配置。 config.py ``` BCRYPT_LOG_ROUNDS = 12 ``` 既然Flask-Bcrypt已經配置完畢了,是時候開始hash密碼。我們本可以在接受注冊表單的視圖函數中手工完成,但是將來在密碼重置和密碼修改視圖中,同樣的代碼還得一再重復。所以,我們需要抽象hash的過程,這樣即使我們忘記了,我們的應用也會悄悄完成它。秘訣在于我們寫了個**setter**,這樣當設置`user.password = 'password1'`時,密碼在存儲之前就會被用Bcrypt自動hash了。 myapp/models.py ```python from sqlalchemy.ext.hybrid import hybrid_property from . import bcrypt, db class User(db.Model): id = db.Column(db.Integer, primary_key=True, autoincrement=True) username = db.Column(db.String(64), unique=True) _password = db.Column(db.String(128)) @hybrid_property def password(self): return self._password @password.setter def _set_password(self, plaintext): self._password = bcrypt.generate_password_hash(plaintext) ``` 我們使用SQLAlchemy的hybird(混合)拓展來定義一個同時供眾多函數調用的接口屬性。當賦值給`user.password`屬性時,我們的setter會被自動調用。而在setter內,我們會hash純文本密碼并存儲在用戶表里的`_password`列里。既然我們定義`user.password`為混合屬性,那么就可以通過這個屬性來獲取`_password`的值。 現在我們用這個模型來實現注冊視圖。 myapp/views.py ```python from . import app, db from .forms import EmailPasswordForm from .models import User @app.route('/signup', methods=["GET", "POST"]) def signup(): form = EmailPasswordForm() if form.validate_on_submit(): user = User(username=form.username.data, password=form.password.data) db.session.add(user) db.session.commit() return redirect(url_for('index')) return render_template('signup.html', form=form) ``` ## 驗證 既然把用戶加入到數據庫中了,就可以實現驗證功能了。我們想要讓用戶通過表單提交他們的用戶名和密碼(當然,有些時候是郵箱和密碼),然后驗證他們提供的密碼是否正確。如果一切安好,我們將通過設置瀏覽器的cookie來標記他們是已驗證的用戶。下一次他們再提交請求時,通過查看cookie,我們就知道他們已經登錄過了。 先從用WTForms定義一個`UsernamePassword`開始吧。 myapp/forms.py ```python from flask_wtf import Form from wtforms import StringField, PasswordField from wtforms.validators import DataRequired class UsernamePasswordForm(Form): username = StringField('Username', validators=[DataRequired()]) password = PasswordField('Password', validators=[DataRequired()]) ``` 接下來我們將往我們的用戶模型添加一個方法,拿一個字符串跟已存儲的hash過的用戶密碼作比較。 myapp/models.py ```python from . import db class User(db.Model): # [...] columns and properties def is_correct_password(self, plaintext) if bcrypt.check_password_hash(self._password, plaintext): return True return False ``` ### Flask-Login 我們下一個目標是定義一個使用我們的表單類的登錄視圖。如果用戶輸入正確的賬號,我們將使用Flask-Login插件來驗證它們。這個插件簡化了處理用戶會話和驗證的操作。 我們只需做少量的配置就能讓Flask-Login用起來了。 我們先在*\_\_init\_\_.py*定義Flask-Login的`login_manager`。 *myapp/\_\_init\_\_.py* ```python from flask_login import LoginManager # 創建并配置應用 # [...] from .models import User login_manager = LoginManager() login_manager.init_app(app) login_manager.login_view = "signin" @login_manager.user_loader def load_user(userid): return User.query.filter(User.id == userid).first() ``` 我們在這里創建一個叫`LoginManager`的實例,用我們的`app`對象初始化它,定義登錄視圖并告訴它如何通過`id`獲取用戶類。這是使用Flask-Login的基本配置。 > **參見** > 你可以在這里找到自定義Flask-Login的更多信息: > https://flask-login.readthedocs.org/en/latest/#customizing-the-login-process 現在我們來定義處理驗證的`signin`視圖。 _myapp/views.py_ ```python from flask import redirect, url_for from flask_login import login_user from . import app from .forms import UsernamePasswordForm() @app.route('signin', methods=["GET", "POST"]) def signin(): form = UsernamePasswordForm() if form.validate_on_submit(): user = User.query.filter_by(username=form.username.data).first_or_404() if user.is_correct_password(form.password.data): login_user(user) return redirect(url_for('index')) else: return redirect(url_for('signin')) return render_template('signin.html', form=form) ``` 我們僅需要從Flask-Login import `login_user`函數,檢查用戶的驗證信息,并調用`login_user(user)`。你使用`logout_user()`登出當前用戶。 _myapp/views.py_ ```python from flask import redirect, url_for from flask_login import logout_user from . import app @app.route('/signout') def signout(): logout_user() return redirect(url_for('index')) ``` ## 忘記密碼? 你總會需要實現一個“忘記密碼?”功能來允許用戶通過郵件重置自己的賬號密碼。這個地方可能會有潛在安全隱患,因為你不得不讓一個未驗證的用戶接管一個賬戶。我們將會使用類似于郵件驗證的方式來實現密碼重置的功能。 我們將需要一個表單類來請求對給定賬戶的重置,還有一個表單來選擇一個新的密碼(前提是未驗證用戶訪問了賬戶郵箱)。這里假設我們的用戶模型有一個email和一個password,而password是我們之前設置過的混合屬性。 > **注意** > 不要發送密碼重置鏈接給未確認的郵箱!你要確保發送鏈接給正確的人。 我們將需要兩個表單。一個用于請求一個重置鏈接,另一個用于在通過驗證之后修改密碼。 myapp/forms.py ```python from flask_wtf import Form from wtforms import StringField, PasswordField from wtforms.validators import DataRequired, Email class EmailForm(Form): email = StringField('Email', validators=[DataRequired(), Email()]) class PasswordForm(Form): password = PasswordField('Email', validators=[DataRequired()]) ``` 假設我們的密碼重置表單只需要密碼這一欄。許多應用需要用戶兩次輸入他們的新密碼,確保沒有打錯。為了實現這個,我們僅需添加另一個`PasswordField`,并加一個WTForms驗證函數`EqualTo`到主密碼域。 > **參見** > 很多人站在用戶體驗的角度,對什么是設計注冊表單的最佳方式有過許多有趣的討論。我個人喜歡Stack Exchange用戶 Roger Attrill說的一番話:“我們不應該一再要求用戶輸入密碼 - 我們應該要求輸入一次,然后確保‘忘記密碼’能無縫且正確地運行。” > * 你可以在User Experience Stack Exchange讀到更多關于這個話題的內容:<http://ux.stackexchange.com/questions/20953/why-should-we-ask-the-password-twice-during-registration/21141> > * 在Smashing Magazine的文章中,你可以讀到一些簡化注冊和登錄表單的酷想法:<http://uxdesign.smashingmagazine.com/2011/05/05/innovative-techniques-to-simplify-signups-and-logins/> 現在我們將開始邁出第一步,讓用戶可以請求發送一個密碼重置鏈接給綁定的郵箱地址。 myapp/views.py ```python from flask import redirect, url_for, render_template from . import app from .forms import EmailForm from .models import User from .util import send_email, ts @app.route('/reset', methods=["GET", "POST"]) def reset(): form = EmailForm() if form.validate_on_submit() user = User.query.filter_by(email=form.email.data).first_or_404() subject = "Password reset requested" # Here we use the URLSafeTimedSerializer we created in `util` at the beginning of the chapter token = ts.dumps(user.email, salt='recover-key') recover_url = url_for( 'reset_with_token', token=token, _external=True) html = render_template( 'email/recover.html', recover_url=recover_url) # Let's assume that send_email was defined in myapp/util.py send_email(user.email, subject, html) return redirect(url_for('index')) return render_template('reset.html', form=form) ``` 當表單接受到一個郵件地址時,我們取出對應的用戶,生成一個重置token,再發送一個重置密碼URL給用戶。這個URL將引導用戶前往驗證token的視圖,并讓用戶重置密碼。 myapp/views.py ```python from flask import redirect, url_for, render_template from . import app, db from .forms import PasswordForm from .models import User from .util import ts @app.route('/reset/<token>', methods=["GET", "POST"]) def reset_with_token(token): try: email = ts.loads(token, salt="recover-key", max_age=86400) except: abort(404) form = PasswordForm() if form.validate_on_submit(): user = User.query.filter_by(email=email).first_or_404() user.password = form.password.data db.session.add(user) db.session.commit() return redirect(url_for('signin')) return render_template('reset_with_token.html', form=form, token=token) ``` 我們將使用驗證用戶郵箱時用的那個token驗證方式。這個視圖傳遞token回模板,然后模板會在表單中提交正確的URL。讓我們看看這個模板到底長啥樣。 _myapp/templates/reset_with_token.html_ ``` {% extends "layout.html" %} {% block body %} <form action="{{ url_for('reset_with_token', token=token) }}" method="POST"> {{ form.password.label }}: {{ form.password }}<br> {{ form.csrf_token }} <input type="submit" value="Change my password" /> </form> {% endblock %} ``` ## 總結 - 使用itsdangerous包來創建和驗證送往郵箱的token。 - 你可以使用token來驗證郵箱,無論是在用戶注冊賬戶,還是修改郵箱,或者忘記密碼的時候。 - 使用Flask-Login插件來驗證用戶,這樣能避免處理一堆會話管理的麻煩事。 - 總是設想會有惡意的用戶試圖從應用中挖掘漏洞。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看