zoukankan      html  css  js  c++  java
  • demo之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失效拦截,由全局异常处理块返回的响应中却少了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
  • 相关阅读:
    你本地测试环境再难搭建也要搭建出来调试代码
    00 alv抬头等
    流水号生产后调用
    ALV 顶栏的按钮设定
    Redis学习总结
    Mybatis总结
    面试题收集
    java 收集2
    关于Spring的69个面试问答——终极列表
    java 面试收集
  • 原文地址:https://www.cnblogs.com/noodlerkun/p/11094564.html
Copyright © 2011-2022 走看看