前后端分离

前后端分离

在前几年的web服务搭建基本都是前后端一起的,就是前端的界面也是后端渲染出来的,这样的话前后端耦合度高,可能只适合于小项目,不容易管理。所以后来的大项目都是使用的前后端分离,这样的话前后端耦合度低,易于管理,提高了拓展性和性能。当然前后端的好处不可能只有这么一点,这里就不多加阐述了。本篇博客也会不断更新,用于解决一些前后端分离的一些不懂的地方。

简单跨域问题

其实就是因为浏览器的同源政策才导致了跨域问题,如果你想深入了解的话,可以看看这片博客——跨域问题. 讲解的蛮好的。

跨域解决方法——代理

我这里就简单在说一下:归根到底就是浏览器的问题,如果只是两个应用服务之间传输数据是不会产生跨域问题,你可能一下子想不通,什么是两个应用服务之间传输数据。比如浏览器的跨域是因为通过浏览器当前域下向当前不同域放出请求,所以,是不是可以开一个代理,该代理就是向当前不同域放出请求。你可能一下子转不过来,下来我就用一个小例子讲解一下

比如一下我写了一个简单的ajax请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<style scoped lang="less">
//...
</style>
<template>
<div class="index">
//...
<Button @click="handleAjax">发出ajax请求</Button>
</div>
</template>
<script>
import axios from 'axios';
export default {
data () {
return {
blogs: [],
}
},
methods: {
//...
handleAjax () {
axios.get('http://blog.qnpic.top/article/article_page/?page=2').then(res=>{
console.log(res.data)
})
}
},
mounted () {
//...
}
}
</script>

img

以上就是请求的网址,是我的那个博客系统首页的博客api,放回就是分页的博客数据,如果直接点击请求就会触发跨域问题

img

所以就是接下来要讲解的东西了。简单来说就是在本地开启一个代理,你可以用node写一个http服务就是替你向当前不同域放出请求,如下就是我写的一个代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const http = require('http');
const request = require('request');

const host = '127.0.0.1';
const port = 8010;

//创建一个api代理服务
const apiServer = http.createServer((req, res)=>{
const url = 'http://blog.qnpic.top'+req.url;
const options = {
url: url
};
function callback(error, response, body){
if(!error && response.statusCode === 200){
// 设置编码类型,否则中文显示乱码
res.setHeader('Content-Type', 'text/plain;charset=UTF-8');
//设置所有域都可以访问
res.setHeader('Access-Control-Allow-Origin', '*');
//返回代理后的内容
res.end(body);
}
}

request.get(options, callback);
})

apiServer.listen(port, host, ()=>{
console.log(`接口代理在http://${host}:${port}/`);
})

该http服务运行在8010端口上,不同端口并不会触发跨域问题,修改请求网址

1
2
3
4
5
handleAjax () {
axios.get('http://127.0.0.1:8010/article/article_page/?page=2').then(res=>{
console.log(res.data)
})
}
  • 效果如下

img

这样就通过代理解决了跨域问题,但是每次都这样写得加上端口,不好看,你就可以配置axios

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import axios from 'axios';

//基本配置
const proxy = {
apiPath: 'http://127.0.0.1:8010/'
}

//Ajax通用配置
proxy.ajax = axios.create({
baseURL: proxy.apiPath
});

//添加响应拦截器
proxy.ajax.interceptors.response.use(res=>{
return res.data
});

export default proxy;

以上就简单配置了axios,在这里面写入请求地址的前缀,业务代码就只需要写相对路径,全部代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<style scoped lang="less">
//...
</style>
<template>
<div class="index">
<div v-for="(item,index) in blogs" style="display:block" @click="handleTest(index)" :key="index">{{item.title}}</div>
<Button @click="handleAjax">发出ajax请求</Button>
</div>
</template>
<script>
import $ from '../libs/axiosProxy';
export default {
components: { vCard },
data () {
return {
blogs: [],
}
},
methods: {
handleTest (id) {
var _this = this;
var blog = _this.blogs[id];
this.$Modal.info({
title: blog.title,
content: blog.body
})
},
getBlogs (page) {
$.ajax.get('/article/article_page/?page='+page).then(res=>{
this.blogs = res.data;
})
}
},
mounted () {
this.getBlogs(1);
}
}
</script>
  • axiosProxy 就是封装的axios配置文件,业务代码就只需要写相对路径就好了,实现效果如下

img

以上就是通过代理解决一些简单跨域问题的解决办法。

前后端开发之登录注册问题

前后端分离以后,由于两个应用的耦合度下降,一些后端开发的框架的前端渲染功能也就弃用了,因此也弃用了一些附带的功能,如,我LightBlog写的django博客丢失了login登录后的sessionid自带传输给浏览器(当然也是要配置的,蛮简单的),前后端分离后就得自己写。

如自己后端产生session存储在redis内存中或者mysql数据库中,然后向前端发送sessionid,前端接受到这个sessionid并存储起来放在cookie里。然后前端配合发送请求自带cookie,后端收到请求分析request请求里的cookie里的sessionid和后端的数据库中进行比较判断是否是登录用户,然后相应返回数据和请求。以上就是前后端的弊端。当然session也是可以用token代替,所以这次我就简单介绍下token的方法。

跨域配置

这里的跨域配置就不是简单如上面的第一种方法那样了。

django 跨域CORS的设置

  • 1.安装 django-cors-headers
1
pip install django-cors-headers
  • 2.添加应用,在settings里面配置
1
2
3
4
5
INSTALLED_APPS = [
...
'corsheaders',
...
]
  • 3.中间件配置,在settings里面配置
1
2
3
4
5
6
MIDDLEWARE = [
...
'corsheaders.middleware.CorsMiddleware', #注意位置
'django.middleware.common.CommonMiddleware',
...
]
  • 4.添加白名单,如我的前端服务部署在8080端口
1
2
3
4
5
CORS_ORIGIN_WHITELIST = (
'http://127.0.0.1:8080',
'http://localhost:8080',
)
CORS_ALLOW_CREDENTIALS = True

上面要注意下,网上的有些教程是CORS_ORIGIN_ALLOW_ALL = True,我刚开始也是确实这样的,刚开始我也是没有用到请求携带cookie所以并没有出现错误,后面的前端设置携带cookie就出现错误,如下

1
Access to XMLHttpRequest at 'http://127.0.0.1:8000/backend/login' from origin 'http://127.0.0.1:8080' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.

上网查询说是,如果后端设置 Access-Control-Allow-Origin: '*', 会有如下报错信息,所以我也是抱着试一试的态度修改了一下就成功了,原理不太懂,sry~

  • 5.settings配置 APPEND_SLASH = False

至此Django的后端跨为问题也是基本解决了,前端就可以携带cookie想我后端发送请求了。^-^

Vue 前端配置

前端配置请求携带cookie也是很简单axios.defaults.withCredentials = true;这行就能全局配置axios请求携带cookie,至于axios的用法就不介绍了

token 配置

以上也是只是解决了跨域问题和请求携带cookie的问题,重头戏token都没介绍了,所以我现在就来介绍一下。token其实也就是一个加密函数,用于验证。

1.在application目录下创建token函数的py文件

img

2.加密token函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import time
import base64
import hmac


def get_token(key, expire=3600):
'''
:param key: str (用户给定的key,需要用户保存以便之后验证token,每次产生token时的key 都可以是同一个key)
:param expire: int(最大有效时间,单位为s)
:return: token
'''
ts_str = str(time.time() + expire)
ts_byte = ts_str.encode("utf-8")
sha1_tshexstr = hmac.new(key.encode("utf-8"),ts_byte,'sha1').hexdigest()
token = ts_str+':'+sha1_tshexstr
b64_token = base64.urlsafe_b64encode(token.encode("utf-8"))
return b64_token.decode("utf-8")

以上也是加密算法,分别是bs64加密和hmac加密,hmac也是哈希加密的一种,不可逆的

3.解密token函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def out_token(key, token):
'''
:param key: 服务器给的固定key
:param token: 前端传过来的token
:return: true,false
'''

# token是前端传过来的token字符串
try:
token_str = base64.urlsafe_b64decode(token).decode('utf-8')
token_list = token_str.split(':')
if len(token_list) != 2:
return False
ts_str = token_list[0]
if float(ts_str) < time.time():
# token expired
return False
known_sha1_tsstr = token_list[1]
sha1 = hmac.new(key.encode("utf-8"),ts_str.encode('utf-8'),'sha1')
calc_sha1_tsstr = sha1.hexdigest()
if calc_sha1_tsstr != known_sha1_tsstr:
# token certification failed
return False
# token certification success
return True
except Exception as e:
print(e)

以上就是token的加解密算法,token里存放的是过期时间和登录用户,后端验证,所以可以存放在mysql里。所以要编写models

1
2
3
4
5
6
7
8
from django.db import models
from django.contrib.auth.models import User


# Create your models here.
class UserToken(models.Model):
user = models.OneToOneField(User, related_name='userToken', on_delete=models.CASCADE, unique=True)
token = models.CharField(max_length=200, blank=True)

记得python manage.py makemigrationspython manage.py migrate生成数据库表

到这里token的加解密就完成了,接下来就是怎么用了,token的储存数据库应该在登录视图中进行,所以登录视图函数如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from django.shortcuts import render,redirect
from django.http import HttpResponse
import json
from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth.models import User
from django.contrib.auth import login,authenticate
from django.contrib.auth.decorators import login_required
from .token_module import out_token, get_token
from .models import UserToken

@csrf_exempt
def loginView(request):
if request.method == 'POST':
print(request.COOKIES)
username = request.POST.get('username','error')
password = request.POST.get('password','error')
print(username,password)
tips = ''
if User.objects.filter(username=username):
# authenticate 判断密码是否正确
user = authenticate(username=username, password=password)
if user:
if user.is_active:
token = get_token(username, 5) ##这里的5就是过期时间,这里为了演示就设置小一点,5秒
UserToken.objects.update_or_create(user=user, defaults={"token": token})
tips = '登陆成功'
res = HttpResponse(json.dumps({'tips':tips, 'token': token}))
return res
else:
tips = ' 密码错误,请重新输入 '
else:
tips = ' 用户不存在,请注册 '
res = HttpResponse(json.dumps({'tips': tips}))
return res
else:
return HttpResponse('nothing')

产生的数据库表如下

img

以上就是登录视图函数,前端vue的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var _this = this;
var username = _this.username;
var password = _this.password;
if (!(username && password)) {
this.$Modal.error({
title: "错误",
content: "用户名和密码不能为空"
});
return;
}
var data = {
username: _this.username,
password: _this.password
};
const _data = new FormData();
_data.append("username", _this.username);
_data.append("password", _this.password);
axios({
method: "post",
url: "http://127.0.0.1:8000/backend/login",
data: _data,
}).then(res => {
console.log(res.data);
_this.msg = res.data;
const { token } = res.data;
document.cookie = `token=${token}&${username};`
});

注意,这里前端是发送的FormData类型的,否则Django后端接收不到。这里的 document.cookie = `token=${token}&${username};` 就是前端设置cookie token

以上就基本解决了token的登陆问题,我写了一个简单的验证视图

1
2
3
4
5
6
7
8
9
10
11
12
13
def islogin(request):
try:
token = request.COOKIES['token']
except:
return HttpResponse(json.dumps({'tips': '您未登录'}))
token_list = token.split('&')
try:
if out_token(token_list[1], token_list[0]):
return HttpResponse(json.dumps({'tips':'登录成功,当前用户'+token_list[1]}))
else:
return HttpResponse(json.dumps({'tips': '您未登录'}))
except Exception as e:
print(e)

前端vue代码全部如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<div>
<label for="username">Username</label>
<Input v-model="username" placeholder="Enter name" id="username" style="width: auto"/>
</div>
<div>
<label for="password">Password</label>
<Input v-model="password" placeholder="Enter password" id="password" style="width: auto"/>
</div>
<br>
<br>
<Button type="primary" @click="handleLogin" key="login">登录</Button>
<Button type="primary" @click="handleIsLogin" key="islogin">测试是否登录</Button>
</div>
</template>

<script>
import axios from "axios";
axios.defaults.withCredentials = true;
export default {
name: "HelloWorld",
data() {
return {
msg: "Welcome to Your Vue.js App",
username: "",
password: ""
};
},
methods: {
handleLogin() {
var _this = this;
var username = _this.username;
var password = _this.password;
if (!(username && password)) {
this.$Modal.error({
title: "错误",
content: "用户名和密码不能为空"
});
return;
}
var data = {
username: _this.username,
password: _this.password
};
console.log(data);
const _data = new FormData();
_data.append("username", _this.username);
_data.append("password", _this.password);
axios({
method: "post",
url: "http://127.0.0.1:8000/backend/login",
data: _data,
// withCredentials: true
}).then(res => {
console.log(res.data);
_this.msg = res.data;
const { token } = res.data;
//localStorage.setItem("sesssionid", sesssionid);
document.cookie = `token=${token}&${username};`
});
},
handleIsLogin () {
axios({
method: "get",
url: "http://127.0.0.1:8000/backend/islogin"
}).then(res=>{
console.log(res.data);
var response = res.data;
this.$Modal.info({
title: '是否登陆',
content: response.tips
})
})
}
}
};
</script>

最终效果图

img

因为设置token过期时间为5秒,所以第一次验证为未登陆。

这次的token设置也是简单讲解成功了

前后端分离开发登录验证——header验证

在前一步我也是实现了在cookie的验证,但是在今天我在进一步做前后端分离项目的时候,服务我的cookie验证不行了,原因是axios发送的请求并没有携带上cookie。很桑心~,也是半天解决不了。emmm,下次再弄吧。所以,就有了这个解决方法,把token验证放在header里,一样的思路,也是一样的方法。

前提准备

前提准备也是一模一样

  • 1.安装 django-cors-headers

  • 2.添加应用,在settings里面配置

  • 3.中间件配置,在settings里面配置

  • 4.添加白名单,如我的前端服务部署在8080端口

  • 5.settings配置 APPEND_SLASH = False

以上就是django 后端的配置,这样前端就可以向后端发送请求。

前端配置

1.封装axios

axios 最好的地方就是可以进行封装,加上请求和回应拦截器,vue-cli项目的目录如上,我们在原有的目录基础上新建api与libs文件夹,libs里新建request.js文件,request.js代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import axios from 'axios'
import {
Message,
Loading
} from 'element-ui'
import router from '../router/index.js' //注意路径与文件名

const service = axios.create({
baseURL: 'http://127.0.0.1:8000', // api 的 base_url
timeout: 50000 // request timeout
})

let loading // 定义loading变量

function startLoading () { // 使用Element loading-start 方法
loading = Loading.service({
lock: true,
text: '加载中...',
background: 'rgba(0, 0, 0, 0.7)'
})
}

function endLoading () { // 使用Element loading-close 方法
loading.close()
}

// 请求拦截 设置统一header
service.interceptors.request.use(
config => {
// 加载
startLoading()
if (localStorage.token) {
config.headers.Authorization = localStorage.token
}
return config
},
error => {
return Promise.reject(error)
}
)

// 响应拦截 401 token过期处理
service.interceptors.response.use(
response => {
endLoading()
return response
},
error => {
// 错误提醒
endLoading()
Message.error(error.response.data)

const { status } = error.response
if (status === 401) {
Message.error('token值无效,请重新登录')
// 清除token
localStorage.removeItem('token')

// 页面跳转
router.push('/login')
}

return Promise.reject(error)
}
)

export default service

request.js中做了三件事

  • 1.创建axios,设置baseURL和超时时间
1
2
3
4
const service = axios.create({
baseURL: 'http://127.0.0.1:8000', // api 的 base_url
timeout: 50000 // request timeout
})
  • 2.拦截请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 请求拦截  设置统一header
service.interceptors.request.use(
config => {
// 加载
startLoading()
if (localStorage.token) {
config.headers.Authorization = localStorage.token
}
return config
},
error => {
return Promise.reject(error)
}
)
  • 3.拦截响应
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 响应拦截  401 token过期处理
service.interceptors.response.use(
response => {
endLoading()
return response
},
error => {
// 错误提醒
endLoading()
Message.error(error.response.data)

const { status } = error.response
if (status === 401) {
Message.error('token值无效,请重新登录')
// 清除token
localStorage.removeItem('token')

// 页面跳转
router.push('/login')
}

return Promise.reject(error)
}
)

项目中我用了element-ui组件库,Message是一个消息弹框,Loading是加载图

登录案列

封装完了axios,我们通过一个登录案例来看看如何在项目中使用。

登录首先撸个界面,放两个input框和一个button,login.vue界面
撸完界面我们在来到api文件夹,建立一个login.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import request from '@/libs/request'
import qs from 'qs'

export function doLogin (username, password) {
let data = {
username,
password
}
data = qs.stringify(data)
return request({
url: '/backend/login',
method: 'post',
data
})
}

我这里引入了qs库,qs.stringify()可以将对象转成字符串形式,这是因为后台接口的要求,一般来说axios传入data对象即可
代码里有一个doLogin方法,接收两个参数:用户名和密码
然后直接调用request.js中封装的axios发送post请求

最后就是在login.vue 运用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<script>
import { doLogin } from '@/api/login'
import request from '@/libs/request'
export default {
//...
methods: {
handleLogin() {
var _this = this;
var username = _this.username;
var password = _this.password;
if (!(username && password)) {
this.$Modal.error({
title: "错误",
content: "用户名和密码不能为空"
});
return;
}
doLogin(username,password).then(res=>{
console.log(res)
if(res.data.status === 200){
const {token} = res.data;
this.$Modal.info({
title: "success",
content: "登陆成功"
});
localStorage.setItem('token', `${token}&${username}`)
}else{
this.$Modal.error({
title: "failure",
content: "登陆失败"
});
}
})
},
handleIsLogin () {
request({
method: "get",
url: "/backend/islogin",
withCredentials: true
}).then(res=>{
console.log(res.data);
var response = res.data;
this.$Modal.info({
title: '是否登陆',
content: response.tips
})
})
}
}
};
</script>

以上定义了两个方法,一个是登录,一个是测试时候登录,后端验证的方法可以看前一章节——cookie的验证登录,但是后端django获取request的header的方法可能得去自行baidu一下。这里是request.META.get(‘HTTP_AUTHORIZATION’).实现效果如下

img

这一部分,重要的部分就是axios的配置拦截器,加上header,其余的部分和上一章节是一样的。

路由拦截

上面实现了登录的功能,但还不够完善,仅仅这样做,未登录的用户直接在浏览器中输入地址不是一样可以访问页面?
这个问题也不难解决,vue-router为我们提供了router.beforeEach,在main.js中设置

1
2
3
4
5
6
7
8
9
10
11
12
router.beforeEach((to, from, next)=>{
iView.LoadingBar.start();
let isLogin = !!localStorage.token
console.log(isLogin)
if (to.path === '/login' || to.path === '/register') {
Util.title(to.meta.title);
next()
} else {
Util.title(to.meta.title);
isLogin ? next() : next('/login')
}
})

加上如上代码,通过localStorage判断是否登录,如果路径不是登录注册的页面,则强制跳回登录界面