本项目将在GitHub上维护更新。
https://github.com/dangjingtao/FeRemarks
选择合适的ui库
本项目使用vue全家桶,axios和cube-ui cube-ui文档地址:https://didi.github.io/cube-ui/#/zh-CN/docs/quick-start
代码语言:javascript复制// 插件式安装
vue add cube-ui
相关模板说明地址https://github.com/cube-ui/cube-template/wiki
在我的电脑里,按官网的思路装怎么都不行。那可以基于这个脚手架进行: https://github.com/cube-ui/cube-template 令牌思路
- 传统时后端存放session,对于spa应用来说,并不适合
- 令牌,有效期。前端会携带令牌(存在session),令牌有有效期。后端根据非对称加密
前端登录(login.vue)
新建三个页面:about,home和login 在登录about时,给一个meta,提示需要做校验:
代码语言:javascript复制{
path:'about',
meta:{auth:true}
}
从本地拿token,如果拿不到,不显示登录成功态。
代码语言:javascript复制router.beforeEach((to,from,next)=>{
if(to.meta.auth){
//只要本地有token就认为登录了
const token=sessionStorage.getItem('token');
if(token){
next();
}else{
next({
path:'/login',
query:{
redirect:to.path
}
})
}
}else{
next()
}
})
这个功能就实现了。
登录态的保存
这是一种比较low的方式。最好放在vuex里。 在vuex中还要设置module
代码语言:javascript复制// 用户登录 user.js
export default {
state:{
isLogin:!!localStorage.getItem('token')
},
mutations:{
setLogin(state,val){
state.isLogin=val
}
},
actions:{
login(){
}
},
}
接下来就是写一套登录界面:
代码语言:javascript复制<template>
<div style="padding-top:200px;">
<h2 style="font-size:30px;line-">Login</h2>
<cube-form
:model="model"
:schema="schema"
:immediate-validate="false"
@validate="validateHandler"
@submit="submitHandler"></cube-form>
</div>
</template>
<script>
export default {
data() {
return {
validity: {},
valid: undefined,
model: {
username:'',
password:''
},
schema: {
groups: [
{
fields: [
{
type: 'input',
modelKey: 'username',
label: 'username',
props: {
placeholder: 'Please input username'
},
rules: {
required: true
},
trigger: 'blur'
},
{
type: 'input',
modelKey: 'password',
label: 'password',
props: {
type:'password',
placeholder: 'Please input your password'
},
rules: {
required: true
},
trigger: 'blur'
},
{
type: 'submit',
label: 'Submit'
},
]
}]},
options: {
scrollToInvalidField: true,
layout: 'standard' // classic fresh
}
}
},
methods: {
submitHandler(e) {
e.preventDefault()
console.log('submit', e)
},
validateHandler(result) {
this.validity = result.validity
this.valid = result.valid
console.log('validity', result.validity, result.valid, result.dirty, result.firstInvalidFieldIndex)
},
}
}
</script>
<style scoped></style>
cube-ui采用纯数据驱动的ui。很像虚拟dom树。此时的界面是:
再处理一下登录逻辑:
代码语言:javascript复制submitHandler(e) {
e.preventDefault()
let params={
username:this.model.username,
password:this.model.password
}
console.log(params)
this.$store.dispatch('login',params).then(sucess=>{
if(success){
const path=this.$route.query.redirect||'/';
this.$router.push(path)
}
}).catch(err=>{
const toast=this.$createToast({
time:2000,
txt:'login failed',
type:'error'
}).show
});
}
接下来是调用store内的action,根据函数式编程的处理思路,action只作为类似控制器,最后只返回异步结果,不负责具体的数据处理。因此可在src根目录中写一个service文件夹,加一个user.js:另写一个:
代码语言:javascript复制// service/user.js
import axios from 'axios';
const path='http://localhost:3000'
export default {
login(user){
return axios.post(path '/api/login',user).then(({data})=>{
return data;
});
}
}
然后再loginAction中执行这个接口
代码语言:javascript复制// ./store/user.js
actions:{
login(ctx,params){
return us.login(params).then(({token})=>{
//只关心是否拿到token
if(token){
commit('setLogin',true);
sessionStorage.setItem('token',token);
return true;
}
return false;
});
}
nodejs服务搭建(koa2)
这时候请求接口,肯定是404的。还记得vue.config.js
吗?可以实现。
但现在是写一个真正的服务器(基于koa2)。
新建一个文件夹be
,
koa2系列并不是一套完整的脚手架。需要router接收请求,bodyparser来获取post参数。
npm init
完成后,执行安装:
npm i koa koa-router koa-bodyparser
然后新建一个index.js
代码语言:javascript复制const koa=require('koa');
const Route=require('koa-router')
const bodyparser=require('koa-bodyparser')
const router=new Route();
// 接口
router.post('/api/login',async (ctx,next)=>{
ctx.response.body='aaa'
})
const app=new koa();
app.use(cors())
app.use(router.routes())
app.use(bodyparser)
app.listen('3000',()=>{
console.log('server is runing at http://localhost:3000')
})
node app.js,即可启动。
vue-cli和koa2开发环境跨域配置
现在的前后端联调是跨域的。
在koa2中引入中间件:
代码语言:javascript复制npm install koa-cors
代码语言:javascript复制var cors = require('koa2-cors');
app.use(cors())
即可实现本地跨域。
接口实现
如果是get请求,你可以用ctx.query
拿到。
如果是post请求,ctx.request.query
可以拿到请求。
假设用户名和密码叫做djtao
和123
,发回token叫做iamtoken
router.post('/api/login',async (ctx,next)=>{
console.log(ctx)
const {username,password}=ctx.request.body;
if(username=='djtao'&&password=='123'){
ctx.response.body={code:1,token:'iamtoken'}
}else{
ctx.response.body={
code:1,
message:'check your username or password'
}
}
})
请求拦截器
有了token之后,每次http请求发出,都要加载header上。这样就不用每次都带token字段去请求东西了。拿到token,就放到请求头中。
在src根目录下创建一个interceptor.js
import axios from 'axios';
export default function(){
axios.interceptors.request.use(config=>{
const token=sessionStorage.getItem('token');
if(token){
config.headers.token=token
}
return config;
})
}
在main中执行这个函数:
代码语言:javascript复制import interceptor from './interceptor'
interceptor();
为了验证一下,写一个请求用户信息的接口,这个接口需要返回
代码语言:javascript复制//service/user.js
getUserInfo(){
return axios.get(path '/api/userinfo').then(res=>{
return res;
})
}
// store/user.js
getUserInfo(ctx){
return us.getUserInfo().then(res=>{
return res
})
}
在home.vue页面请求调用这个接口:
代码语言:javascript复制<template>
<div>
this is Home page
<div v-if="$store.state.user.isLogin">
welcome back <span>{{this.user}}</span>
</div>
<div v-else>
unlisted.
</div>
</div>
</template>
<script>
export default {
data() {
return {
user: ''
}
},
created () {
this.$store.dispatch('getUserInfo').then(res=>{
if(res.data.code==1){
this.user=res.data.data.name
}
});
},
}
</script>
在后端:
代码语言:javascript复制// be/app.js
var auth=function(ctx,next){
console.log(ctx)
if(ctx.header.token){
next();
}else{
// 直接返回401
ctx.status = 401
}
}
// 无法通过auth中间件认证,这个接口将不会得到预期结果
router.get('/api/userinfo',auth,async (ctx,next)=>{
ctx.response.body={code:1,data:{name:'dangjingtao'}}
})
这里自己写了一个了auth
中间件。它是一个函数,接收两个参数,分别对应ctx
和next
。
你可以从ctx上下文中拿到请求相关的数据。
现在请求头都带token字段了:
注销
目前login页面还是存在的,现在要把它去掉了:如果登录后,将返回注销。 注销做两件事:
- 把session清空;
- 把isLogin变为false 在app.vue里派发一个'logout'
logout() {
this.$store.dispatch('logout')
}
然后在store/user.js中写:
代码语言:javascript复制 logout(ctx){
sessionStorage.removeItem('token');
ctx.commit('setLogin',false)
}
在写一个注销方法:
代码语言:javascript复制logout(ctx){
sessionStorage.removeItem('token');
ctx.commit('setLogin',false)
}
主动过期
响应拦截
对于401错误,目前拦截器没有处理。 现在在interceptor中设置:
代码语言:javascript复制// 设置401的响应操作
axios.interceptors.response.use((res)=>{
return res
},err=>{
if(err.response.status=401){
//清空缓存
vm.$store.dispatch('logout');
vm.$router.push('/login')
}
return Promise.reject(err)
})
在main.js中,把app实例拿出来::
代码语言:javascript复制const app=new Vue({
el: '#app',
router,
store,
template: '<App/>',
components: { App }
})
interceptor(app);
令牌机制
bearer token规范
服务器不关心令牌的所有者是谁。如果token被盗走了。就没法识别。以上实现本质上是bearer token规范。
代码语言:javascript复制Authorization:`Bearer` 你的token
防止窃取token: 别乱点,有效期要短一点:
json web token规范(jwt规范)
JWT 是 JSON Web Token 的简写,它定义了一种在客户端和服务器端安全传输数据的规范。通过 JSON 格式 来传递信息。 基本格式:
代码语言:javascript复制头.载荷.签名
- 头部:加密类型,令牌类型
- 载荷:用户信息,签发事件和过期时间(base64编码,不加密)
- 签名:由前二者和服务器独有的密钥得到的哈希串:Hmac Sha1 256 签名是前端无法获取的,
实现jwt:主要在后端操作:安装对应依赖后:
代码语言:javascript复制const koa=require('koa');
const Route=require('koa-router')
var cors = require('koa2-cors');
var bodyParser = require('koa-bodyparser');
const jwt=require('jsonwebtoken');
const jwtAuth=require('koa-jwt');
const valid=require('jwt-simple')
const secret ='talk is cheap,show me the code';
const router=new Route();
router.post('/api/login',async (ctx,next)=>{
// console.log(ctx)
const {username,password}=ctx.request.body;
// console.log(ctx.request.body)
if(username=='djtao'&&password=='123'){
let content ={name:'dangjingtao'}
const token = jwt.sign(
{
data: { name: "dangjingtao" }, // 用户信息数据
exp: Math.floor(Date.now() / 1000) 60 * 60 // 过期时间
},
secret
);
ctx.response.body={code:1,token}
}else{
ctx.response.body={
code:1,
message:'check your username or password'
}
}
});
let auth=function(ctx,next){
let token = ctx.header.token
try {
valid.decode(token, secret);
next()
} catch (error) {
ctx.status = 401;
ctx.response.body={
code:1,
message:'check your username or password'
}
}
}
// 无法通过auth中间件认证,这个接口将不会得到预期结果
router.get('/api/userinfo',auth,async (ctx,next)=>{
ctx.response.body={code:1,data:{name:'dangjingtao'}}
})
// router.get
const app=new koa();
app.use(cors())
app.use(bodyParser());
app.use(router.routes())
app.listen('3000',()=>{
console.log('server is runing at http://localhost:3000')
})
由此,功能基本完成。