日志

笔记 vue+springboot前后端分离实现token登录验证和状态保存的简单实现方案

 来源    2019-12-01    1991  

简单实现

token可用于登录验证和权限管理。
大致步骤分为:

  1. 前端登录,post用户名和密码到后端。
  2. 后端验证用户名和密码,若通过,生成一个token返回给前端。
  3. 前端拿到token用vuex和localStorage管理,登录成功进入首页。
  4. 之后前端每一次权限操作如跳转路由,都需要判断是否存在token,若不存在,跳转至登录页。
  5. 前端之后的每一个对后端的请求都要在请求头上带上token,后端查看请求头是否有token,拿到token检查是否过期,返回对应状态给前端。
  6. 若token已过期,清除token信息,跳转至登录页。
    具体代码如下:

    前端

  • 登录页
<template>
    <div class="signin-form">
        <h3 class="sign-title">ticket-sys 登录</h3>
        <div>
            <el-form :model="loginForm" :rules="rules" status-icon ref="ruleForm" class="demo-ruleForm">
                <el-form-item prop="username">
                    <el-input
                            v-model="loginForm.username"
                            autocomplete="off"
                            placeholder="用户名"
                            prefix-icon="el-icon-user-solid"
                    ></el-input>
                </el-form-item>
                <el-form-item prop="password">
                    <el-input
                            type="password"
                            v-model="loginForm.password"
                            autocomplete="off"
                            placeholder="请输入密码"
                            prefix-icon="el-icon-lock"
                    ></el-input>
                </el-form-item>
                <el-form-item>
                    <el-button type="primary" @click="submitForm" id="login-btn">登录</el-button>
                </el-form-item>
            </el-form>
        </div>
    </div>
</template>
<script>
    import api from '../constant/api';
    import {mapMutations} from "vuex";
    export default {
        name: 'login',
        data() {
            return {
                loginForm:{
                    username:'',
                    password:''
                },
                userToken:'',
                rules:{
                    username:[
                        { required: true, message: '请输入用户名', trigger: 'blur' },
                    ],
                    password:[
                        { required: true, message: '请输入密码', trigger: 'blur' },
                    ]
                }
            }
        },
        methods: {
            ...mapMutations(['changeLogin']),
            submitForm() {
                let v=this;
                this.$axios({
                    method: 'post',
                    url: api.base_url+'/user/login',
                    data:{
                        'username':v.loginForm.username,
                        'password':v.loginForm.password
                    }
                }).then(function(res){
                    console.log(res.data);
                    v.userToken = 'Bearer ' + res.data.token;
                    // 将用户token保存到vuex中
                    v.changeLogin({ Authorization:v.userToken });
                    v.$router.push('/home');
                    v.$message('登录成功');
                }).catch(function(err){
                    console.log("err",err);
                    v.$message('密码或用户名错误');
                })
            }
        }
    }
</script>
<style scoped>...</style>
  • vuex状态管理
    /store/index.js
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
const store = new Vuex.Store({
    state: {
        // 存储token
        Authorization: localStorage.getItem('Authorization') ? localStorage.getItem('Authorization') : ''
    },
    mutations: {
        // 修改token,并将token存入localStorage
        changeLogin (state, user) {
            state.Authorization = user.Authorization;
            localStorage.setItem('Authorization', user.Authorization);
        }
    }
});

export default store;
  • 路由守卫
    /router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'

Vue.use(VueRouter)

const routes = [
  {
    path: '/home',
    name: 'home',
    component: Home,
  }
  ,
  {
    path: '/about',
    name: 'about',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: function () {
      return import(/* webpackChunkName: "about" */ '../views/About.vue')
    }
  },
  {
    path:'/login',
    name:'login',
    component:function () {
      return import('../views/Login.vue');
    }
  }
]

const router = new VueRouter({
 <template>
  <div class="home">
    <el-container>
<!--      <Header/>-->
<!--      <el-main>首页</el-main>-->
      <el-button @click="exit">退出登录</el-button>
      <el-button @click="test">携带token的测试请求</el-button>
    </el-container>
  </div>
</template>

<script>
// @ is an alias to /src
import HelloWorld from '@/components/HelloWorld.vue'
import Header from "../components/Header";
import api from "../constant/api";

export default {
  name: 'home',
  components: {
    Header,
    HelloWorld
  },
  methods:{
    exit(){
      localStorage.removeItem('Authorization');
      this.$router.push('/login');
    },
    test(){
      this.$axios({
        method: 'get',
        url: api.base_url+'/user/test',
      }).then(function(res){
        console.log("res",res);
      }).catch(function(err){
        console.log("err",err);
      })
    }
  }
}
</script>
<style>...</style>
 mode: 'history',
  base: process.env.BASE_URL,
  routes
});
// 导航守卫
// 使用 router.beforeEach 注册一个全局前置守卫,判断用户是否登陆
router.beforeEach((to, from, next) => {
  if (to.path === '/login') {
    next();
  } else {
    let token = localStorage.getItem('Authorization');
    if (token === 'null' || token === '') {
      next('/login');
    } else {
      next();
    }
  }
});
export default router
  • 主文件中注册并添加拦截器
    /main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import './plugins/element.js'
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';

import axios from 'axios' ;
import Vuex from 'vuex' //引入状态管理
Vue.prototype.$axios= axios ;
Vue.use(Vuex) ;

Vue.config.productionTip = false
Vue.use(ElementUI)

//这里要导入store
import store from "./store";

// 添加请求拦截器,在请求头中加token
axios.interceptors.request.use(
    config => {
      if (localStorage.getItem('Authorization')) {
        config.headers.Authorization = localStorage.getItem('Authorization');
      }

      return config;
    },
    error => {
      return Promise.reject(error);
    });

new Vue({
    //这里要配置store
  router, store:store,
  render: function (h) { return h(App) }
}).$mount('#app')
  • home页面
<template>
  <div class="home">
    <el-container>
<!--      <Header/>-->
<!--      <el-main>首页</el-main>-->
      <el-button @click="exit">退出登录</el-button>
      <el-button @click="test">携带token的测试请求</el-button>
    </el-container>
  </div>
</template>

<script>
// @ is an alias to /src
import HelloWorld from '@/components/HelloWorld.vue'
import Header from "../components/Header";
import api from "../constant/api";

export default {
  name: 'home',
  components: {
    Header,
    HelloWorld
  },
  methods:{
    exit(){
      //退出登录,清空token
      localStorage.removeItem('Authorization');
      this.$router.push('/login');
    },
    test(){
      this.$axios({
        method: 'get',
        url: api.base_url+'/user/test',
      }).then(function(res){
        console.log("res",res);
      }).catch(function(err){
        console.log("err",err);
      })
    }
  }
}
</script>
<style>...</style>

后端

  • 登录controller
package com.zxc.ticketsys.controller;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/user")
public class UserController{
    @RequestMapping(value = "/login",method = RequestMethod.POST)
    @ResponseBody
    public String login(@RequestHeader Map<String,Object> he,@RequestBody Map<String,Object> para) throws JsonProcessingException {
        System.out.println(he);
        String username=(String)para.get("username");
        String password=(String)para.get("password");
        HashMap<String,Object> hs=new HashMap<>();
        hs.put("token","token"+username+password);
        ObjectMapper objectMapper=new ObjectMapper();
        return objectMapper.writeValueAsString(hs);
    }

    @RequestMapping(value = "/test",method = RequestMethod.GET)
    @ResponseBody
    public String test(@RequestHeader Map<String,Object> he) throws JsonProcessingException {
        System.out.println(he);
        HashMap<String,Object> hs=new HashMap<>();
        ObjectMapper objectMapper=new ObjectMapper();
        return objectMapper.writeValueAsString(hs);
    }
}

测试

  • 登录

    此时后台的请求头:
{host=localhost:8088, connection=keep-alive, accept=application/json, text/plain, */*, origin=http://localhost:8080, user-agent=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36, sec-fetch-site=same-site, sec-fetch-mode=cors, referer=http://localhost:8080/home, accept-encoding=gzip, deflate, br, accept-language=zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7}
  • 登录成功,进入home页面

    后端返回的token:
  • 前端持有token,访问测试接口
    后端收到请求头:
{host=localhost:8088, connection=keep-alive, accept=application/json, text/plain, */*, origin=http://localhost:8080, authorization=Bearer tokenadminadmin, user-agent=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36, sec-fetch-site=same-site, sec-fetch-mode=cors, referer=http://localhost:8080/home, accept-encoding=gzip, deflate, br, accept-language=zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7}

带有token,可以进行验证

  • 退出登录,清空token

后台实际应用

在实际的应用中,一般需要一个生成token的工具类和一个拦截器对请求进行拦截。

  • token生成工具类
    /utils/TokenUtil.java
package com.zxc.ticketsys.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.zxc.ticketsys.model.User;
import java.util.Date;

public class TokenUtil {

    private static final long EXPIRE_TIME= 10*60*60*1000;
    private static final String TOKEN_SECRET="txdy";  //密钥盐
    
    /**
     * 签名生成
     * @param user
     * @return
     */
    public static String sign(User user){
        String token = null;
        try {
            Date expiresAt = new Date(System.currentTimeMillis() + EXPIRE_TIME);
            token = JWT.create()
                    .withIssuer("auth0")
                    .withClaim("username", user.getUsername())
                    .withExpiresAt(expiresAt)
                    // 使用了HMAC256加密算法。
                    .sign(Algorithm.HMAC256(TOKEN_SECRET));
        } catch (Exception e){
            e.printStackTrace();
        }
        return token;
    }

    /**
     * 签名验证
     * @param token
     * @return
     */
    public static boolean verify(String token){
        try {
            JWTVerifier verifier = JWT.require(Algorithm.HMAC256(TOKEN_SECRET)).withIssuer("auth0").build();
            DecodedJWT jwt = verifier.verify(token);
            System.out.println("认证通过:");
            System.out.println("username: " + jwt.getClaim("username").asString());
            System.out.println("过期时间:      " + jwt.getExpiresAt());
            return true;
        } catch (Exception e){
            return false;
        }
    }
}
  • 拦截器类
    /interceptor/TokenInterceptor.java
package com.zxc.ticketsys.interceptor;

import com.alibaba.fastjson.JSONObject;
import com.zxc.ticketsys.utils.TokenUtil;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class TokenInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,Object handler)throws Exception{
        if(request.getMethod().equals("OPTIONS")){
            response.setStatus(HttpServletResponse.SC_OK);
            return true;
        }
        response.setCharacterEncoding("utf-8");
        String token = request.getHeader("token");
        if(token != null){
            boolean result = TokenUtil.verify(token);
            if(result){
                System.out.println("通过拦截器");
                return true;
            }
        }
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        try{
            JSONObject json = new JSONObject();
            json.put("msg","token verify fail");
            json.put("code","50000");
            response.getWriter().append(json.toJSONString());
            System.out.println("认证失败,未通过拦截器");
        }catch (Exception e){
            e.printStackTrace();
            response.sendError(500);
            return false;
        }
        return false;
    }
}
  • 配置拦截器
    /config/WebConfiguration.java
    注意最好写在一个配置类里,且WebMvcConfigurationSupport和WebMvcConfigurerAdapter不要同时存在
    这里包括处理跨域的配置,而且全部改为implements WebMvcConfigurer接口
package com.zxc.ticketsys.config;

import com.zxc.ticketsys.interceptor.TokenInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ConcurrentTaskExecutor;
import org.springframework.web.servlet.config.annotation.*;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;


/**
 * 跨域请求支持/token拦截
 * tip:只能写在一个配置类里
 */
@Configuration
public class WebConfiguration implements WebMvcConfigurer {

    private TokenInterceptor tokenInterceptor;

    //构造方法
    public WebConfiguration(TokenInterceptor tokenInterceptor){
        this.tokenInterceptor = tokenInterceptor;
    }

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowCredentials(true)
                .allowedHeaders("*")
                .allowedMethods("*")
                .allowedOrigins("*");
    }

    @Override
    public void configureAsyncSupport(AsyncSupportConfigurer configurer){
        configurer.setTaskExecutor(new ConcurrentTaskExecutor(Executors.newFixedThreadPool(3)));
        configurer.setDefaultTimeout(30000);
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry){
        List<String> excludePath = new ArrayList<>();
        //排除拦截,除了注册登录(此时还没token),其他都拦截
        excludePath.add("/user/register");  //登录
        excludePath.add("/user/login");     //注册
        excludePath.add("/static/**");  //静态资源
        excludePath.add("/assets/**");  //静态资源

        registry.addInterceptor(tokenInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns(excludePath);
        WebMvcConfigurer.super.addInterceptors(registry);
    }
}
  • 控制器类
package com.zxc.ticketsys.controller;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zxc.ticketsys.model.User;
import com.zxc.ticketsys.utils.TokenUtil;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/user")
public class UserController{
    @RequestMapping(value = "/login",method = RequestMethod.POST)
    @ResponseBody
    public String login(@RequestBody Map<String,Object> para) throws JsonProcessingException {
        String username=(String)para.get("username");
        String password=(String)para.get("password");
        String token= TokenUtil.sign(new User(username,password));
        HashMap<String,Object> hs=new HashMap<>();
        hs.put("token",token);
        ObjectMapper objectMapper=new ObjectMapper();
        return objectMapper.writeValueAsString(hs);
    }

    @RequestMapping(value = "/test",method = RequestMethod.POST)
    @ResponseBody
    public String test(@RequestBody Map<String,Object> para) throws JsonProcessingException {
        HashMap<String,Object> hs=new HashMap<>();
        hs.put("data","data");
        ObjectMapper objectMapper=new ObjectMapper();
        return objectMapper.writeValueAsString(hs);
    }
}
  • 测试
  1. 登录
    成功登录,获得token。
  2. 不带token访问test接口
    被拦截,访问失败。
  3. 带有效token访问test接口
    访问成功,获得数据。

    完整项目代码

    github

相关文章
一套基于SpringBoot+Vue+Shiro 前后端分离 开发的代码生成器
日志一.前言 最近花了一个月时间完成了一套基于Spring Boot+Vue+Shiro前后端分离的代码生成器,目前项目代码已基本完成 止步传统CRUD,进阶代码优化: 该项目可根据数据库字段动态生成 c ...
33
springboot+vue的前后端分离与合并方案
日志pringboot和vue结合的方案网络上的主要有以下两种: 1. [不推荐]在html中直接使用script标签引入vue和一些常用的组件,这种方式和以前传统的开发是一样的,只是可以很爽的使用vue ...
20
SpringBoot前后端分离Instant时间戳自定义解析
日志在SpringBoot项目中,前后端规定传递时间使用时间戳(精度ms). @Data public class Incident { @ApiModelProperty(value = "故 ...
1
SpringBoot应用篇(二):SpringSecurity实现前后端分离项目的登录认证 附代码
日志一.文章简介 本文简要介绍了spring security的基本原理和实现,并基于springboot整合了spring security实现了基于数据库管理的用户的登录和登出,登录过程实现了验证码的 ...
27
Springboot前后端分离开发
日志.1.springboot前后端分离开发之前要配置好很多东西,这周会详细补充博客内容和遇到的问题的解析 2,按照下面流程走一遍   此时会加载稍等一下 pom.xml显示中加上阿里云镜像可以加速下载配 ...
8
dotnetcore+vue+elementUI 前后端分离 三(前端篇)
日志说明: 本项目使用了 mysql employees数据库,使用了vue + axois + element UI 2.0 ,演示了 单页程序 架构 ,vue router 的使用,axois 使用, ...
25
SpringCloud SpringBoot 前后端分离企业级微服务架构源码赠送
日志基于SpringBoot2.x.SpringCloud和SpringCloudAlibaba并采用前后端分离的企业级微服务敏捷开发系统架构.并引入组件化的思想实现高内聚低耦合,项目代码简洁注释丰富上手 ...
61
dotnetcore+vue+elementUI 前后端分离架 二(后端篇)
日志前言 最近几年前后端分离架构大行其道,而且各种框架也是层出不穷.本文通过dotnetcore +vue 来介绍 前后端分离架构实战. 涉及的技术栈 服务端技术 mysql 本项目使用mysql 作为持 ...
102
Session与Token认证机制 前后端分离下如何登录
日志 字号 1 Web登录涉及到知识点 1.1 HTTP无状态性 HTTP是无状态的,一次请求结束,连接断开,下次服务器再收到请求,它就不知道这个请求是哪个用户发过来的.当然它知道是哪个客户端地址发过来的 ...
8
springboot 前后端分离开发 从零到整(四、更改密码操作)
日志前端发送更改密码请求,头部携带token,服务端拦截器拦截头部token并解析,根据token中的信息来查询用户信息.需要登录才能进行的操作是由自己定的,有些操作可以直接放行.具体实现是: 上一章写到 ...
68
springboot 前后端分离开发 从零到整(三、登录以及登录状态的持续)
日志今天来写一下怎么登录和维持登录状态. 相信登录验证大家都比较熟悉,在Javaweb中一般保持登录状态都会用session.但如果是前后端分离的话,session的作用就没有那么明显了.对于前后端分离的 ...
20
springboot 前后端分离开发 从零到整(二、邮箱注册)
日志spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver username: root password: 123456 url: ...
5
转 python+django+vue搭建前后端分离项目
日志https://www.cnblogs.com/zhixi/p/9996832.html   以前一直是做基于PHP或JAVA的前后端分离开发,最近跟着python风搭建了一个基于django的前后端 ...
2
python+django+vue搭建前后端分离项目
日志以前一直是做基于PHP或JAVA的前后端分离开发,最近跟着python风搭建了一个基于django的前后端分享项目 准备工作:IDE,[JetBrains PyCharm2018][webpack 3 ...
1
vue+rest-framework前后端分离整合(二)
日志一.基于api前端显示课程详细信息 1.调整Course.vue模块 <template> <div> <h1>课程列表</h1> <div v- ...
2
vue+rest-framework前后端分离整合
日志一.为什么要做前后端分离项目 1.满足多端适配 随着移动端的兴起,现在公司产品不只限于pc端的,包括Android,IOS. 按照以前的方式,我们后端其实就要有多套,pc一套,APP端两套.开发成本以 ...
1
前后端分离下如何登录
日志1 Web登录涉及到知识点 1.1 HTTP无状态性 HTTP是无状态的,一次请求结束,连接断开,下次服务器再收到请求,它就不知道这个请求是哪个用户发过来的.当然它知道是哪个客户端地址发过来的,但是对 ...
2
SpringBoot 和Vue前后端分离入门教程(附源码)
日志作者:梁小生0101 juejin.im/post/5c622fb5e51d457f9f2c2381 推荐阅读(点击即可跳转阅读) 1. SpringBoot内容聚合 2. 面试题内容聚合 3. 设计 ...
4
SpringBoot+Vue前后端分离,使用SpringSecurity完美处理权限问题
日志原文链接:https://segmentfault.com/a/1190000012879279 当前后端分离时,权限问题的处理也和我们传统的处理方式有一点差异.笔者前几天刚好在负责一个项目的权限管理 ...
6