前后端权限机制

2019-07-18 17:08:47 浏览数 (1)

本项目将在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完成后,执行安装:

代码语言:javascript复制
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可以拿到请求。 假设用户名和密码叫做djtao123,发回token叫做iamtoken

代码语言:javascript复制
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

代码语言:javascript复制
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中间件。它是一个函数,接收两个参数,分别对应ctxnext。 你可以从ctx上下文中拿到请求相关的数据。 现在请求头都带token字段了:

注销

目前login页面还是存在的,现在要把它去掉了:如果登录后,将返回注销。 注销做两件事:

  • 把session清空;
  • 把isLogin变为false 在app.vue里派发一个'logout'
代码语言:javascript复制
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')
})

由此,功能基本完成。

0 人点赞