JWT(JSON WEB TOKEN)
WT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
JWT构成
header
- 声明类型,这里是jwt
- 声明加密的算法 通常直接使用 HMAC SHA256
playload
存放有效信息。包括三部分:标准中注册的声明、公共的声明、私有的声明
标准中注册的声明 (建议但不强制使用) :
- iss: jwt签发者,可以在解密的时候验证是否是该签发者
- sub: jwt所面向的用户
- aud: 接收jwt的一方
- exp: jwt的过期时间,这个过期时间必须要大于签发时间
- nbf: 不早于什么时间处理JWT,“nbf”索赔的处理要求当前日期/时间必须晚于或等于“nbf”索赔中列出的不早于日期/时间。
- iat: jwt的签发时间
- jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
公共的声明 :
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
私有的声明 :
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
signature
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
- header (base64后的)
- payload (base64后的)
- secret
这个部分需要base64加密后的header和base64加密后的payload使用.
连接组成的字符串,然后通过header中声明的加密方式进行加盐secret
组合加密,然后就构成了jwt的第三部分。
pyjwt
官方文档,可以参考该文档进行开发:https://pyjwt.readthedocs.io/en/stable/installation.html
python request
request.META
request.META 是一个Python字典,包含了所有本次HTTP请求的Header信息,比如用户IP地址和用户Agent(通常是浏览器的名称和版本号)。Header信息的完整列表取决于用户所发送的Header信息和服务器端设置的Header信息。
当试图访问一个不存在的键时,会触发一个KeyError异常。
这个字典中几个常见的键值有:
- HTTP_USER_AGENT,用户浏览器的user-agent字符串,如果有的话。 例如: “Mozilla/5.0 (X11; U; Linux i686; fr-FR; rv:1.8.1.17) Gecko/20080829 Firefox/2.0.0.17” .
- REMOTE_ADDR 客户端IP,如:”12.345.67.89″ 。(如果申请是经过代理服务器的话,那么它可能是以逗号分割的多个IP地址,如:”12.345.67.89,23.456.78.90″ 。)
Token
Token字面意思是令牌,功能跟Session类似,也是用于验证用户信息的,Token是服务端生成的一串字符串,当客户端发送登录请求时,服务器便会生成一个Token并将此Token返回给客户端,作为客户端进行请求的一个标识以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码。与session的不同之处在于,Session是将用户信息存储在服务器中保持用户的请求状态,而Token在服务器端不需要存储用户的登录记录,客户端每次向服务端发送请求的时候都会带上服务端发给的Token,服务端收到请求后去验证客户端请求里面带着Token,如果验证成功,就向客户端返回请求的数据。
功能实现
实现登录功能
整个基于Token的验证流程如下:
- 客户端使用用户名跟密码请求登录
- 服务器收到请求,去验证用户名和密码
- 验证成功后,服务端会签发一个Token,再把这个Token发送到客户端
- 客户端收到的Token以后可以把它存储起来,比如放在Cookie或LocalStorage里
- 客户端每次向服务器发送其他请求的时候都要带着服务器签发的Token
- 服务器收到请求,去验证客户端请求里面带着的Token,如果验证成功,就像客户端返回请求的数据
服务器端返回的结果以JSON格式发送。
角色管理和用户管理
角色管理控制着权限,不同角色拥有不同的权限。
在数据库中,做好角色管理的表,然后将值传递给前端。
用户管理表中添加这个用户属于什么角色,以及该角色拥有什么权限两个字段。
- 查询用户管理时,获取该角色权限,更新到用户管理角色权限字段上
- 查询用户管理,在vue生命周期上,将角色名称传递给用户管理,以便用户在编辑和新增时,选择角色时是数据库已有角色
- 编辑用户角色后,更新该用户管理角色权限字段值
- 有一个不可删的最小权限角色,删除角色后,如果该角色有用户,则将用户更新为最低角色
Django
官方文档:https://docs.djangoproject.com/en/3.1/
django models
官方地址:https://docs.djangoproject.com/zh-hans/3.1/ref/contrib/auth/
创建类
在应用中创建一个类,这个类名就是sqlite中新建的表名,例如在login应用下建立user类,则数据库中会增加login_user表。
在类中定义字段(field),有以下定义字段方式:
元数据
在创建类后需定义元数据,使用内部的class Meta。
ordering:排序选项
db_table:数据库表名
verbose_name、verbose_name_plural:单数和复数名称
还有其他可选选项
创建用户自定义模型后不生成新的用户表
即使执行了
python manage.py makemigrations
python manage.py migrate
也不生成新的用户表,还会出现
sqlite3.OperationalError: no such table
这就是用户自定义的表没有生成,然后用到了就报错了
解决方法:
这是因为django.mgrations里面存了记录,导致一直都不更换新的迁移语句,在这个表里删除有关诸如001,就是应用内migrations文件夹生成的0001_initial
如果删了出现:django.db.migrations.exceptions.InconsistentMigrationHistory: Migration admin.0001_initial is applied before its dependency signin.0001_initial on database ‘default’.
那就删掉除auth_user表外的其他所有表,然后重新执行
python manage.py makemigrations
python manage.py migrate
django form.py
form.py是Django用来生成form表单代码和验证表单数据是否合法的一个文件, 可以在该文件中创建Form类, 实现自定义表单的功能
这是在后端对用户名和密码进行验证,比如增加限定条件,账号必须全英文,密码不超过多长等这样的功能。
例如在form.py定义如下:
widgets是一个配置forms组件的参数配置(标签类型,属性等),可以为form渲染模板提供相应支持,可以在widget中指定标签的类型(默认TextInput)和属性(值) attrs。
attrs后面接的属性以及属性值为键值对形式
如:pwd = forms.CharField(max_length=32,label=”密码”,widget=forms.PasswordInput(attrs={“class”:”form-control”}))
就比如我们定义了PasswordInput标签,那么在前端表单上就会以’****’方式进行显示,而不是明文。
widget部件
处理 input 的部件
TextInput
NumberInput
EmailInput
URLInput
PasswordInput
HiddenInput
DateInput
DateTimeInput
TimeInput
Textarea
Selector 和 checkbox 部件
CheckboxInput
Select
NullBooleanSelect
RadioSelect
CheckboxSelectMultiple
File upload 部件
FileInput
ClearableFileInput
合成部件
MultipleHiddenInput
SplitDateTimeWidget
SplitHiddenDateTimeWidget
SelectDateWidget
django 登录验证 django.contrib.auth
官方文档:https://docs.djangoproject.com/zh-hans/3.1/topics/auth/default/
使用sqlite3存储用户名和密码
python3 manage.py makemigrations 解释命令,这个命令是记录我们对models.py的所有改动,并且将这个改动迁移到migrations这个文件下生成一个文件例如:0001文件,但是这个命令并没有作用到数据库
如果在新应用models.py中创建一个类,则会创建一个新表,名字为应用名.表名,也会生成相应的段,但是数据库中并不会更新,直到调用python3 manage.py migrate才会更新出表。例如,创建一个login应用,在其models.py创建user表,则会在数据库中生成login_user表
python3 manage.py migrate 初始化(迁移)命令,这条命令的主要作用就是把这些改动作用到数据库也就是执行migrations里面新改动的迁移文件更新数据库,比如创建数据表,或者增加字段属性。也即将makemigrations 的表和字段写入sqlite中。
这两个命令默认情况下是作用于全局,也就是对所有最新更改的models或者migrations下面的迁移文件进行对应的操作,如果要想仅仅对部分app进行作用的话 则执行如下命令:
也可以特定增加某个应用的表。
python3 manage.py makemigrations appname
python3 manage.py migrate appname
直接操作sqlite3
from django.db import connection
cursor = connection.cursor()
cursor.execute(upd_token)
connection.commit()
使用QuerySet API操作数据库
官方文档:https://docs.djangoproject.com/zh-hans/3.1/ref/models/querysets/
查询数据库字段值
在models定义一个User,然后使用
models.User.objects.values_list('id','username')
就能查出对应数据,然后再使用for循环取出元组中的数据
where查询
用filter查询,如
models.User.objects.filter(userrole=rolename)
就能查询出,userrole字段等于rolename的用户名集合。
更新或创建
update_or_create是如果存在就更新,否则就创建,会返回obj和created,created是bool值,标志值是否创建
使用where进行update
models.User.objects.filter(id='1').update(userpermis=rolepermis)
这个意思就是,更新userpermis=rolepermis,然后where id=1的字段
解决跨域问题
from django.views.decorators.csrf import csrf_exempt @csrf_exempt
也可以在setting上写上:
'corsheaders.middleware.CorsMiddleware', # 新增跨域 'django.middleware.common.CommonMiddleware',# 新增跨域 'django.middleware.csrf.CsrfViewMiddleware',# 新增跨域 # 处理跨域 CORS_ALLOW_CREDENTIALS = True # 指明在跨域访问中,后端是否支持对cookie的操作。 # 跨域增加忽略 CORS_ORIGIN_ALLOW_ALL = True # 允许所有跨域请求 # 跨域允许的操作 CORS_ALLOW_METHODS = ( 'DELETE', 'GET', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'VIEW', ) # 跨域允许的请求头 CORS_ALLOW_HEADERS = ( 'XMLHttpRequest', 'X_FILENAME', 'accept-encoding', 'authorization', 'content-type', 'dnt', 'origin', 'user-agent', 'x-csrftoken', 'x-requested-with', 'Pragma', )
解决is_valid一直为False问题
由于在forms.py中定义的形参和前端传过来的形参不一致,比如后台定义username,但是前端就是userName,这就导致了is_valid一直不成功。
Vue
框架
使用的框架是iview-admin-master,地址为https://github.com/iview/iview-admin,演示地址为:https://admin.iviewui.com/home
目录结构
vue.config.js:vue配置文件,修改代理等
src/libs/api.request.js:
src/config/index.js:配置文件,修改baseUrl,如果设置了代理,dev和pro改成‘/’
/src/router/router.js:前台访问地址
数据流
登录数据流:
/src/components/login-form/login-form.vue:登录表单 —>
/src/view/login/login.vue:登录视图和函数 —>
/src/store/module/user.js:登录函数,这这里处理请求 —>
/src/api/user.js:axios函数,发送请求
vue生命周期
每个 Vue 实例在被创建时都要经过一系列的初始化过程——例如,需要设置数据监听、编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,这给了用户在不同阶段添加自己的代码的机会。
比如 created 钩子可以用来在一个实例被创建之后执行代码:
new Vue({
data: {
a: 1
},
created: function () {
// `this` 指向 vm 实例
console.log('a is: ' + this.a)
}
})
// => "a is: 1"
也有一些其它的钩子,在实例生命周期的不同阶段被调用,如 mounted、updated 和 destroyed。生命周期钩子的 this 上下文指向调用它的 Vue 实例。
生命周期图示
生命周期钩子
所有的生命周期钩子自动绑定 this 上下文到实例中,因此你可以访问数据,对 property 和方法进行运算。这意味着你不能使用箭头函数来定义一个生命周期方法 (例如 created: () => this.fetchTodos())。这是因为箭头函数绑定了父上下文,因此 this 与你期待的 Vue 实例不同,this.fetchTodos 的行为未定义。
beforeCreate
- 类型:
Function
- 详细:在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。
- 参考:生命周期图示
created
- 类型:
Function
- 详细:在实例创建完成后被立即调用。在这一步,实例已完成以下的配置:数据观测 (data observer),property 和方法的运算,watch/event 事件回调。然而,挂载阶段还没开始,
$el
property 目前尚不可用。 - 参考:生命周期图示
beforeMount
- 类型:
Function
- 详细:在挂载开始之前被调用:相关的
render
函数首次被调用。该钩子在服务器端渲染期间不被调用。 - 参考:生命周期图示
mounted
- 类型:
Function
- 详细:实例被挂载后调用,这时
el
被新创建的vm.$el
替换了。如果根实例挂载到了一个文档内的元素上,当mounted
被调用时vm.$el
也在文档内。注意mounted
不会保证所有的子组件也都一起被挂载。如果你希望等到整个视图都渲染完毕,可以在mounted
内部使用 vm.$nextTick:mounted: function () { this.$nextTick(function () { // Code that will run only after the // entire view has been rendered }) }
该钩子在服务器端渲染期间不被调用。
- 参考:生命周期图示
beforeUpdate
- 类型:
Function
- 详细:数据更新时调用,发生在虚拟 DOM 打补丁之前。这里适合在更新之前访问现有的 DOM,比如手动移除已添加的事件监听器。该钩子在服务器端渲染期间不被调用,因为只有初次渲染会在服务端进行。
- 参考:生命周期图示
updated
- 类型:
Function
- 详细:由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。当这个钩子被调用时,组件 DOM 已经更新,所以你现在可以执行依赖于 DOM 的操作。然而在大多数情况下,你应该避免在此期间更改状态。如果要相应状态改变,通常最好使用计算属性或 watcher 取而代之。注意
updated
不会保证所有的子组件也都一起被重绘。如果你希望等到整个视图都重绘完毕,可以在updated
里使用 vm.$nextTick:updated: function () { this.$nextTick(function () { // Code that will run only after the // entire view has been re-rendered }) }
该钩子在服务器端渲染期间不被调用。
- 参考:生命周期图示
activated
- 类型:
Function
- 详细:被 keep-alive 缓存的组件激活时调用。该钩子在服务器端渲染期间不被调用。
- 参考:
deactivated
- 类型:
Function
- 详细:被 keep-alive 缓存的组件停用时调用。该钩子在服务器端渲染期间不被调用。
- 参考:
beforeDestroy
- 类型:
Function
- 详细:实例销毁之前调用。在这一步,实例仍然完全可用。该钩子在服务器端渲染期间不被调用。
- 参考:生命周期图示
destroyed
- 类型:
Function
- 详细:实例销毁后调用。该钩子被调用后,对应 Vue 实例的所有指令都被解绑,所有的事件监听器被移除,所有的子实例也都被销毁。该钩子在服务器端渲染期间不被调用。
- 参考:生命周期图示
errorCaptured
2.5.0+ 新增
- 类型:
(err: Error, vm: Component, info: string) => ?boolean
- 详细:当捕获一个来自子孙组件的错误时被调用。此钩子会收到三个参数:错误对象、发生错误的组件实例以及一个包含错误来源信息的字符串。此钩子可以返回
false
以阻止该错误继续向上传播。你可以在此钩子中修改组件的状态。因此在捕获错误时,在模板或渲染函数中有一个条件判断来绕过其它内容就很重要;不然该组件可能会进入一个无限的渲染循环。
错误传播规则
- 默认情况下,如果全局的
config.errorHandler
被定义,所有的错误仍会发送它,因此这些错误仍然会向单一的分析服务的地方进行汇报。 - 如果一个组件的继承或父级从属链路中存在多个
errorCaptured
钩子,则它们将会被相同的错误逐个唤起。 - 如果此
errorCaptured
钩子自身抛出了一个错误,则这个新错误和原本被捕获的错误都会发送给全局的config.errorHandler
。 - 一个
errorCaptured
钩子能够返回false
以阻止错误继续向上传播。本质上是说“这个错误已经被搞定了且应该被忽略”。它会阻止其它任何会被这个错误唤起的errorCaptured
钩子和全局的config.errorHandler
。
- 默认情况下,如果全局的
获取用户信息和角色信息
我在生命周期的mounted阶段,使用查询用户信息或角色信息的函数,等到执行到mounted阶段,就会自定执行函数并渲染,这样就保证了,只要路由到用户管理或者角色管理块,就能立刻查询出信息。
模态框(Modal)
Vue.js 模态框(Modal)是一个易用、高度可定制化的Vue.js模态框组件库,支持SSR。
安装
npm i vue-js-modal
yarn add vue-js-modal
使用
Client中使用
在 main.js文件中引入
import VModal from 'vue-js-modal'
或者
import VModal from 'vue-js-modal/dist/index.nocss.js'
import 'vue-js-modal/dist/styles.css'
注册
Vue.use(VModal)
SSR中使用
文件 nuxt.config.js:
export default {
...
/*
** Plugins to load before mounting the App
*/
plugins: [
'~plugins/vue-js-modal.js'
],
}
文件plugins/vue-js-modal.js
import Vue from 'vue'
import VModal from 'vue-js-modal/dist/ssr.nocss'
import 'vue-js-modal/dist/styles.css'
Vue.use(VModal, { ... })
/*
export default function(_, inject) {
inject('modal', VModal)
}
*/
Promise:resolve、reject和catch
Promise其实是一个构造函数,它有resolve,reject,race等静态方法;它的原型(prototype)上有then,catch方法,因此只要作为Promise的实例,都可以共享并调用Promise.prototype上面的方法(then,catch)
Promise有三种状态
- pending: 初始状态,成功或失败状态。
- fulfilled: 意味着操作成功完成。
- rejected: 意味着操作失败。
参数resolve和reject的作用是将Promise中函数要传递的值,作为参数传给后面的then和catch中函数。
resolve(值1)把值1传给promise,然后再由promise把值1传给then(function(值1)),then就是回调函数。
var p = new Promise(function (resolve, reject) { var timer = setTimeout(function () { console.log('执行操作1'); resolve('这是数据1'); }, 1000); }); p.then(function (data) { console.log(data); console.log('这是成功操作'); });
控制台输出:
执行操作1 这是数据1 这是成功的操作
reject(值2)把值2给promise,然后再由promise把值2传给catch(function(值2))。
var p = new Promise(function (resolve, reject) { var flag = false; if(flag){ resolve('这是数据2'); }else{ reject('这是数据2'); } }); p.then(function(data){//状态为fulfilled时执行 console.log(data); console.log('这是成功操作'); },function(reason){ //状态为rejected时执行 console.log(reason); console.log('这是失败的操作'); });
控制台输出:
这是数据2 这是失败的操作
catch在Promise状态为rejected时执行,then方法捕捉到Promise的状态为rejected,就执行catch方法里面的操作
var p = new Promise(function (resolve, reject) { var flag = false; if(flag){ resolve('这是数据2'); }else{ reject('这是数据2'); } }); p.then(function(data){ console.log(data); console.log('这是成功操作'); }).catch(function(reason){ console.log(reason); console.log('这是失败的操作'); });
控制台输出:
这是数据2 这是失败的操作
scope.$index, scope.row
scope.$index→拿到每一行的index
scope.$row→拿到每一行的数据
我遇到情况是,必须有顺序的:scope.row, scope.$index
否则只能获取到第一个row的值,无论第一个写row还是index。
解决跨域问题-使用代理
解决前端POST给服务器值为空
前端使用axios POST给服务器的值是如下所示,但是服务器无法获取到request.post的值。
export const login = ({ userName, password }) => { const data = { userName, password } return axios.request({ url: 'signin', method: 'post', data }) }
通常前端通过POST请求向服务器端提交数据格式有4中,分别是
“application/x-www-form-urlencoded”格式
” multipart/form-data”格式
.”application/json”格式
“text/xml”格式。
而django搭建的后台需要Form格式的数据才能解析,所以需要对前端的数据进行修改
引入qs把object转化为string形式,修改后的代码如下。
import qs from 'qs' export const login = ({ userName, password }) => { const data = qs.stringify({ userName, password }) console.log('data:', typeof data) return axios.request({ url: 'signin', method: 'post', data }) }
取消数据流总进入mock
在/src/main.js 注释掉mock请求
解决权限管理问题
权限验证数据流
在登录时,在/router/index.js中进行路由判定,用户已经登录,则会使用getUserInfo获取用户信息,然后转到turnTo函数
turnto会调用/libs/util/canTurnTo函数,canTurnTo这里要做权限验证,调用hasAccess函数,hasAccess函数会调用/libs/tools/hasOneOf来比较用户权限和模块权限,hasOneOf的targetarr必须是数组,arr可以是字符串,最好两个都是数组。
权限设置方案
iview admin本身已经做了权限管理,只要在/src/router/router.js的meta中写上access: […]即可。比如在home上写入access: [superadmin],这样就只有用户名为superadmin的用户可以使用home这一功能区。但是这样并不能和角色联系起来,难道以后我有几百个用户,在每个路由上都编辑access来决定哪个用户可以使用该功能区?
iview admin的权限认证原理是:
后端传递来数组如[a,b,c],前端access中如果包含其中一个则可以访问该功能区。例如后端传递过来[a,b,c],前端access中定义[a],则本次可以使用该路由
那么,最后我的方案是,把每个路由(功能区)的access都定义为它功能区本身的名字,比如,我这个工具是系统管理,那我就定义一个access: [system_manager]
后端会判断这个用户的角色,以及这个用户角色的权限,权限就是[home,system_manager],这样,这个角色就是可以访问home和系统管理两个工功能区。
子路由设置权限
后台传递过来的权限是字符串,先要把字符串修改为数组,这里是在获取用户module/user.js 的setAccess将字符串修改为数组
iview-admin 子路由在routers.js中设置access,然后要修改index.js,在下面这里将user.access 修改为store.state.user.access,不然会获取是后台传递过来的字符串格式,不是数组。
解决怎么把后端值传递给selector选择器
<Select v-model="editform.role"> <Option v-for="item in rolelist" :value="item.rolename" :key="item.rolename" :label="item.rolename"></Option> </Select>
上面是一个选择器,用v-for循环不断获得rolelist里值,rolerist是列表,下面这样。然后value,label就能获得rolename的值,实际上是数组里面有字典。
后端传递给前端一个数组,数组里面是字典。这样直接将后端数组赋值给rolelist后,就能自动渲染,然后选择器就是后端的数据库值了。
解决checkbox的使用
checkbox是勾选框,使用CheckboxGroup来多个定义。label是值,也就是如果能获得checkbox勾选值,最后获得的就是label的值
然后这样就定义了,让home默认勾选,然后上面的disabled就是不可编辑。
然后,我只要获取上面的addform.rolepermis就可以直接获取勾选值。是一个数组。