登录验证实现【学生作业管理系统】

905 阅读7分钟

一、系统使用技术

基于B/S架构前后端分离式开发:

前端:vue+ElementUI

后端:Node.js+Express框架

二、登录验证实现思路

功能概要:

1、用户登录后,在开发者设置好的一段时间内,用户的操作不需要再次登录。

2、当有效时间截止后,用户的操作引发系统提示用户需要重新登录后跳转到登录页面。

3、用户点击退出后,系统跳转到登录页面,用户需要再次登录才能使用系统功能。

三、知识储备

1、Session与Cookie

Session和Cookie可以看做是用来存储用户信息的数据结构。

(1)Cookie:

用户第一次登录后服务器返回Cookie(一般是用户信息)给浏览器,浏览器存储Cookie,在第二次用户发起请求的时候,Cookie被添加到请求头中。服务器接收到请求后,通过判断Cookie的内容识别该请求是否是用户发送的请求。

(2)Session:

功能与Cookie类似,不过本身类似于key-value键值对的数据表,用户第一次登录系统后,Session(value指用户信息)存储在服务中,将SessionID(用户信息标识)发送给浏览器存储,可以在Cookie中查看到浏览器存储的SessionID。用户发送下一次请求的时候,服务器接收SessionID比对自身存储的Session表,判断登录用户的信息是否存在,以此来判断是否为该系统用户发送的请求。

(3)Session与Cookie的区别:

  • Cookie存储在浏览器的Cookie项中,Session存储在服务器中,SessionID存储在浏览器的Cookie项中。

  • Cookie不安全,用户可以通过修改存放在浏览器中的Cookie信息进行Cookie欺骗,考虑安全性的系统应当使用Session。

  • Session存储在服务端,当访问量增加时会增加服务器的压力,不影响安全性的信息内容可以存储在cookie中。

  • 单个cookie存储量不超过4k。

2、请求拦截与响应拦截

(1)前端axios拦截器(请求与响应拦截)
//导入axios
import axios from "axios"
  • 请求拦截

    axios.interceptors.request.use(
    	config=>{
    		//config为axios配置对象
    		return config
    	}
    ),
    error=>{
    	//错误处理
    }
    
  • 响应拦截

    //请求返回拦截,把数据返回到页面之前做些什么...
    axios.interceptors.response.use((response) => {
      //response为服务端返回对象
      //此处执行操作
    }, function (error) {
      //错误处理
    });
    
(2)服务器请求拦截

服务器在执行请求操作时先拦截该请求

// 在app.js中设置请求拦截中间件,需要放在所有路由中间件前
app.use(function(req,res,next){

})

3、vue的导航守卫功能

vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。

简单来说就是使用导航功能切换页面时可以执行设定好的功能。例如,切换页面时用户的登录有效时间已经结束,在切换页面时判断用户信息是否还存在,不存在则退出到登录页面。

有多种机会植入路由导航过程中:全局的, 单个路由独享的, 或者组件级的。

(1)全局前置守卫与全局后置钩子
const router = new VueRouter({ ... })

router.beforeEach((to, from, next) => {
  //to: Route: 即将要进入的目标 路由对象
	//from: Route: 当前导航正要离开的路由
	//next: Function: 一定要调用该方法来 resolve 这个钩子。执行效果依赖 next 方法的调用参数。
	//确保要调用 next 方法,否则钩子就不会被 resolved
})

router.afterEach((to, from) => {
   //to: Route: 即将要进入的目标 路由对象
	 //from: Route: 当前导航正要离开的路由
})
全局解析守卫

在 2.5.0+ 你可以用 router.beforeResolve 注册一个全局守卫。这和 router.beforeEach 类似,区别是在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用。

(2)路由独享的守卫

在router文件夹下的index.js文件中可以直接配置路由独享的守卫

const router = new VueRouter({
  routes: [
    {
      path: '/foo',
      component: Foo,
      beforeEnter: (to, from, next) => {
        // ...
      }
    }
  ]
})
(3)组件内的守卫
const Foo = {
  template: `...`,
  beforeRouteEnter (to, from, next) {
    // 在渲染该组件的对应路由被 confirm 前调用
    // 不!能!获取组件实例 `this`
    // 因为当守卫执行前,组件实例还没被创建
  },
  beforeRouteUpdate (to, from, next) {
    // 在当前路由改变,但是该组件被复用时调用
    // 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
    // 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
    // 可以访问组件实例 `this`
  },
  beforeRouteLeave (to, from, next) {
    // 导航离开该组件的对应路由时调用
    // 可以访问组件实例 `this`
  }
}
  • beforeRouterEnter不能使用this

    不能访问this,因为守卫在导航确认前被调用,因此即将登场的新组件还没被创建,不过可以通过一个回调给next来访问组件实例,在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数。

    beforeRouterEnter(to,from,next){
    	next(vm=>{
    		//通过vm访问组件实例
    	})
    }
    
  • beforeRouterLeave离开后卫

    离开守卫通常用来精致用户在还没保存修改前突然离开。该导航可以通过next(false)来取消

    beforeRouteLeave (to, from, next) {
      const answer = window.confirm('Do you really want to leave? you have unsaved changes!')
      if (answer) {
        next()
      } else {
        next(false)
      }
    }
    
(4)完整的导航解析流程

导航被触发。

在失活的组件里调用离开守卫。

调用全局的 beforeEach 守卫。

在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。

在路由配置里调用 beforeEnter

解析异步路由组件。

在被激活的组件里调用 beforeRouteEnter

调用全局的 beforeResolve 守卫 (2.5+)。

导航被确认。

调用全局的 afterEach 钩子。

触发 DOM 更新。

用创建好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数。

4、路由元信息

定义导航的时候可以配置meta字段:

const router = new VueRouter({
  routes: [
    {
      path: '/foo',
      component: Foo,
      children: [
        {
          path: 'bar',
          component: Bar,
          // a meta field
          meta: { requiresAuth: true }
        }
      ]
    }
  ]
})

routes配置中的每个路由对象为路由记录。路由记录是可以嵌套的 ,因此,当一个路由匹配成功后,它可能匹配多个路由记录。

一个路由匹配到的所有路由记录会暴露为 $route 对象 (还有在导航守卫中的路由对象) 的 $route.matched 数组。因此,我们需要遍历 $route.matched 来检查路由记录中的 meta 字段。

router.beforeEach((to, from, next) => {
  if (to.matched.some(record => record.meta.requiresAuth)) {
    // this route requires auth, check if logged in
    // if not, redirect to login page.
    if (!auth.loggedIn()) {
      next({
        path: '/login',
        query: { redirect: to.fullPath }
      })
    } else {
      next()
    }
  } else {
    next() // 确保一定要调用 next()
  }
})

四、功能实现

实现登录验证功能需要完成以下几个方面:

服务器方面(node+express):

  1. 用户登录后服务器设置session,设置session有效期。
  2. 服务器设置请求拦截中间件,路由匹配前先判断session信息是否存在(过期后或者用户退出后sessionID对应的用户信息会清除,清除后为空,不存在的时候应该向前端发送提示信息,如状态码等。
  3. 匹配用户的退出路由后,需要清除指定session信息。

前端方面(vue+vue-router):

  1. 用户登录成功后,将从服务端接收到用户信息存储到localStorage中作为判断用户是否登录的依据。
  2. 响应拦截,前端发送请求后,在获取请求数据之前,将先判断服务器是否返回了session不存在的提示信息或者状态码。
  3. 退出功能的实现,向服务器发送退出请求,请求成功后删除存储的localStorage用户信息。
  4. 结合路由元的全局守卫前置守卫功能,需要使用前置守卫功能的路由将设置路由元,在全局前置守卫功能中判断当前切换的路由是否具有路由元设置的信息,若有则需要判断localStorage中的用户信息是否存在,若不存在则控制跳转到登录页面。

五、程序代码

  1. 服务器设置session

    安装第三方包

    npm install express express-session --save

    全局导入

    var express= require("express")
    var session= require("session")
    

    配置session信息

    var app=express()
    
    app.use(session({
        secret: 'zac',			//设置秘钥,随意填写
        name: 'myCookie',   //这里的name值得是cookie的name,可以在浏览器调试栏中的cookie项查看到
        cookie: {maxAge: 3600000 },  //设置maxAge是360000ms,即一小时s后session和相应的cookie失效过期
        resave: false, //是否强制session保存到session store中
        saveUninitialized: true, //强制没有“初始化”的session保存到storage中
    }));
    

    登录信息验证成功后设置session

    router.post('/login', function(req, res, next) {
    	let table = req.body.table
    	let email=req.body.email
    	let password=req.body.password
    	let sql=`SELECT * FROM ${table} WHERE Email="${email}" and Password = "${password}"`
    	query(sql,function(err,val){
    		if (err) {
    			res.send(err)
    		}else {
    			req.session.user=val   //直接将用户信息作为session的存储项
    			res.send(val)
    		}
    	})
    });
    
  2. 服务器拦截请求中间件

    app.js文件中,拦截请求中间件位置应放在所有设定的路由的前面

    var openPage = ['/','/login','/resign','/logout']  //建立一个不需要拦截的路由数组,如登录、注册、退出等
    
    app.use(function(req,res,next){     //拦截中间件
      var url=req.originalUrl						//判断请求路径是否需要拦截
      if(openPage.indexOf(url)>-1){
        next()
      }else{
        if(req.session.user){						//存在则下一步
          next()
        }else {
          res.send("401")								//不存在则返回字符串401给前端
        }
      }
    })
    
    app.use('/', indexRouter);						//自定义的路由
    app.use('/teacher', teacherRouter);
    app.use('/student', studentRouter);
    app.use('/admin', adminRouter);
    
    
  3. 路由接收退出请求的操作
    router.get('/logout',function(req,res,next){
    	req.session.destroy(function(err){    //使用destroy方法清除session
    		res.send(err)
    	})
    })
    
    
  4. 前端提交用户信息后,存储获取到的用户信息数据
    onSubmit(){					//提交用户信息的方法
    				this.$axios.post('api/login', 
    				{
    					email:this.user.email,
    					password:this.user.password,
    					table:this.showInfo.table
    				})
    				.then((res)=> {
    					console.log(res)
    					let userInfo=res.data[0]
    					localStorage.setItem('userInfo', JSON.stringify(userInfo))  //存储用户信息到localStorage中
    					if (userInfo) {
    						if(this.showInfo.table === "Student"){
    							this.$router.push("/student/mywork")
    						}else{
    							this.$router.push("/teacher/mycourse")
    						}
    					}else {
    						this.error=true
    					}
    				})
    				.catch(function (error) {
    					console.log(error);
    				})
    			}
    
  5. 前端响应拦截,判断session是否过期

    session过期时,前端会接收到服务字符串401

    在main.js文件中

    //响应拦截,把数据返回到页面之前做些什么...
    axios.interceptors.response.use((response) => {
      //特殊错误处理,状态为401时为session过期,需要重新登录  
      if (response.data == "401") {
        router.push("/login")
        alert("登录超时,请重新登录")
      //其余错误状态处理    
      }else{
        //将我们请求到的信息返回页面中请求的逻辑
        return response;
      }
    }, function (error) {
      console.log(error)
    });
    
  6. 前端退出方法的实现
    logout(){
    				localStorage.removeItem("userInfo")   //清除localStorage中存储的用户信息
    				this.$axios.get('api/logout')
    				.then((res)=> {
    					this.$router.push("/login")
    				})
    				.catch(function (error) {
    					console.log(error);
    				})	
    			}
    
  7. 前端结合路由元的全局守卫前置守卫功能
    • 给路由设置路由元

      在router文件夹中的index.js文件夹下

      {
            path: '/admin',
            name: 'Admin',
            component: ()=>import('@/pages/admin/Admin'),
            meta:{
                  isLogin:true   //判断标记
            },
            children:[
              {
                path: 'teacher',
                name: 'AdminTeacher',
                component: AdminTeacher,
                meta:{
                  isLogin:true
                }
              },
              {
                path: 'student',
                name: 'AdminStudent',
                component: AdminStudent,
                meta:{
                  isLogin:true
                }
              },
              {
                path: 'administrator',
                name: 'Administrator',
                component: Administrator,
                meta:{
                  isLogin:true
                }
              },
            ]
          },
      
    • 在main.js文件中

      // 路由守卫
      router.beforeEach((to,from,next)=>{
              if(to.matched.some(res=>res.meta.isLogin)){//判断是否需要登录
                  if (localStorage['userInfo']) {
                      next();
                  }else{
                      next({
                          path:"/login"
                      });
                  }
      
              }else{
                  next()
              }
          });
      

六、声明

本文是作者做项目的归纳与总结,只用于学习用途。之后将添加防csrf攻击功能。