Haytham我的博客開發日誌 -- Flask+Vue基於token的登陸狀態與路由管理

指路牌

符合一下關鍵詞,這篇博客有可能會對你有幫助css

  • 不使用工廠函數的Flask應用
  • 不使用藍本的Flask應用
  • Flask跨域配置
  • 基於Token的登陸狀態管理
  • Flask+Vue
  • Vue路由攔截
  • Axios 鉤子

適用場景

這是一篇我的博客搭建的記錄博客,也是一篇關於Flask和Vue的簡單"工具書",最後的代碼會包含Web開發中經常使用的功能。(不全,只是使用頻率相對高的)前端

環境

  • 系統: 無關
  • Flask(Python3)
  • Vue(Node.js)

參考

《Flask Web開發 基於Python的Web應用開發實戰》 Vue.jsvue

背景

我的博客的解決方案那麼多,爲何我要本身再搭建一個呢? 其實搭建我的博客的目的並非爲了寫博客...不然直接使用WordPress了,我的博客只是我想要實踐本身學的技術,同時考慮到之後可能會加入負載均衡、集羣等技術,致使架構大改,或者嘗試實現語音控制等新玩法,一行一行碼出來的在操做的可行性上必然是更好的。ios

代碼功能

博客功能尚不健全,只實現瞭如下的基本功能 前端:註冊登錄,博客建立(markdown編輯器),首頁拉取全部文章,建立博客須要登錄狀態。 後端:以上服務須要的視圖函數,配置跨域,令牌管理與驗證,數據庫管理。git

出於記錄的分享的目的,將實現登陸狀態管理的代碼整理以下github

實現思路

要實現基於令牌的登陸狀態管理,其思路大體以下vue-router

  1. 前端將賬號密碼提交後臺
  2. 後臺驗證,經過這返回token
  3. 前端在每次請求前將token設置到請求頭當中(使用axios鉤子)
  4. 後臺在受保護的視圖函數被調用時獲取請求頭的token,並驗證token,若無問題則容許調用

這是一個大體的思路,後續調用手保護的視圖函數部分,不管是讓先後端完成什麼操做,均可以執行根據須要實現。 如下部分將根據以上思路的順序,展現主要代碼,最後將貼出完成代碼。sql

具體步驟

Flask配置跨域

先後端分離首選須要配置跨域,此處採用後端解決的方案,使用flask_cors庫,代碼以下:vuex

因爲會前端在獲取token後會在每次HTTP請求時將token設置在頭部,我給出的命名爲'token',若使用了其餘名稱,需在'Access-Control-Allow-Headers'中替換數據庫

from flask_cors import CORS

CORS(app,supports_credentials=True)
@app.after_request
def after_request(resp):
	resp = make_response(resp)
	resp.headers['Access-Control-Allow-Origin'] = 'http://127.0.0.1:8080'
	resp.headers['Access-Control-Allow-Methods'] = 'GET,POST'
	resp.headers['Access-Control-Allow-Headers'] = 'content-type,token'
	return resp
複製代碼

Vue經過axios向flask發起登陸請求

前端將獲取的賬號密碼傳遞給後臺,將請求獲取的token寫入Vuex中。(Vuex中會將token寫入localStorage)

let _this = this
axios.post('http://127.0.0.1:5000/login',{
	username:this.username,
	password:this.password,
})
.then(function(response){
	let token = response.data
	_this.changeLogin({Authorization: token})
})
.catch(function(error){
})
複製代碼

Flask實現視圖函數

視圖函數將經過用戶名和密碼,驗證用戶信息,並生成token,返回token。

# Routes
@app.route('/login',methods=['POST'])
def login():
	json = request.get_json()
	user = User.query.filter_by(username = json['username']).first()
	if user.verify_password(json['password']):
		g.currnet_user = user
		token = user.generate_auth_token(expiration=3600)
		return token
	return "wrong password"
複製代碼

Vue配置Axios鉤子

配置Axios鉤子,在每次HTTP請求的頭部都添加token

axios.interceptors.request.use(
	config => {
		let token = localStorage.getItem('Authorization');
		if(token){
			config.headers.common['token'] = token
		}
		return config
	},
	err => {
		return Promise.reject(err);
	});
複製代碼

實現HTTPBasicAuth

flask_httpauth模塊實現的功能不多,其核心部分是咱們須要本身實現@auth.verify_password這個回調函數,當被@auth.login_required修飾的視圖函數被訪問時,會先執行回調函數,在回調函數中將獲取http頭部的token,並驗證該token是否合法,若合法則容許訪問。

from flask_httpauth import HTTPBasicAuth
auth = HTTPBasicAuth()

@auth.verify_password
def verify_password(username_token):
	username_token = request.headers.get('Token')
	if username_token == '':
		return False
	else:
		g.currnet_user = User.verify_auth_token(username_token)
		g.token_used = True
		return g.currnet_user is not None


@auth.login_required
@app.route('/creatpost',methods=['POST'])
def new_post():
	json = request.get_json()
	newpost = Post(title=json['title'],content=json['content'])
	db.session.add(newpost)
	db.session.commit()
	return "200 OK"
複製代碼

備註

以上部分便是實現基於令牌管理的代碼核心部分,閱讀以上代碼知曉思路便可,因爲其還調用了諸如ORM中的函數的緣由,因此只有以上部分代碼功能並不健全,請參考下面簡化後的完整代碼。

完整代碼

強調:如下代碼出於簡化的目的,皆爲實現功能的最基本碼,並無遵循各類規範。

Flask

import os
from flask import Flask,make_response,render_template,redirect,url_for,jsonify,g,current_app,request,session
from flask_cors import CORS
from flask_sqlalchemy import SQLAlchemy
from flask_httpauth import HTTPBasicAuth
from flask_login import login_user,UserMixin,LoginManager,login_required
from werkzeug.security import generate_password_hash,check_password_hash
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer

basedir = os.path.abspath(os.path.dirname(__file__))

# SQLite
app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret-key'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(basedir,'data.sqlite')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db = SQLAlchemy(app)

# CORS
CORS(app,supports_credentials=True)
@app.after_request
def after_request(resp):
	resp = make_response(resp)
	resp.headers['Access-Control-Allow-Origin'] = 'http://127.0.0.1:8080'
	resp.headers['Access-Control-Allow-Methods'] = 'GET,POST'
	resp.headers['Access-Control-Allow-Headers'] = 'content-type,token'
	return resp

# Http auth
auth = HTTPBasicAuth()

@auth.verify_password
def verify_password(username_token):
	username_token = request.headers.get('Token')
	if username_token == '':
		return False
	else:
		g.currnet_user = User.verify_auth_token(username_token)
		g.token_used = True
		return g.currnet_user is not None

@auth.error_handler
def auth_error():
	return unauthorized('Invalid credentials')

# Routes
@app.route('/login',methods=['POST'])
def login():
	json = request.get_json()
	user = User.query.filter_by(username = json['username']).first()
	if user.verify_password(json['password']):
		g.currnet_user = user
		token = user.generate_auth_token(expiration=3600)
		return token
	return "wrong password"

@app.route('/register',methods=['POST'])
def register():
	json = request.get_json()
	email = json['username'] + '@email.com'
	user = User(email=email,username=json['username'],password=json['password'])
	db.session.add(user)
	db.session.commit()
	return "200 OK register"


@app.route('/postlist')
def article():
	ptemp = Post.query.all()
	return jsonify({
			'posts': [post.to_json() for post in ptemp],
		})

@auth.login_required
@app.route('/creatpost',methods=['POST'])
def new_post():
	json = request.get_json()
	newpost = Post(title=json['title'],content=json['content'])
	db.session.add(newpost)
	db.session.commit()
	return "200 OK"

def unauthorized(message):
    response = jsonify({'error': 'unauthorized', 'message': message})
    response.status_code = 401
    return response

# ORM
class User(UserMixin,db.Model):
	__tablename__ = 'users'
	id = db.Column(db.Integer, primary_key=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))

	@property
	def password(self):
		raise AttributeError('password is not a 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)

	def generate_auth_token(self,expiration):
		s = Serializer(current_app.config['SECRET_KEY'],expires_in = expiration)
		return  s.dumps({'id':self.id}).decode('utf-8')

	@staticmethod
	def verify_auth_token(token):
		s = Serializer(current_app.config['SECRET_KEY'])
		try:
			data = s.loads(token)
		except:
			return None
		return User.query.get(data['id'])

class Post(db.Model):
	__tablename__ = 'posts'
	id = db.Column(db.Integer, primary_key=True)
	title = db.Column(db.String(64),unique=True,index=True)
	content = db.Column(db.String(64))

	def to_json(self):
		json_post = {
			'title': self.title,
			'content': self.content,
		}
		return json_post

if __name__ == '__main__':
	db.drop_all()
	db.create_all()
	app.run()
複製代碼

Vue -- main.js

import Vue from 'vue';
import App from './App.vue';
import VueRouter from 'vue-router';
import router from './router';
import iView from 'iview';
import 'iview/dist/styles/iview.css';
import axios from 'axios';
import vueAxios from 'vue-axios';
import store from './store';
import Vuex from 'vuex'

Vue.config.productionTip = false

Vue.use(VueRouter)
Vue.use(iView)
Vue.use(vueAxios,axios)
Vue.use(Vuex)

router.afterEach(route=>{
	window.scroll(0,0);
})

router.beforeEach((to,from,next)=>{
	let token = localStorage.getItem('Authorization');
	if(!to.meta.isLogin){
		next()
	}else{
		let token = localStorage.getItem('Authorization');
		if(token == null || token == ''){
			next('/')
		}else{
			next()
		}
	}
})

axios.interceptors.request.use(
	config => {
		let token = localStorage.getItem('Authorization');
		if(token){
			config.headers.common['token'] = token
		}
		return config
	},
	err => {
		return Promise.reject(err);
	});


new Vue({
  el:'#app',
  render: h => h(App),
  router,
  store,
})

複製代碼

Vue -- Vuex

import Vue from 'vue';
import Vuex from 'vuex';
import store from './index';

Vue.use(Vuex);

export default new Vuex.Store({
	state:{
		Authorization: localStorage.getItem('Authorization') ? localStorage.getItem('Authorization') : ''
	},
	mutations:{
		changeLogin (state, user) {
			state.Authorization = user.Authorization;
			localStorage.setItem('Authorization', user.Authorization);
		}
	},
})
複製代碼

Vue -- router

import Vue from 'vue'
import Router from 'vue-router'

import home from '../components/home.vue'
import articleDetail from '../components/articleDetail'
import createPost from '../components/createPost'

Vue.use(Router)
export default new Router({
	mode:'history',
	routes:[
		{
			path:'/',
			component:home,
			name:'home',
			meta:{
				isLogin:false
			}
		},
		{
			path:'/article',
			component:articleDetail,
			name:'article',
			meta:{
				isLogin:false
			}
		},
		{
			path:'/createpost',
			component:createPost,
			name:'createpost',
			meta:{
				isLogin:true
			}
		},
	]
})
複製代碼

Vue -- Components -- home.vue

<template>
	<div class="super">
		<div class="header">
			<div class="buttomDiv">
				<Button type="success" class="loginButton" @click="showLoginModal">Login</Button>
				<Button type="primary" class="loginButton" @click="showRegisterModal">Register</Button>
			</div>
		</div>

		<div class = "content">
			<div class="contentLeft">
				<div
					v-for = "post in blogList"
					>
					<thumbnail 
						v-bind:title=post.title
						v-bind:content=post.content
					></thumbnail>
				</div>
			</div>
			<div class="contentRight"></div>
			
		</div>

		<Modal v-model="registerModalStatus" @on-ok="registerEvent">
			<p>Register</p>
			<Input v-model="username" placeholder="Username" style="width: 300px" />
			<Input v-model="password" placeholder="Password" style="width: 300px" />
		</Modal>

		<Modal v-model="loginModalStatus" @on-ok="loginEvent">
			<p>Login</p>
			<Input v-model="username" placeholder="Username" style="width: 300px" />
			<Input v-model="password" placeholder="Password" style="width: 300px" />
		</Modal>

	</div>
</template>

<script>
	import axios from 'axios'
	import {mapMutations} from 'vuex'
	import store from '../store'
	import thumbnail from './articleThumbnail.vue'

	export default{
		name: 'home',
		data:function(){
			return {
				loginModalStatus:false,
				registerModalStatus:false,
				username:'',
				password:'',
				blogList:'',
			}
		},
		components:{
			thumbnail:thumbnail,
		},
		created(){
			localStorage.removeItem("Authorization","")
			let _this = this
			axios.get('http://127.0.0.1:5000/postlist')
					.then(function(response){
						_this.blogList = response.data.posts
					})
					.catch(function(error){
					})
		},
		methods:{
			...mapMutations([
				'changeLogin'
			]),
			showRegisterModal:function(){
				this.registerModalStatus = true;
			},
			showLoginModal:function(){
				this.loginModalStatus = true;
			},
			registerEvent:function(){
				let that = this
				axios.post('http://127.0.0.1:5000/register',{
					username:this.username,
					password:this.password,
					})
				.then(function(res){

				})
				.catch(function(error){

				})
			},
			loginEvent:function(){
				let _this = this
				axios.post('http://127.0.0.1:5000/login',{
							username:this.username,
							password:this.password,
						})
					.then(function(response){
						let token = response.data
						_this.changeLogin({Authorization: token})
					})
					.catch(function(error){
					})
			},
			navigator:function(){
				this.$router.push("/article")
			},

		},
	}
</script>

<style scoped>

</style>

複製代碼

後記

完整代碼github地址 haythamBlog haythamBlog_flask

要獲取更多Haytham原創文章,請關注公衆號"許聚龍":

個人微信公衆號
相關文章
相關標籤/搜索