Haytham Personal Blog Development Log-Flask+Vue Logon Status and Routing Management Based on token

Keywords: Front-end Vue axios JSON Session

The signpost

In line with the key words, this blog may be helpful to you.

  • Flask applications without factory functions
  • Flask Applications Without Blueprint
  • Flask cross-domain configuration
  • Login Status Management Based on Token
  • Flask+Vue
  • Vue Routing Interception
  • Axios hook

Applicable scenario

This is a personal blog built record blog, but also a simple "tool book" about Flask and Vue, the final code will contain common functions in Web development. (incomplete, but relatively high frequency)

Environmental Science

  • System: Irrelevant
  • Flask(Python3)
  • Vue(Node.js)

Reference resources

Flask Web Development Python-based Web Application Development Practice Vue.js

background

There are so many solutions for personal blog, why should I build another one by myself? In fact, the purpose of building a personal blog is not to write a blog... Otherwise, I will use WordPress directly. Personal blog is just a technology I want to learn by myself. Considering the possibility of adding load balancing, clustering and other technologies in the future, it will lead to major structural changes, or try to implement new methods such as voice control, one line by one code. The feasibility of the operation is bound to be better.

Code function

The function of blog is not perfect, it only realizes the following basic functions Front-end: Register for login, create a blog (markdown editor), pull all articles on the home page, create a blog needs to login status. Backend: View functions required by the above services, configuring cross-domain, token management and validation, database management.

For record sharing purposes, the code for login state management is organized as follows

Implementation ideas

To achieve token-based login status management, the ideas are as follows.

  1. Front End Submits Account Password to Background
  2. Background validation, through which token is returned
  3. The front end sets token to the request header before each request (using axios hooks)
  4. The background obtains the token of the request header when the protected view function is called, verifies the token, and allows the call if there is no problem.

This is a general idea, follow-up calls to hand-protected view function part, regardless of what the front and back end to complete, can be implemented as needed. The following sections will show the main code according to the order of the above ideas, and finally post the completion code.

Specific steps

Flask configuration across domains

The first choice for front-end and back-end separation is to configure cross-domain. Here, the solution of back-end solution is adopted. flask_cors library is used. The code is as follows:

Since the front end of the conference sets token at the head of each HTTP request after obtaining token, I give it the name'token'. If other names are used, they need to be replaced in'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 initiates a login request to flask via axios

The front end passes the obtained account password to the background and writes the requested token into Vuex. (Token will be written to local Storage in Vuex)

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 Implements View Functions

The view function verifies the user information through the username and password, generates token and returns 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 configuration Axios hook

Configure the Axios hook to add token at the head of each HTTP request

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

Implementing HTTP Basic Auth

The flask_httpauth module implements very few functions, and the core part is that we need to implement @auth.verify by ourselves._ The callback function password, when @auth.login _ When the required modified view function is accessed, the callback function is executed first. In the callback function, the token of the http header is obtained, and the validity of the token is verified. If it is valid, access is allowed.

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"

Remarks

The above part is the core part of the code based on token management. Read the above code to know the idea. Because it also calls functions such as ORM, only the above part of the code is not perfect. Please refer to the simplified complete code below.

Complete code

Emphasis: For the purpose of simplification, the following codes are the most basic codes to achieve functions, and do not follow various specifications.

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>

Epilogue

Full code github address haythamBlog haythamBlog_flask

For more original Haytham articles, please pay attention to the public number "Xu Julong":

Posted by andychurchill on Sun, 15 Sep 2019 03:54:50 -0700