vuejs 实现前后端分离登录验证和页面自动跳转

vuejs实现前后端分离登录验证和页面自动跳转

最近一直在学习vuejs的全家桶,为了能达到熟练运用,刚好参与的开源项目,就实现了一部分。

使用的技术点:

  1. vue-router
  2. axios
  3. vuex
  4. element-ui
  5. qs

项目介绍:
这个项目是一个类似google相册功能的项目,目前实现的是图片特征提取,可以以图搜图,最终打造成一个智能相册。后台由go语言开发,图片特征提取由c++开发,前端使用vuejs实现完全前后端分离。

现在就开始我们的前段代码吧。这里我们都使用vue组件开发

实现登录

组件代码如下:

<template>
  <div id="bg" class="bg">
    <div class="login" @keyup.13="doLogin">
      <div class="form-horizontal login">
        <div class="logo">My-Albums</div>
        <div class="form-group input-group input-group-lg ">
          <span class="input-group-addon"><i class="fa fa-user-o" aria-hidden="true"></i></span>
          <input type="text" class=" form-control" placeholder="username" v-model="userInfo.userName">
        </div>
        <div class="form-group input-group input-group-lg">
          <span class="input-group-addon"><i class="fa fa-key" aria-hidden="true"></i></span>
          <input type="password" class=" form-control" placeholder="password" v-model="userInfo.password">
        </div>
        <div class="form-group">
          <el-button class="form-control" @click="doLogin">登 录</el-button>
          <!--<button class="btn btn-default btn-sm form-control login-btn" @click="doLogin">登 录</button>-->
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import axios from 'axios';
export default {
  name: 'login',
  data () {
    return {
      userInfo :{
          userName : '',
          password : '',
      },
      show : false,
    }
  },
  methods : {
      doLogin (){
          if (this.userName == ''){
              alert('用户名不能为空');
              return false
          }
          if (this.password == ''){
              alert('密码名不能为空');
              return false
          }
          axios.post('/login',JSON.stringify(this.userInfo))
              .then(res => {
                  console.log(res)
                  if(res.status == 200){
                      this.$store.commit('setToken',res.data);
                      localStorage.userName = this.userInfo.userName;
                      localStorage.token_expire = res.data.expire;
                      localStorage.token = res.data.token;
                      this.$notify({
                          title : '提示信息',
                          message : '登录成功',
                          type : 'success'
                      });
                      this.$router.push({path:'/'})
                  }else {
                      this.$notify({
                          title : '提示信息',
                          message : '账号或密码错误',
                          type : 'error'
                      });
                  }
              })
              .catch(err => {
                  console.log(err)
              })
      }
  },
  mounted (){
      var wi=window.screen.width;
      var hi=window.screen.height;
      document.getElementById("bg").style.width=wi+"px";
      document.getElementById("bg").style.height=hi+"px";
  },
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
  /*.bg {*/
    /*!*background-color: aqua;*!*/
    /*background-image: url("../assets/bj.jpg");*/
    /*background-size:100% 100%*/
  /*}*/
  .login {
    position:absolute;
    top: 50%;
    left: 50%;
    -webkit-transform: translate(-50%, -50%);
    -moz-transform: translate(-50%, -50%);
    -ms-transform: translate(-50%, -50%);
    -o-transform: translate(-50%, -50%);
    transform: translate(-50%, -50%);
    width: 400px;

  }
  .login-btn {
    background-color: whitesmoke;
  }
  .logo {
    font-family: "DejaVu Sans Mono";
    color: lightblue;
    font-size: 50px;
  }
</style>

这里先不对代码进行说明,我先把index的组件代码也一并贴上,稍后解析知识要点。

实现首页展示相册

代码如下:


<template>
    <div id="index" class="row">
        <nav class="navbar navbar-inverse navbar-fixed-top">
            <div class="container-fluid">
                <div class="navbar-header">
                    <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
                        <span class="sr-only">Toggle navigation</span>
                        <span class="icon-bar"></span>
                        <span class="icon-bar"></span>
                        <span class="icon-bar"></span>
                    </button>
                    <a class="navbar-brand" href="#">{{title}}</a>
                </div>
                <div id="navbar" class="navbar-collapse collapse">
                    <ul class="nav navbar-nav navbar-right">
                        <li><a href="#">退出</a></li>
                    </ul>
                    <form class="navbar-form navbar-right">
                        <input type="text" class="form-control" placeholder="Search...">
                    </form>
                </div>
            </div>
        </nav>
        <div class="container-fluid">
            <div class="row">
                <div class="col-sm-3 col-md-2 sidebar">
                    <ul class="nav nav-sidebar">
                        <li class="active"><a href="#"><i class="fa fa-folder" aria-hidden="true"></i> 相册列表  <span class="sr-only">(current)</span></a></li>
                        <li>
                            <a href="#" v-for="list in albumsName" @click="listImages(list)"><i class="fa fa-picture-o" aria-hidden="true"></i> {{list}} <span class="btn btn-default btn-sm"><i class="fa fa-trash-o" aria-hidden="true"></i> 删除</span></a>
                        </li>
                    </ul>
                </div>
                <div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
                    <create-album :show.sync="Create"></create-album>
                    <el-button class="my-create"  type="success" @click="showCreate">创建相册</el-button>

                    <upload-image :show.sync="show"></upload-image>
                    <el-button class="my-upload" type="button" @click="uploadImage">上传照片到此相册</el-button>
                    <h1 class="page-header"></h1>

                    <div class="row placeholders">
                        <div class="col-xs-6 col-sm-3 placeholder" v-for="image in images">
                            <img :src="image.url" width="200" height="200" class="img-responsive" alt="Generic placeholder thumbnail">
                            <h4>{{image.album}} </h4>
                            <span class="text-muted">{{image.filename}}</span>
                            <a  :href="image.url">下载</a>
                            <el-button size="small" type="danger" @click="deleteImage(image.url,image.album)">删除</el-button>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>

</template>
<script>
import axios from 'axios';
import qs from 'qs';
import UploadImage from './UploadImage.vue';
import CreateAlbum from './CreateAlbum.vue';
export default {
    name : 'index',
    data () {
        return {
            Create : false,
            show : false,
            title : this.$store.state.title
        }
    },
    components :{UploadImage,CreateAlbum},
    computed : {
        // 获取相册列表
        albumsName (){
            return this.$store.getters.getAlbumsName;
        },
        // 获取相册内的图片列表
        images (){
            return this.$store.getters.getImagesList;
        }
    },
    mounted (){
        axios.post('/auth/managealbum/get', JSON.stringify({username : localStorage.userName})).then(res => {
                if (res.data.status == 0){
                    this.$store.commit('setAlbums',res.data.data)
                }else {
                    this.$store.commit('setAlbums','您没有相册')
                }
            }).catch(err => {

            })
    },
    methods : {
        // 打开上传弹窗
        uploadImage (){
            this.show = true;
        },
        showCreate(){
          this.Create = true;
        },
        // 获取选中的相册的图片列表
        listImages (name){
            this.$store.commit('setCurrentAlbum',name);
            axios.post('/auth/download?page=1&size=10',qs.stringify({username :localStorage.userName,album:name})).then(res => {
                if (res.data.status == 0){
                    if (res.data.data == null){
                        this.$notify({
                            title : '提示信息',
                            message : ' 相册是空的哦,上传一些照片吧',
                            type : 'error'
                        })
                    }else {
                        this.$store.commit('putImages',res.data.data);
                    }
                }else {
                    this.$msgbox({
                        title : '提示信息',
                        message : '没有此相册',
                    })
                }
            }).catch(err => {

            })
        },
        // 删除图片
        deleteImage (image,album){
            console.log(image);
            axios.post('/auth/delete',qs.stringify({username : localStorage.userName,album : album,md5:image.split('/')[4]})).then(res => {
                if (res.data.status == 0){
                    this.$notify({
                        title : '提示信息',
                        message : '删除成功',
                        type : 'success'
                    });
                    this.listImages(album);
                }else {
                    this.$notify({
                        title : '提示信息',
                        message : '删除失败',
                        type : 'error'
                    })
                }
            }).catch(err => {

            })
        }
    }
}
</script>
<style>
    /* Move down content because we have a fixed navbar that is 50px tall */
    body {
        padding-top: 50px;
    }
    /*
     * Global add-ons
     */

    .sub-header {
        padding-bottom: 10px;
        border-bottom: 1px solid #eee;
    }
    /*
     * Top navigation
     * Hide default border to remove 1px line.
     */
    .navbar-fixed-top {
        border: 0;
    }
    /*
     * Sidebar
     */
    /* Hide for mobile, show later */
    .sidebar {
        text-align: left;
        display: none;
    }
    @media (min-width: 768px) {
        .sidebar {
            position: fixed;
            top: 51px;
            bottom: 0;
            left: 0;
            z-index: 1000;
            display: block;
            padding: 20px;
            overflow-x: hidden;
            overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
            background-color: #f5f5f5;
            border-right: 1px solid #eee;
        }
    }
    .my-create {
        float: left;
    }
    /* Sidebar navigation */
    .nav-sidebar {
        margin-right: -21px; /* 20px padding + 1px border */
        margin-bottom: 20px;
        margin-left: -20px;
    }
    .nav-sidebar > li > a {
        padding-right: 20px;
        padding-left: 20px;
    }
    .nav-sidebar > .active > a,
    .nav-sidebar > .active > a:hover,
    .nav-sidebar > .active > a:focus {
        color: #fff;
        background-color: #428bca;
    }
    /*
     * Main content
     */
    .main {
        padding: 20px;
        margin-top: -65px;
    }
    .main > span {
    }
    @media (min-width: 768px) {
        .main {
            padding-right: 40px;
            padding-left: 40px;
        }
    }
    .main .page-header {
        margin-top: 0;
    }
    /*
     * Placeholder dashboard ideas
     */
    .placeholders {
        margin-bottom: 30px;
        text-align: center;
    }
    .placeholders h4 {
        margin-bottom: 0;
    }
    .placeholder {
        margin-bottom: 20px;
    }
    .placeholder img {
        display: inline-block;
        border-radius: 10%;
    }
</style>

ok, 到这里。我们两个页面就完成了,中间使用了一些element-ui的组件,这个可以在element-ui官网查看如何使用,由于此项目还未完成,所以代码还没有整理,功能也没有完善。有想参与这个开源项目的可以加我QQ 343125118.下面我们就对我们的知识点进行解析。

登录验证

因为是完全的前后端分离项目,所以这里我们使用了jwt做认证,这样我们前端就需要发送账号密码到后端进行验证,验证通过后会返回给你一个token和一个token的过期时间,然后我们把token和token的过期时间存储在localStorage里面,方便后面请求需要登录的资源时使用,至于为什么存储在localStorage里面,自己可以百度原因。ok,我们来看看代码吧。

// 这里我们使用了axios 去发送一个post请求,
axios.post('/login',JSON.stringify(this.userInfo))
              .then(res => {
                  console.log(res)
                  if(res.status == 200){
                  // 当后台返回代码里面显示登录成功之后我们将token进行保存
                      this.$store.commit('setToken',res.data);
                      localStorage.userName = this.userInfo.userName;
                      localStorage.token_expire = res.data.expire;
                      localStorage.token = res.data.token;
                      this.$notify({
                          title : '提示信息',
                          message : '登录成功',
                          type : 'success'
                      });
                     // 同事跳转到首页,这里使用vue-router实现
                      this.$router.push({path:'/'})
                  }else {
                  // 登录失败提示
                      this.$notify({
                          title : '提示信息',
                          message : '账号或密码错误',
                          type : 'error'
                      });
                  }
              })
              .catch(err => {
                  console.log(err)
              })
      }

细心的同学可能已经注意到上面我post请求的地址了,不是一个完整的api请求地址,这是怎么回事呢,这里我们是使用了axios的全局配置,代码在main.js里面,代码如下

axios.defaults.baseURL = 'http://xxxxxx.xxx:9990';

这里刚还说到了另外一个,就是实现登录后的自动跳转,当然,如果你没有登录是不能访问相册主页的,也就是index,所以这里我们来看一个vue-router的知识点,配置需要登录后才能访问的路由和全局路由钩子,由这两个东西就可以实现我们的登录自动跳转和验证需要登录的页面。

我们先看路由配置:

export default new Router({
  mode: 'history',
  routes: [
    {
      path: '/login',
      name: 'Login',
      component: Login
    },
    {
      path: '/',
      name: 'index',
      // 下面这个meta是重点,这里面配置requireAuth 为true,就是说必须登录的才能访问
      meta : {
        requireAuth: true, 
      },
      component: Index
    }
  ]
})

刚刚还说到全局路由钩子,我们先来看代码,这段代码同样是全局的,也就是说放在main.js ,里面注释进行说明

// 为什么传这三个参数,官网有详细介绍
router.beforeEach((to,from,next) => {
    // 这里的meta就是我们刚刚在路由里面配置的meta
    if(to.meta.requireAuth){
        // 下面这个判断是自行实现到底是否有没有登录
        if (store.getters.isLogin){
        // 登录就继续
            next();
        }else {
        // 没有登录跳转到登录页面,登录成功之后再返回到之前请求的页面
            next({
                path : '/login',
                query : {redirect : to.fullPath}
            })
        }
    }else {
    // 不需要登录的,可以继续访问
        next()
    }
});

现在我们实现了登录验证和登录自动跳转之后,需要请求相册类容了。但相册是需要登录之后才能访问的。不过没关系,我们刚刚不是保存token到localStorage里面吗,把token传过去就可以认证是否登录了,怎么传过去呢,这里因为项目设计为除了登录注册之外的资源请求,都需要验证,所以我们使用axios的全局拦截器。在我们请求发出之前,我们统一加上这个token。同样是在main.js 里

直接看代码吧,:


axios.interceptors.request.use(function (config) {
    if (localStorage.token) {
        config.headers.Authorization = `token ${localStorage.token}`;
    }
    return config;
}, function (err) {
    return Promise.reject(err);
});

ok, 至此,我们就完成了登录验证,保存token,自动跳转,请求资源自动加token。由于我也是初学者,代码写的不好,希望大神能够指点,另外我们开源项目还需要vuejs 大神加入。希望感兴趣的同学用于参与。请联系QQ 343125118

项目地址 Github地址

本帖已被设为精华帖!
本帖由系统于 5年前 自动加精
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 4
Toiu

mark 写的不错

6年前 评论
叶落山城

赞一个!
正好最近我也写了一个...除了表单验证,还有授权登录..
发个demo吧 http://index.g9zz.com/
目前还是demo阶段

6年前 评论

配色很好看,可以分享下吗? :joy:

4年前 评论

@Gins 这个配色是这个网站上面解析的配色,这个你要问站长了

3年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!