springboot-vue前后端分离session过期重新登录
简单回顾cookie和session
- cookie和session都是回话管理的方式
- Cookie
- cookie是浏览器端存储信息的一种方式
- 服务端可以通过响应浏览器set-cookie标头(header),浏览器接收到这个标头信息后,将以文件形式将cookie信息保存在浏览器客户端的计算机上。之后的请求,浏览器将该域的cookie信息再一并发送给服务端
- cookie默认的存活期限关闭浏览器后失效,即浏览器在关闭时清除cookie文件信息。我们可以在服务端响应cookie时,设置其存活期限,比如设为一周,这样关闭浏览器后也cookie还在期限内没有被清除,下次请求浏览器就会将其发送给服务端了
- 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失效拦截,由全局异常处理块返回的响应中却少了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..
@20190808 更新
真正上线,代理转发交给nginx,则不会采用后端配置方式,也就不会有这个问题。
可以继续的话题(链接坑待填)
- cookie被清理,sessionID对应的session对象怎么回收?
暴脾气用户禁掉浏览器cookie? - springboot-vue-nginx前后端分离跨域配置
- axios 辅助配置
- 过滤器与拦截器
过滤器是在servlet.service()请求前后拦截,springmvc拦截器则是在handle方法前后拦截,粒度不一样。 - vue-URL跳转路由拦截,vuex状态管理
- 集群session与redis
springboot-vue前后端分离session过期重新登录的更多相关文章
- SpringBoot+Vue前后端分离,使用SpringSecurity完美处理权限问题
原文链接:https://segmentfault.com/a/1190000012879279 当前后端分离时,权限问题的处理也和我们传统的处理方式有一点差异.笔者前几天刚好在负责一个项目的权限管理 ...
- Springboot+vue前后端分离项目,poi导出excel提供用户下载的解决方案
因为我们做的是前后端分离项目 无法采用response.write直接将文件流写出 我们采用阿里云oss 进行保存 再返回的结果对象里面保存我们的文件地址 废话不多说,上代码 Springboot 第 ...
- SpringBoot,Vue前后端分离开发首秀
需求:读取数据库的数据展现到前端页面 技术栈:后端有主要有SpringBoot,lombok,SpringData JPA,Swagger,跨域,前端有Vue和axios 不了解这些技术的可以去入门一 ...
- SpringBoot+Vue前后端分离项目,maven package自动打包整合
起因:看过Dubbo管控台的都知道,人家是个前后端分离的项目,可是一条打包命令能让两个项目整合在一起,我早想这样玩玩了. 1. 建立个maven父项目 next 这个作为父工程,next Finish ...
- Jeecg-Boot 2.0 版本发布,基于Springboot+Vue 前后端分离快速开发平台
目录 Jeecg-Boot项目简介 源码下载 升级日志 Issues解决 v1.1升级到v2.0不兼容地方 系统截图 Jeecg-Boot项目简介 Jeecg-boot 是一款基于代码生成器的智能开发 ...
- SpringBoot +Vue 前后端分离实例
今天下了Vue,想试一试前后端分离的实现,没想到坑还不少,这里就记录一下我遇到的坑和我的代码: 一.Vue的下载安装:从网上找就好了,没什么问题,除了下载以后,要把镜像库改成淘宝的,要不然太慢了. 二 ...
- SpringBoot+Vue前后端分离,使用SpringSecurity完美处理权限问题(一)
当前后端分离时,权限问题的处理也和我们传统的处理方式有一点差异. 笔者前几天刚好在负责一个项目的权限管理模块,现在权限管理模块已经做完了,我想通过5-6篇文章,来介绍一下项目中遇到的问题以及我的解决方 ...
- springboot+vue前后端分离,nginx代理配置 tomcat 部署war包详细配置
1.做一个小系统,使用了springboot+vue 基础框架参考这哥们的,直接拿过来用,链接https://github.com/smallsnail-wh/interest 前期的开发环境搭建就不 ...
- Springboot+Vue前后端分离的博客项目
项目介绍 演示站(服务器已过期):http://blog.hanzhe.site 开源项目地址 ( 求给个Star ):https://gitee.com/zhang_hanzhe/blog 前端采用 ...
随机推荐
- Flume+Kafka收集Docker容器内分布式日志应用实践
1 背景和问题 随着云计算.PaaS平台的普及,虚拟化.容器化等技术的应用,例如Docker等技术,越来越多的服务会部署在云端.通常,我们需要需要获取日志,来进行监控.分析.预测.统计等工作,但是云端 ...
- JS面向对象编程(一):封装
js是一门基于面向对象编程的语言. 如果我们要把(属性)和(方法)封装成一个对象,甚至要从原型对象生成一个实例,我们应该怎么做呢? 一.生成对象的原始模式 假定把猫看 ...
- Flutter学习笔记(13)--表单组件
如需转载,请注明出处:Flutter学习笔记(13)--表单组件 表单组件是个包含表单元素的区域,表单元素允许用户输入内容,比如:文本区域,下拉表单,单选框.复选框等,常见的应用场景有:登陆.注册.输 ...
- Mysql之锁、事务绝版详解---干货!
一 锁的分类及特性 数据库锁定机制简单来说,就是数据库为了保证数据的一致性,而使各种共享资源在被并发访问变得有序所设计的一种规则.对于任何一种数据库来说都需要有相应的锁定机制,所以MySQL自然也不能 ...
- 使用用树莓派打造远程WEB服务器
简介:系统配置Raspberry Pi 3B + Raspbian + MySQL5.7 + Tomcat 9 + Nginx + 公网IP. 工具:Win32DiskImager .FileZill ...
- spring学习笔记之---bean属性注入
bean属性注入 (一)构造方法的属性注入 1.Student.java package entity; public class Student { private String name; pri ...
- Spring Cloud微服务接口这么多怎么调试
导读 我们知道在微服务架构下,软件系统会被拆分成很多个独立运行的服务,而这些服务间需要交互通信,就需要定义各种各样的服务接口.具体来说,在基于Spring Cloud的微服务模式中,各个微服务会基于S ...
- C# 在某个线程上创建的控件不能成为在另一个线程上创建的控件的父级
//使用委托进行修改UI界面 if (this.InvokeRequired) { this.Invoke(new MethodInvoker(delegate { createList(); })) ...
- 一起来学JavaScript吧(JS兔子领进门)
首先我们学习一门语言呢不一要学习它的所有历史,但是一定要知道它的使用基本规则.不要在最基础的部分出错.不过胡萝贝还是带你了解JavaScript的历史吧. 1994年网景公司(Netscape)发布了 ...
- java并发编程(十四)----(JUC原子类)对象的属性修改类型介绍
今天我们介绍原子类的最后一个类型--对象的属性修改类型: AtomicIntegerFieldUpdater,AtomicLongFieldUpdater,AtomicReferenceFieldUp ...