「 從今天開始,準備從頭開始搭建一個基於flask的鑑權系統,一點一滴,積累於生活」
本文涉及到如下知識點
1. flask-login的簡單使用
2. 本地鑑權實踐
3. GitHub鑑權登陸實踐,flask-github使用
4. 可擴展的表結構設計思路
我們首先設計一個User用戶表,裡面的欄位可以包括username,password,email等用戶信息,大致如下
usernamepasswordemailuser1p1user1@gmail.comuser2p2user2@gmail.comuser3p3user3@gmail.com因為我們還會涉及到第三方登陸,那麼為了後面便於擴展,再設計一張表,就命名為ThirdAuth,裡面可以包括user_id,與user表關聯,oauth_name,oauth_access_token等欄位
user_idoauth_nameoauth_access_tokenuser-id1auth1token1user-id2auth2token2user-id3auth3token3這樣,oauth_name欄位可以用來存儲第三方來源,例如github,以此來區別不同的第三方登陸用戶。
到此,一個簡單的表結構就設計好了。
03.OAuth鑑權
簡單來說,為一個網站添加第三方登錄指的是提供通過其他第三方平臺帳號登入當前網站的功能。比如,使用QQ、微信、新浪微博帳號登錄。對於某些網站,甚至可以僅提供社交帳號登錄的選項,這樣網站本身就不需要管理用戶帳戶等相關信息。對用戶來說,使用第三方登錄可以省去註冊的步驟,更加方便和快捷。這裡,我就是使用GitHub的OAuth認證來進行鑑權登陸。
這裡首先需要在自己的GitHub上創建一個OAuth程序,非常簡單,訪問這個地址:https://github.com/settings/applications/new,按照要求填寫即可。
其中的callback需要填寫一個回調函數,具體後面再說。
創建好這個OAuth程序後,我們就會獲得Client ID(客戶端ID)和Client Secret(客戶端密鑰),在後面調用Github的API時使用。
04. 本地鑑權
根據剛才的表結構設計,對於本地鑑權,可以在models.py文件中創建一個WebUser類,定義對應的資料庫欄位。
對於password,不建議直接在資料庫中存儲明文,所以這裡使用了werkzeug庫來做hash轉換。
同時WebUser類還繼承自flask-login的UserMixin類,該類實現了關鍵的用於檢測用戶狀態的方法:
is_authenticated,如果用戶已經登陸返回True,否則返回False
is_active,如果用戶允許登陸,返回True,否則返回Flase
is_anonymous,對普通用戶必須返回False
get_id,必須返回用戶的唯一標識
後面主要使用到了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. 定義登陸登出函數
當表單正確提交時,如果用戶名和密碼匹配,則提示登陸成功,並跳轉頁面,否則提示登陸失敗。
因為是使用flask-login擴展,所以登陸直接調用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裝飾器保證只有已經登陸的用戶才能調用該函數。
@auth.route('/logout')
@login_required
def logout():
flash('You have logged out!')
return redirect(url_for('main.index'))
4. web模板
創建一個base.html基礎模板(繼承自flask-bootstrap模板),後面其他頁面都繼承自該模板,這樣可以保證所有的頁面風格統一,也可以減少代碼量。
{% 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. 初始化資料庫
使用flask-script擴展,定義runserver和shell兩個命令行命令,shell用於資料庫等調測操作,runserver用於啟動服務。
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,進入調測shell,然後輸入db.create_all()和WebUser.init_user(),分別創建表並插入原始用戶。
7. 登陸測試
在輸入框分別鍵入admin@163.com和123456,並點擊登陸,發現可以正常登陸,效果如下
其中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鑑權
1. 創建表結構
類似的,定義需要的欄位即可
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. 發送授權請求
這一步,flask-github已經為我們封裝好了,直接調用即可
@auth.route('/githublogin', methods=['GET', 'POST'])
def githublogin():
return github.authorize(scope='repo')
這裡需要說明,該調用需要用到我們前面獲得的客戶端ID和密鑰,我這裡把相關信息寫到了一個配置文件中,並在初始化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令牌
當用戶同意授權或拒絕授權後,GitHub會將用戶重定向到我們設置的callback URL,我們需要創建一個視圖函數來處理回調請求。如果用戶同意授權,GitHub會在重定向的請求中加入code參數,一個臨時生成的值,用於程序再次發起請求交換access token。程序這時需要向請求訪問令牌URL(即https://github.com/login/oauth/access_token)發起一個POST請求,附帶客戶端ID、客戶端密鑰、code。請求成功後的的響應會包含訪問令牌(Access Token)。
很幸運,上面的一系列工作flask-github會在背後替我們完成。我們只需要創建一個視圖函數,定義正確的URL規則(這裡的URL規則需要和GitHub上填寫的Callback URL匹配),並為其附加一個github.authorized_handler裝飾器。另外,這個函數要接受一個access_token參數,GitHub-Flask會在授權請求結束後通過這個參數傳入訪問令牌。
同時判斷,該用戶是否存在於資料庫中,並更新相關欄位。
@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開發文檔資料可以查看:
https://developer.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps/
更多flask-github資料可以查看:
https://github-flask.readthedocs.io/en/latest/
4. 更新登陸頁面
更新登陸頁面,增加一個以GitHub登陸的按鈕
<div class="col-md-12">
<a class="btn btn-primary" href="{{ url_for('auth.githublogin') }}">Login with GitHub</a>
</div>
現在的登陸頁面為
更新index路由函數,增加以GitHub登陸時的頭像
@main.route('/', methods=['GET', 'POST'])
def index():
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')
又因為在callback函數中增加了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
往期文章:
使用sklearn+jieba完成一個文檔分類器
從頭完成一個restful API服務
從頭搭建一個HTTPS網站
運用百度API來測評女神的質量
聖誕來臨,爬取女神美圖放鬆下
用Python探索紅樓夢裡的關係
Kubernetes系列入門文章
爬取女神王祖賢的海報評論,看看粉絲們是怎麼說
果喜歡我的文章,那就關注我吧!
萬分感謝!
歡迎留言討論