先后端分离使用 Token 登陆解决方案

这篇文章写一下先后端分离下的登陆解决方案,目前大多数都采用请求头携带 Token 的形式。前端

开写以前先捋一下整理思路:vue

  • 首次登陆时,后端服务器判断用户帐号密码正确以后,根据用户id、用户名、定义好的秘钥、过时时间生成 token ,返回给前端;
  • 前端拿到后端返回的 token ,存储在 localStorage 和 Vuex 里;
  • 前端每次路由跳转,判断 localStorage 有无 token ,没有则跳转到登陆页,有则请求获取用户信息,改变登陆状态;
  • 每次请求接口,在 Axios 请求头里携带 token;
  • 后端接口判断请求头有无 token,没有或者 token 过时,返回401;
  • 前端获得 401 状态码,重定向到登陆页面。

我这里前端使用 Vue ,地址:vue-token-loginios

后端使用阿里的 egg,地址:egg-token-logingit

首先,咱们先轻微封装一下 Axios:

我把 Token 存在localStorage,检查有无 Token ,每次请求在 Axios 请求头上进行携带github

if (window.localStorage.getItem('token')) {
  Axios.defaults.headers.common['Authorization'] = `Bearer ` + window.localStorage.getItem('token')
}
复制代码

使用 respone 拦截器,对 2xx 状态码之外的结果进行拦截。web

若是状态码是401,则有多是 Token 过时,跳转到登陆页。数据库

instance.interceptors.response.use(
  response => {
    return response
  },
  error => {
    if (error.response) {
      switch (error.response.status) {
        case 401:
          router.replace({
            path: 'login',
            query: { redirect: router.currentRoute.fullPath } // 将跳转的路由path做为参数,登陆成功后跳转到该路由
          })
      }
    }
    return Promise.reject(error.response)
  }
)
复制代码

定义路由:

const router = new Router({
  mode: 'history',
  routes: [
    {
      path: '/',
      name: 'Index',
      component: Index,
      meta: {
        requiresAuth: true
      }
    },
    {
      path: '/login',
      name: 'Login',
      component: Login
    }
  ]
})
复制代码

上面我给首页路由加了 requiresAuth,因此使用路由钩子来拦截导航,localStorage 里有 Token ,就调用获取 userInfo 的方法,并继续执行,若是没有 Token ,调用退出登陆的方法,重定向到登陆页。json

router.beforeEach((to, from, next) => {
  let token = window.localStorage.getItem('token')
  if (to.meta.requiresAuth) {
    if (token) {
      store.dispatch('getUser')
      next()
    } else {
      store.dispatch('logOut')
      next({
        path: '/login',
        query: { redirect: to.fullPath }
      })
    }
  } else {
    next()
  }
})
复制代码

这里使用了两个 Vuex 的 action 方法,立刻就会说到 。后端

Vuex

首先,在 mutation_types 里定义:api

export const LOGIN = 'LOGIN' // 登陆
export const USERINFO = 'USERINFO' // 用户信息
export const LOGINSTATUS = 'LOGINSTATUS' // 登陆状态
复制代码

而后在 mutation 里使用它们:

const mutations = {
  [types.LOGIN]: (state, value) => {
    state.token = value
  },
  [types.USERINFO]: (state, info) => {
    state.userInfo = info
  },
  [types.LOGINSTATUS]: (state, bool) => {
    state.loginStatus = bool
  }
}
复制代码

在以前封装 Axios 的 JS里定义请求接口:

export const login = ({ loginUser, loginPassword }) => {
  return instance.post('/login', {
    username: loginUser,
    password: loginPassword
  })
}

export const getUserInfo = () => {
  return instance.get('/profile')
}
复制代码

在 Vuex 的 actions 里引入:

import * as types from './types'
import { instance, login, getUserInfo } from '../api'
复制代码

定义 action

export default {
  toLogin ({ commit }, info) {
    return new Promise((resolve, reject) => {
      login(info).then(res => {
        if (res.status === 200) {
          commit(types.LOGIN, res.data.token) // 存储 token
          commit(types.LOGINSTATUS, true)     // 改变登陆状态为 
          instance.defaults.headers.common['Authorization'] = `Bearer ` + res.data.token // 请求头添加 token
          window.localStorage.setItem('token', res.data.token)  // 存储进 localStorage
          resolve(res)
        }
      }).catch((error) => {
        console.log(error)
        reject(error)
      })
    })
  },
  getUser ({ commit }) {
    return new Promise((resolve, reject) => {
      getUserInfo().then(res => {
        if (res.status === 200) {
          commit(types.USERINFO, res.data) // 把 userInfo 存进 Vuex
        }
      }).catch((error) => {
        reject(error)
      })
    })
  },
  logOut ({ commit }) { // 退出登陆
    return new Promise((resolve, reject) => {
      commit(types.USERINFO, null)        // 状况 userInfo
      commit(types.LOGINSTATUS, false)  // 登陆状态改成 false
      commit(types.LOGIN, '')          // 清除 token
      window.localStorage.removeItem('token')
    })
  }
}
复制代码

接口

这时候,咱们该去写后端接口了。

我这里用了阿里的 egg 框架,感受很强大。

首先定义一个 LoginController

const Controller = require('egg').Controller;
const jwt = require('jsonwebtoken'); // 引入 jsonwebtoken

class LoginController extends Controller {
  async index() {
    const ctx = this.ctx;
/*
 把用户信息加密成 token ,由于没链接数据库,因此都是假数据
正常应该先判断用户名及密码是否正确
*/
    const token = jwt.sign({       
      user_id: 1,      // user_id
      user_name: ctx.request.body.username // user_name
    }, 'shenzhouhaotian', { // 秘钥
        expiresIn: '60s' // 过时时间
    });
    ctx.body = {             // 返回给前端
      token: token
    };
    ctx.status = 200;           // 状态码 200
  }
}

module.exports = LoginController;
复制代码

UserController:

class UserController extends Controller {
  async index() {
    const ctx = this.ctx
    const authorization = ctx.get('Authorization');
    if (authorization === '') { // 判断请求头有没有携带 token ,没有直接返回 401
        ctx.throw(401, 'no token detected in http header "Authorization"');
    }
    const token = authorization.split(' ')[1];
    // console.log(token)
    let tokenContent;
    try {
        tokenContent = await jwt.verify(token, 'shenzhouhaotian');     //若是 token 过时或验证失败,将返回401
        console.log(tokenContent)
        ctx.body = tokenContent     // token有效,返回 userInfo ;同理,其它接口在这里处理对应逻辑并返回
    } catch (err) {
        ctx.throw(401, 'invalid token');
    }
  }
}
复制代码

在 router.js 里定义接口:

module.exports = app => {
  const { router, controller } = app;
  router.get('/', controller.home.index);
  router.get('/profile', controller.user.index);
  router.post('/login', controller.login.index);
};
复制代码

前端请求

接口写好了,该前端去请求了。

这里我写了个登陆组件,下面是点击登陆时的 login 方法:

login () {
      if (this.username === '') {
        this.$message.warning('用户名不能为空哦~~')
      } else if (this.password === '') {
        this.$message.warning('密码不能为空哦~~')
      } else {
        this.$store.dispatch('toLogin', {      // dispatch toLogin action
          loginUser: this.username,
          loginPassword: this.password
        }).then(() => {
          this.$store.dispatch('getUser')      // dispatch getUserInfo action
          let redirectUrl = decodeURIComponent(this.$route.query.redirect || '/')
          console.log(redirectUrl)
          // 跳转到指定的路由
          this.$router.push({
            path: redirectUrl
          })
        }).catch((error) => {
          console.log(error.response.data.message)
        })
      }
    }
复制代码

登陆成功后,跳转到首页以前重定向过来的页面。

总体流程跑完了,实现的主要功能就是:

  1. 访问登陆注册以外的路由,都须要登陆权限,好比首页,判断有无token,有则访问成功,没有则跳转到登陆页面;
  2. 成功登陆以后,跳转到以前重定向过来的页面;
  3. token 过时后,请求接口时,身份过时,跳转到登陆页,继续第二步;这一步主要用了能够作7天自动登陆等功能。

写的很糙,你们见谅,抱拳了!

相关文章
相关标签/搜索