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的「有效期」便是瀏覽器關閉

開發環境

  • JDK八、Maven3.5.三、springboot2.1.六、STS4
  • node10.1六、npm6.九、vue2.九、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,則不會採用後端配置方式,也就不會有這個問題。html

能夠繼續的話題(連接坑待填)

  • cookie被清理,sessionID對應的session對象怎麼回收?
    暴脾氣用戶禁掉瀏覽器cookie?
  • springboot-vue-nginx先後端分離跨域配置
  • axios 輔助配置
  • 過濾器與攔截器
    過濾器是在servlet.service()請求先後攔截,springmvc攔截器則是在handle方法先後攔截,粒度不同。
  • vue-URL跳轉路由攔截,vuex狀態管理
  • 集羣session與redis
相關文章
相關標籤/搜索