“ 從今天開始,準(zhǔn)備從頭開始搭建一個基于flask的鑒權(quán)系統(tǒng),一點(diǎn)一滴,積累于生活”
本文涉及到如下知識點(diǎn) 1. flask-login的簡單使用 2. 本地鑒權(quán)實(shí)踐 3. GitHub鑒權(quán)登陸實(shí)踐,flask-github使用 4. 可擴(kuò)展的表結(jié)構(gòu)設(shè)計(jì)思路
02.表結(jié)構(gòu)設(shè)計(jì)
我們首先設(shè)計(jì)一個User用戶表,里面的字段可以包括username,password,email等用戶信息,大致如下
username | password | email |
---|
user1 | p1 | user1@gmail.com | user2 | p2 | user2@gmail.com | user3 | p3 | user3@gmail.com |
因?yàn)槲覀冞€會涉及到第三方登陸,那么為了后面便于擴(kuò)展,再設(shè)計(jì)一張表,就命名為ThirdAuth,里面可以包括user_id,與user表關(guān)聯(lián),oauth_name,oauth_access_token等字段
user_id | oauth_name | oauth_access_token |
---|
user-id1 | auth1 | token1 | user-id2 | auth2 | token2 | user-id3 | auth3 | token3 |
這樣,oauth_name字段可以用來存儲第三方來源,例如github,以此來區(qū)別不同的第三方登陸用戶。 到此,一個簡單的表結(jié)構(gòu)就設(shè)計(jì)好了。
03.OAuth鑒權(quán)
簡單來說,為一個網(wǎng)站添加第三方登錄指的是提供通過其他第三方平臺賬號登入當(dāng)前網(wǎng)站的功能。比如,使用QQ、微信、新浪微博賬號登錄。對于某些網(wǎng)站,甚至可以僅提供社交賬號登錄的選項(xiàng),這樣網(wǎng)站本身就不需要管理用戶賬戶等相關(guān)信息。對用戶來說,使用第三方登錄可以省去注冊的步驟,更加方便和快捷。這里,我就是使用GitHub的OAuth認(rèn)證來進(jìn)行鑒權(quán)登陸。 這里首先需要在自己的GitHub上創(chuàng)建一個OAuth程序,非常簡單,訪問這個地址:https://github.com/settings/applications/new,按照要求填寫即可。
其中的callback需要填寫一個回調(diào)函數(shù),具體后面再說。 創(chuàng)建好這個OAuth程序后,我們就會獲得Client ID(客戶端ID)和Client Secret(客戶端密鑰),在后面調(diào)用Github的API時使用。
04. 本地鑒權(quán)
1. 創(chuàng)建表結(jié)構(gòu) 根據(jù)剛才的表結(jié)構(gòu)設(shè)計(jì),對于本地鑒權(quán),可以在models.py文件中創(chuàng)建一個WebUser類,定義對應(yīng)的數(shù)據(jù)庫字段。 對于password,不建議直接在數(shù)據(jù)庫中存儲明文,所以這里使用了werkzeug庫來做hash轉(zhuǎn)換。 同時WebUser類還繼承自flask-login的UserMixin類,該類實(shí)現(xiàn)了關(guān)鍵的用于檢測用戶狀態(tài)的方法: is_authenticated,如果用戶已經(jīng)登陸返回True,否則返回False is_active,如果用戶允許登陸,返回True,否則返回Flase is_anonymous,對普通用戶必須返回False get_id,必須返回用戶的唯一標(biāo)識 后面主要使用到了is_authenticated方法。 而init_user是用來初始化第一個用戶的,password等幾個方法分別是用來檢測密碼是否正確的。 class WebUser(UserMixin, db.Model): __tablename__ = 'webuser' id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.String(64), unique=True, index=True) email = db.Column(db.String(64), unique=True, index=True) username = db.Column(db.String(64), unique=True, index=True) password_hash = db.Column(db.String(128))
@staticmethod def init_user(): users = WebUser.query.filter_by(username='admin').first() if users is None: users = WebUser(email='admin@123.com', username='admin', user_id=time.time()) users.password = '123456' db.session.add(users) db.session.commit()
@property def password(self): raise AttributeError('password is not readable attribute')
@password.setter def password(self, password): self.password_hash = generate_password_hash(password)
def verify_password(self, password): return check_password_hash(self.password_hash, password)
2. 定義登陸表單 登陸表單比較簡單,兩個輸入框,分別為用戶名和密碼,一個check box,用來選擇是否保持登陸,外加一個提交按鈕 class LoginForm(FlaskForm): email = StringField('Email', validators=[DataRequired(), Length(1, 64), Email()]) password = PasswordField('Password', validators=[DataRequired()]) remember_me = BooleanField('Keep me logged in') submit = SubmitField('Log In')
3. 定義登陸登出函數(shù) 當(dāng)表單正確提交時,如果用戶名和密碼匹配,則提示登陸成功,并跳轉(zhuǎn)頁面,否則提示登陸失敗。
因?yàn)槭鞘褂胒lask-login擴(kuò)展,所以登陸直接調(diào)用login_user()即可。 @auth.route('/login', methods=['GET', 'POST']) def login(): form = LoginForm() if form.validate_on_submit(): user = WebUser.query.filter_by(email=form.email.data).first() if user is not None and user.verify_password(form.password.data): login_user(user, form.remember_me.data) return redirect(request.args.get('next') or url_for('main.index')) flash('Invalid username or password!') return render_template('auth/login.html', form=form)
對于登出,同樣簡單,注意需要用login_required裝飾器保證只有已經(jīng)登陸的用戶才能調(diào)用該函數(shù)。 @auth.route('/logout') @login_required def logout(): flash('You have logged out!') return redirect(url_for('main.index'))
4. web模板 創(chuàng)建一個base.html基礎(chǔ)模板(繼承自flask-bootstrap模板),后面其他頁面都繼承自該模板,這樣可以保證所有的頁面風(fēng)格統(tǒng)一,也可以減少代碼量。 {% extends 'bootstrap/base.html' %}
{% block title %}Flasky{% endblock %}
{% block navbar %} <div class='navbar navbar-inverse' role='navigation'> <div class='container'> <div class='navbar-header'> <button type='button' class='navbar-toggle' data-toggle='collapse' data-target='.navbar-collapse'> <span class='sr-only'>Toggle navigation</span> <span class='icon-bar'></span> <span class='icon-bar'></span> <span class='icon-bar'></span> </button> <a class='navbar-brand' href='/'>WebAuth</a> </div> <div class='navbar-collapse collapse'> <ul class='nav navbar-nav'> <li><a href='/'>Home</a></li> </ul> <ul class='nav navbar-nav navbar-right'> {% if current_user.is_authenticated %} <li><a href='{{ url_for('auth.logout') }}'>Sign Out</a></li> {% else %} <li><a href='{{ url_for('auth.login') }}'>Sign In</a></li> {% endif %} </ul> </div> </div> </div> {% endblock %}
{% block content %} <div class='container'> {% block page_content %}{% endblock %} </div> {% endblock %}
5. 登陸頁面 登陸頁面繼承自base.html模板,并使用wtf快速渲染表單
{% extends 'base.html' %} {% import 'bootstrap/wtf.html' as wtf %} {% block title %}Login{% endblock %} {% block page_content %} <div class='page-header'> <h1>Login</h1> </div>
<div class='col-md-4'> {{ wtf.quick_form(form) }} </div> {% endblock %}
最后的登陸頁面為
6. 初始化數(shù)據(jù)庫 使用flask-script擴(kuò)展,定義runserver和shell兩個命令行命令,shell用于數(shù)據(jù)庫等調(diào)測操作,runserver用于啟動服務(wù)。
from app import create_app, db from flask_script import Manager, Shell, Server from app.models import WebUser
app = create_app('testing') manager = Manager(app)
def make_shell_context(): return dict(app=app, db=db, WebUser=WebUser)
manager.add_command('runserver', Server(use_debugger=True, host='0.0.0.0', port='9982')) manager.add_command('shell', Shell(make_context=make_shell_context))
if __name__ == '__main__': manager.run(default_command='runserver')
在命令行輸入python manage.py shell,進(jìn)入調(diào)測shell,然后輸入db.create_all()和WebUser.init_user(),分別創(chuàng)建表并插入原始用戶。
7. 登陸測試 在輸入框分別鍵入admin@163.com和123456,并點(diǎn)擊登陸,發(fā)現(xiàn)可以正常登陸,效果如下
其中index頁面代碼為 {% extends 'base.html' %} {% import 'bootstrap/wtf.html' as wtf %} {% block title %}Login{% endblock %} {% block page_content %} <div class='container'> {% for message in get_flashed_messages() %} <div class='alert alert-warning'> <button type='button' class='close' data-dismiss='alert'>×</button> {{ message }} </div> {% endfor %} </div> <div class='page-header'> <h1>Home</h1> </div> <div class='col-md-4'> 這是首頁 </div> <div class='col-md-12'> {% if current_user.is_authenticated %} {{ current_user.username }} {{ name }} <div> <img style='-webkit-user-select: none;' src='{{ avatar }}' /> </div> {% else %} Your are not login yet {% endif %} </div> {% endblock %}
05. GitHub鑒權(quán)
1. 創(chuàng)建表結(jié)構(gòu)
類似的,定義需要的字段即可 class ThirdOAuth(db.Model): __tablename__ = 'thirdoauth' id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.String(64), unique=True, index=True) oauth_name = db.Column(db.String(128)) oauth_id = db.Column(db.String(128), unique=True, index=True) oauth_access_token = db.Column(db.String(128), unique=True, index=True) oauth_expires = db.Column(db.String(64), unique=True, index=True)
2. 發(fā)送授權(quán)請求 這一步,flask-github已經(jīng)為我們封裝好了,直接調(diào)用即可
@auth.route('/githublogin', methods=['GET', 'POST']) def githublogin(): return github.authorize(scope='repo')
這里需要說明,該調(diào)用需要用到我們前面獲得的客戶端ID和密鑰,我這里把相關(guān)信息寫到了一個配置文件中,并在初始化flask app時加載 配置文件 class Config: SECRET_KEY = 'hardtoguess' GITHUB_CLIENT_ID = 'cf1AA35ef11d20bcdXXX' GITHUB_CLIENT_SECRET = 'ba7c8c8SSe9cd574eb3da1b5e704d11d35aXXXb8'
初始化app def create_app(config_name): app = Flask(__name__) app.config.from_object(config[config_name]) config[config_name].init_app(app) db.init_app(app) cors.init_app(app, supports_credentials=True) login_manager.init_app(app) bootstrap.init_app(app) github.init_app(app)
from .main import main as main_blueprint app.register_blueprint(main_blueprint) from .api_1_0 import api_1_0 as api_blueprint app.register_blueprint(api_blueprint) from .auth import auth as auth_blueprint app.register_blueprint(auth_blueprint, url_prefix='/auth')
return app
3. 獲取access令牌 當(dāng)用戶同意授權(quán)或拒絕授權(quán)后,GitHub會將用戶重定向到我們設(shè)置的callback URL,我們需要創(chuàng)建一個視圖函數(shù)來處理回調(diào)請求。如果用戶同意授權(quán),GitHub會在重定向的請求中加入code參數(shù),一個臨時生成的值,用于程序再次發(fā)起請求交換access token。程序這時需要向請求訪問令牌URL(即https://github.com/login/oauth/access_token)發(fā)起一個POST請求,附帶客戶端ID、客戶端密鑰、code。請求成功后的的響應(yīng)會包含訪問令牌(Access Token)。
很幸運(yùn),上面的一系列工作flask-github會在背后替我們完成。我們只需要創(chuàng)建一個視圖函數(shù),定義正確的URL規(guī)則(這里的URL規(guī)則需要和GitHub上填寫的Callback URL匹配),并為其附加一個github.authorized_handler裝飾器。另外,這個函數(shù)要接受一個access_token參數(shù),GitHub-Flask會在授權(quán)請求結(jié)束后通過這個參數(shù)傳入訪問令牌。 同時判斷,該用戶是否存在于數(shù)據(jù)庫中,并更新相關(guān)字段。 @auth.route('/callback/github') @github.authorized_handler def authorized(access_token): if access_token is None: flash('Login Failed!') return redirect(url_for('main.index')) response = github.get('user', access_token=access_token) username = response['login'] u_id = response['id'] email = response['email'] avatar = response['avatar_url'] user = WebUser.query.filter_by(username=username).first() if user is None: user = WebUser(username=username, user_id=time.time()) db.session.add(user) db.session.commit() thirduser = ThirdOAuth(user_id=WebUser.query.filter_by(username=username).first().user_id, oauth_name='github', oauth_access_token=access_token, oauth_id=u_id) db.session.add(thirduser) db.session.commit() login_user(user) user.email = email db.session.add(user) db.session.commit() session['userid'] = user.user_id return render_template('index.html', avatar=avatar) else: thirduser = ThirdOAuth.query.filter_by(user_id=user.user_id).first() thirduser.oauth_access_token = access_token db.session.add(thirduser) db.session.commit() user.email = email db.session.add(user) db.session.commit() login_user(user) session['userid'] = user.user_id return render_template('index.html', avatar=avatar)
更多的GitHub開發(fā)文檔資料可以查看: https://developer.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps/ 更多flask-github資料可以查看: https://github-flask./en/latest/
4. 更新登陸頁面 更新登陸頁面,增加一個以GitHub登陸的按鈕 <div class='col-md-12'> <a class='btn btn-primary' href='{{ url_for('auth.githublogin') }}'>Login with GitHub</a> </div>
現(xiàn)在的登陸頁面為
更新index路由函數(shù),增加以GitHub登陸時的頭像 @main.route('/', methods=['GET', 'POST']) def index(): # print(session) if current_user.is_authenticated: if 'userid' in session: user = ThirdOAuth.query.filter_by(user_id=session['userid']).first() if user: response = github.get('user', access_token=user.oauth_access_token) avatar = response['avatar_url'] username = response['login'] return render_template('index.html', username=username, avatar=avatar) return render_template('index.html')
又因?yàn)樵赾allback函數(shù)中增加了session.userid字段,所以在logout時,把該字段手動刪除 @auth.route('/logout') @login_required def logout(): logout_user() if 'userid' in session: session.pop('userid') flash('You have logged out!') return redirect(url_for('main.index'))
5. 測試GitHub登陸 登陸成功后,如下
至此,登陸功能完成
完整代碼: https://github.com/zhouwei713/flask-webauth
|