springboot-vue前后端分离session过期重新登录的实现

springboot-vue前后端分离session过期重新登录的实现

简单回顾cookie和session

  1. cookie和session都是回话管理的方式
  2. Cookie
    • cookie是浏览器端存储信息的一种方式
    • 服务端可以通过响应浏览器set-cookie标头(header,),浏览器接收到这个标头信息后,将以文件形式将cookie信息保存在浏览器客户端的计算机上。之后的请求,浏览器将该域的cookie信息再一并发送给服务端。
    • cookie默认的存活期限关闭浏览器后失效,即浏览器在关闭时清除cookie文件信息。我们可以在服务端响应cookie时,设置其存活期限,比如设为一周,这样关闭浏览器后也cookie还在期限内没有被清除,下次请求浏览器就会将其发送给服务端了。
  3. Session
    • session的使用是和cookie紧密关联的
    • cookie存储在客户端(浏览器负责记忆),session存储在服务端(在Java中是web容器对象,服务端负责记忆)。
    • 每个session对象有一个sessionID,这个ID值还是用cookie方式存储在浏览器,浏览器发送cookie,服务端web容器根据cookie中的sessionID得到对应的session对象,这样就能得到各个浏览器的“会话”信息。
    • 正是因为sessionID实际使用的cookie方式存储在客户端,而cookie默认的存活期限是浏览器关闭,所以session的“有效期”即是浏览器关闭

开发环境

  • JDK8、Maven3.5.3、springboot2.1.6、STS4
  • node10.16、npm6.9、vue2.9、element-ui、axios

springboot后端提供接口

  • demo 已放置 Gitee
  • 本次 demo 只需要 starter-web pom.xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
  • 后台接口只提供接口服务,端口8080 application.properties
server.port=8080
  • 只有一个controller,里面有3个handle,分别是登录、注销和正常请求 TestCtrller.java
@RestController
public class TestCtrller extends BaseCtrller{
    //session失效化-for功能测试
    @GetMapping("/invalidateSession")
    public BaseResult invalidateSession(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if(session != null &&
                session.getAttribute(SysConsts.Session_Login_Key)!=null) {
            request.getSession().invalidate();
            getServletContext().log("Session已注销!");
        }
        return new BaseResult(true);
    }

    //模拟普通ajax数据请求(待登录拦截的)
    @GetMapping("/hello")
    public BaseResult hello(HttpServletRequest request) {
        getServletContext().log("登录session未失效,继续正常流程!");
        return new BaseResult(true, "登录session未失效,继续正常流程!");
    }

    //登录接口
    @PostMapping("/login")
    public BaseResult login(@RequestBody SysUser dto, HttpServletRequest request) {
        //cookie信息
        Cookie[] cookies = request.getCookies();
        if(null!=cookies && cookies.length>0) {
            for(Cookie c:cookies) {
                System.out.printf("cookieName-%s, cookieValue-%s, cookieAge-%d%n", c.getName(), c.getValue(), c.getMaxAge());
            }
        }

        /**
         * session处理
         */
        //模拟库存数据
        SysUser entity = new SysUser();
        entity.setId(1);
        entity.setPassword("123456");
        entity.setUsername("Richard");
        entity.setNickname("Richard-管理员");
        //验密
        if(entity.getUsername().equals(dto.getUsername()) && entity.getPassword().equals(dto.getPassword())) {
            if(request.getSession(false) != null) {
                System.out.println("每次登录成功改变SessionID!");
                request.changeSessionId(); //安全考量,每次登陆成功改变 Session ID,原理:原来的session注销,拷贝其属性建立新的session对象
            }
            //新建/刷新session对象
            HttpSession session = request.getSession();
            System.out.printf("sessionId: %s%n", session.getId());
            session.setAttribute(SysConsts.Session_Login_Key, entity);
            session.setAttribute(SysConsts.Session_UserId, entity.getId());
            session.setAttribute(SysConsts.Session_Username, entity.getUsername());
            session.setAttribute(SysConsts.Session_Nickname, entity.getNickname());

            entity.setId(null); //敏感数据不返回前端
            entity.setPassword(null);
            return new BaseResult(entity);
        }
        else {
            return new BaseResult(ErrorEnum.Login_Incorrect);
        }
    }
}
  • 全局跨域配置和登陆拦截器注册 MyWebMvcConfig.java
@Configuration
public class MyWebMvcConfig implements  WebMvcConfigurer{
    //全局跨域配置
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**") //添加映射路径
                .allowedOrigins("http://localhost:8081") //放行哪些原始域
                .allowedMethods("*") //放行哪些原始域(请求方式) //"GET","POST", "PUT", "DELETE", "OPTIONS"
                .allowedHeaders("*") //放行哪些原始域(头部信息)
                .allowCredentials(true) //是否发送Cookie信息
//              .exposedHeaders("access-control-allow-headers",
//                              "access-control-allow-methods",
//                              "access-control-allow-origin",
//                              "access-control-max-age",
//                              "X-Frame-Options") //暴露哪些头部信息(因为跨域访问默认不能获取全部头部信息)
                .maxAge(1800);
    }

    //注册拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new MyLoginInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns("/login")
                .excludePathPatterns("/invalidateSession");
                //.excludePathPatterns("/static/**");
    }
}
  • 登录拦截器 MyLoginInterceptor.java
public class MyLoginInterceptor implements HandlerInterceptor{
    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response, Object handler) throws Exception {
        request.getServletContext().log("MyLoginInterceptor preHandle");

        HttpSession session = request.getSession();
        request.getServletContext().log("sessionID: " + session.getId());

        Optional<Object> token = Optional.ofNullable(session.getAttribute(SysConsts.Session_Login_Key));
        if(token.isPresent()) { //not null
            request.getServletContext().log("登录session未失效,继续正常流程!");
        } else {
            request.getServletContext().log(ErrorEnum.Login_Session_Out.msg());
//          Enumeration<String> enumHeader =  request.getHeaderNames();
//          while(enumHeader.hasMoreElements()) {
//              String name = enumHeader.nextElement();
//              String value = request.getHeader(name);
//              request.getServletContext().log("headerName: " + name + " headerValue: " + value);
//          }
            //尚未弄清楚为啥全局异常处理返回的响应中没有跨域需要的header,于是乎强行设置响应header达到目的 XD..
            //希望有答案的伙伴可以留言赐教
            response.setHeader("Access-Control-Allow-Origin", request.getHeader("origin"));
            response.setHeader("Access-Control-Allow-Credentials", "true");
            response.setCharacterEncoding("UTF-8");
            response.setContentType("text/html; charset=utf-8");
//            PrintWriter writer = response.getWriter();
//            writer.print(new BaseResult(ErrorEnum.Login_Session_Out));
//            return false;
            throw new BusinessException(ErrorEnum.Login_Session_Out);
        }

        return true;
    }
}
  • 全局异常处理 MyCtrllerAdvice.java
@ControllerAdvice(
        basePackages = {"com.**.web.*"},
        annotations = {Controller.class, RestController.class})
public class MyCtrllerAdvice {

    //全局异常处理-ajax-json
    @ExceptionHandler(value=Exception.class)
    @ResponseBody
    public BaseResult exceptionForAjax(Exception ex) {
        if(ex instanceof BusinessException) {
            return new BaseResult((BusinessException)ex);
        }else {
            return new BaseResult(ex.getCause()==null?ex.getMessage():ex.getCause().getMessage());
        }
    }
}
  • 后端项目包结构

vue-cli(2.x)前端

  • demo 已放置 Gitee
  • 前端项目包结构-标准的 vue-cli

  • 路由设置,登录(‘/‘)和首页 router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import Home from '@/components/Home'
import Login from '@/components/Login'

Vue.use(Router)

export default new Router({
  routes: [
        {
          path: '/',
          name: 'Login',
          component: Login
        },
        {
          path: '/home',
          name: 'Home',
          component: Home
        }
  ]
})
  • 设置端口为8081(后端则是8080)config/index.js
module.exports = {
  dev: {

    // Paths
    assetsSubDirectory: 'static',
    assetsPublicPath: '/',
    proxyTable: {},

    // Various Dev Server settings
    host: 'localhost', // can be overwritten by process.env.HOST
    port: 8081, // can be overwritten by
    //...
  • 简单的登录和首页组件(完整代码-见demo-Gitte链)

    • 登录

    • 登录后首页

  • axios ajax请求全局设置、响应和异常处理 src/main.js
import axios from 'axios'
axios.defaults.baseURL = 'http://localhost:8080'
//axios.defaults.timeout = 3000
axios.defaults.withCredentials = true //请求发送cookie

// 添加请求拦截器
axios.interceptors.request.use(function (config) {
    // 在发送请求之前做些什么
    console.log('in interceptor, request config: ', config);
    return config;
  }, function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
  });

// 添加响应拦截器
axios.interceptors.response.use(function (response) {
    // 对响应数据做点什么
    console.log('in interceptor, response: ', response);
    if(!response.data.success){
        console.log('errCode:', response.data.errCode, 'errMsg:', response.data.errMsg);
        Message({type:'error',message:response.data.errMsg});
        let code = response.data.errCode;
        if('login02'==code){ //登录session失效
            //window.location.href = '/';
            console.log('before to login, current route path:', router.currentRoute.path);
            router.push({path:'/', query:{redirect:router.currentRoute.path}});
        }
    }
    return response;
  }, function (error) {
    // 对响应错误做点什么
        console.log('in interceptor, error: ', error);
        Message({showClose: true, message: error, type: 'error'});
    return Promise.reject(error);
  });
  • 路由URL跳转拦截(sessionStorage初级版)src/main.js
//URL跳转(变化)拦截
router.beforeEach((to, from, next) => {
    //console.log(to, from, next) //
    if(to.name=='Login'){ //本身就是登录页,就不用验证登录session了
        next()
        return
    }
    if(!sessionStorage.getItem('username')){ //没有登录/登录过期
        next({path:'/', query:{redirect:to.path}})
    }else{
        next()
    }
})
  • 测试过程

    前端进入即是login页,用户名和密码正确则后端保存登录的Session,前端登录成功跳转home页,点击‘功能测试‘则是正常json响应(Session有效)。如何在本页中主动将Session失效,再次功能测试则会被拦截,跳转登录页。

碰到的问题

  • 全局异常处理返回的响应中没有跨域需要的 header

    描述:本身跨域设置在后端,所以前端所有的请求都是跨域的,但是当我主动将Session失效,然后点击功能测试触发登录拦截,拦截器抛出Session失效异常,由全局异常处理捕捉并正常地响应json,此时响应头中就少了console中提示的项:

XMLHttpRequest cannot load http://localhost:8080/hello. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:8081' is therefore not allowed access.
//PS:查看network可以看到请求是200的,但是前端不能拿到响应

而后我是强行塞入指定响应头达到目的的(见后端拦截器),这样做不优雅,希望知道原因的小伙伴可以不吝指教下 XD..

拓展话题(链接坑待填)

  • cookie被清理,sessionID对应的session对象怎么回收?
    暴脾气用户禁掉浏览器cookie?
  • 前后端分离跨域请求相关
  • axios 辅助配置
  • 过滤器与拦截器

    过滤器是在servlet.service()请求前后拦截,springmvc拦截器则是在handle方法前后拦截,粒度不一样。
  • URL跳转路由拦截
  • 可以继续的主题:vuex状态管理,redis与session。

联系&交流

原文地址:https://www.cnblogs.com/noodlerkun/p/11094564.html

时间: 2024-11-07 20:36:40

springboot-vue前后端分离session过期重新登录的实现的相关文章

Springboot2 Vue 前后端分离 整合打包 docker镜像

项目使用springboot2和Vue前后端分离开发模式,再整合,容器化部署. 主要说明下大体的流程,扫除心里障碍,期间遇到的问题请自行解决. 首先说下Vue打包: 1.在Vue项目目录下运行命令打包:npm run build:prod --report 生成需要使用的dist文件,打包后会出现在项目目录下.(目录结构可能会不同) 按照如下方式整合到springboot项目中,resources在main目录下. (结构不同的话)一样拆到static目录下,static下面直接跟img.css

python 记录Django与Vue前后端分离项目搭建

python 记录Django与Vue前后端分离项目搭建 参考链接: https://blog.csdn.net/liuyukuan/article/details/70477095 1. 安装python与vue 2. 创建Django项目 django-admin startproject ulb_manager 3. 进入项目并创建名为backeng的app cd ulb_manager   python manage.py startapp backend 4. 使用vue-cli创建v

SpringBoot 和Vue前后端分离入门教程(附源码)

作者:梁小生0101 juejin.im/post/5c622fb5e51d457f9f2c2381 推荐阅读(点击即可跳转阅读) 1. SpringBoot内容聚合 2. 面试题内容聚合 3. 设计模式内容聚合 4. 排序算法内容聚合 5. 多线程内容聚合 前端工具和环境: Node.js V10.15.0 Vue.js V2.5.21 yarn: V1.13.0 IDE:VScode 后端工具和环境: Maven: 3.52 jdk: 1.8 MySql: 14.14 IDE: IDEA S

springboot shiro 前后端分离,解决跨域、过虑options请求、shiro管理session问题、模拟跨域请求

一.解决跨域.过虑options请求问题 1.创建过虑类 import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; im

springBoot 解决前后端分离项目中跨越请求,同源策略

今天在做项目的过程,采用前后端分离技术的时遇到采用ajax请求无法访问后台接口,按F12,查看浏览器运行状态时,报如下错误 为了解决浏览的同源策略,就必须了解什么是同源策略. 1.什么是同源策略 同源策略(Same origin policy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响.可以说Web是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现. 而所谓同源是指,域名,协议,端口相同.如静态资源所在的服务器和后端接口所在服

Spring Boot + Vue 前后端分离开发,权限管理的一点思路

在传统的前后端不分的开发中,权限管理主要通过过滤器或者拦截器来进行(权限管理框架本身也是通过过滤器来实现功能),如果用户不具备某一个角色或者某一个权限,则无法访问某一个页面. 但是在前后端分离中,页面的跳转统统交给前端去做,后端只提供数据,这种时候,权限管理不能再按照之前的思路来. 首先要明确一点,前端是展示给用户看的,所有的菜单显示或者隐藏目的不是为了实现权限管理,而是为了给用户一个良好的体验,不能依靠前端隐藏控件来实现权限管理,即数据安全不能依靠前端. 这点就像普通的表单提交一样,前端做数据

ASP.NET WebApi+Vue前后端分离之允许启用跨域请求

前言: 这段时间接手了一个新需求,将一个ASP.NET MVC项目改成前后端分离项目.前端使用Vue,后端则是使用ASP.NET WebApi.在搭建完成前后端框架后,进行接口测试时发现了一个前后端分离普遍存在的问题跨域(CORS)请求问题.因此就有了这篇文章如何启用ASP.NET WebApi 中的 CORS 支持. 一.解决Vue报错:OPTIONS 405 Method Not Allowed问题: 错误重现: index.umd.min.js:1 OPTIONS http://local

SpringBoot2.0.3 + SpringSecurity5.0.6 + vue 前后端分离认证授权

新项目引入安全控制 项目中新近添加了Spring Security安全组件,前期没怎么用过,加之新版本少有参考,踩坑四天,终完成初步解决方案.其实很简单,Spring Security5相比之前版本少了许多配置,操作起来更轻量 MariaDb登录配置加密策略 SpringSecurity5在执行登录认证时,需预设加密策略. 坑一:加密策略配置,验密始终不通过,报错401 坑二:本地重写的UserDetailsService实现类在注入的时候找不到,目前图省事直接用了 @Qualifier制定 其

SpringBootSecurity学习(12)前后端分离版之简单登录

前后端分离 前面讨论了springboot下security很多常用的功能,其它的功能建议参考官方文档学习.网页版登录的形式现在已经不是最流行的了,最流行的是前后端分离的登录方式,前端单独成为一个项目,与后台的交互,包括登录认证和授权都是由异步接口来实现.在前后端不分离的应用模式中,前端页面看到的效果都是由后端控制,由后端渲染页面或重定向,也就是后端需要控制前端的展示,前端与后端的耦合度很高.这种应用模式比较适合纯网页应用, 但是当后端对接App时,App可能并不需要后端返回一个HTML网页,而