前後端分離後產生的跨域問題sessionid丟失,cookies無法寫入等

前言

現在大部分項目都採用的前後端分離,比哪後臺用spring boot ,前端用vue等。

一、會話機制

session和cookies常用來會話保持。

1. 何爲一次會話,會話從什麼時候開始,從什麼時候結束?

一次會話是指: 好比打電話,當A打給B,電話接通了 會話開始,持斷會話結束。 瀏覽器訪問服務器,就如同打電話,瀏覽器A給服務器發送請求,訪問web程序,該次會話就開始,其中不管瀏覽器發送了多少請求 ,都爲一次會話,直到瀏覽器關閉,本次會話結束。

2.cookies如何保持會話,它的工作流程?

工作流程:

  1. servlet創建cookie,保存少量數據,發送瀏覽器。
  2. 瀏覽器獲得服務器發送的cookie數據,將自動的保存到瀏覽器端。
  3. 下次訪問時,瀏覽器將自動攜帶cookie數據發送給服務器。

在這裏插入圖片描述

3、session原理分析:

工作流程:
1、首先瀏覽器請求服務器訪問web站點時,程序需要爲客戶端的請求創建一個session的時候,服務器首先會檢查這個客戶端請求是否已經包含了一個session標識、稱爲SESSIONID
2、如果已經包含了一個sessionid則說明以前已經爲此客戶端創建過session,服務器就按照sessionid把這個session檢索出來使用
3、如果客戶端請求不包含session id,則服務器爲此客戶端創建一個session並且生成一個與此session相關聯的session id,sessionid 的值應該是一個既不會重複,又不容易被找到規律以仿造的字符串
4、這個sessionid將在本次響應中返回到客戶端保存,保存這個sessionid的方式就可以是cookie,這樣在交互的過程中,瀏覽器可以自動的按照規則把這個標識發回給服務器,服務器根據這個sessionid就可以找得到對應的session,又回到了這段文字的開始

在這裏插入圖片描述

實例記錄sessionid變化(前後端不分離網站,同一個域名不存在跨域問題)

1、第一次訪問 http://127.0.0.1:8085/login 登錄頁面
在這裏插入圖片描述
2、後臺獲取sessionid,信息

sessionId:8038E64DE4036536341C7EB784AC1AA7,getLastAccessedTime:2020-06-09,getMaxInactiveInterval:1800

3、刷新一下 http://127.0.0.1:8085/login 這個接口
在這裏插入圖片描述
4、後臺打印sessionid信息;

sessionId:8038E64DE4036536341C7EB784AC1AA7,getLastAccessedTime:2020-06-09,getMaxInactiveInterval:1800

5、登錄後sessionid也是同一個
在這裏插入圖片描述
6、後臺打印也是同一個

sessionId:8038E64DE4036536341C7EB784AC1AA7,getLastAccessedTime:2020-06-09,getMaxInactiveInterval:1800

4、session的生命週期

常常聽到這樣一種誤解“只要關閉瀏覽器,session就消失了”。其實可以想象一下會員卡的例子,除非顧客主動對店家提出銷卡,否則店家絕對不會輕易刪除顧客的資料。對session來說也是一樣的,除非程序通知服務器刪除一個session,否則服務器會一直保留。
所以瀏覽器從來不會主動在關閉之前通知服務器它將要關閉,因此服務器根本不會有機會知道瀏覽器已經關閉,之所以會有這種錯覺,是大部分session機制都使用會話cookie來保存session id,而關閉瀏覽器後這個session id就消失了,再次連接服務器時也就無法找到原來的session
 恰恰是由於關閉瀏覽器不會導致session被刪除,迫使服務器爲seesion設置了一個失效時間,一般是30分鐘,當距離客戶端上一次使用session的時間超過這個失效時間時,服務器就可以認爲客戶端已經停止了活動,纔會把session刪除以節省存儲空間

5、控制session有效時間

  • session.invalidate()將session對象銷燬
  • setMaxInactiveInterval(int interval) 設置有效時間,單位秒
  • 在web.xml中配置session的有效時間
 <session-config>
       <session-timeout>30</session-timeout>   單位:分鐘
 <session-config>

6、 session的生命週期就是

創建:第一次調用getSession()
銷燬:1、超時,默認30分鐘
2、執行api:session.invalidate()將session對象銷燬、setMaxInactiveInterval(int interval) 設置有效時間,單位秒
3、服務器非正常關閉
自殺,直接將JVM馬上關閉
如果正常關閉,session就會被持久化(寫入到文件中,因爲session默認的超時時間爲30分鐘,正常關閉後,就會將session持久化,等30分鐘後,就會被刪除)
位置: D:\java\tomcat\apache-tomcat-7.0.53\work\Catalina\localhost\test01\SESSIONS.ser

7、session id的URL重寫

當瀏覽器將cookie禁用,基於cookie的session將不能正常工作,每次使用request.getSession() 都將創建一個新的session。達不到session共享數據的目的,但是我們知道原理,只需要將session id 傳遞給服務器session就可以正常工作的。

解決:通過URL將session id 傳遞給服務器:URL重寫

  • 手動方式: url;jsessionid=…
  • api方式:
    encodeURL(java.lang.String url) 進行所有URL重寫
    encodeRedirectURL(java.lang.String url) 進行重定向 URL重寫

如果瀏覽器禁用cooke,api將自動追加session id ,如果沒有禁用,api將不進行任何修改。

8、小結

8.1、cookie工作原理,

可以看上面講解cookie的那張圖,cookie是由服務器端創建發送回瀏覽器端的,並且每次請求服務器都會將cookie帶過去,以便服務器知道該用戶是哪一個。其cookie中是使用鍵值對來存儲信息的,並且一個cookie只能存儲一個鍵值對。所以在獲取cookie時,是會獲取到所有的cookie,然後從其中遍歷。

8.2、session的工作原理

session的工作原理就是依靠cookie來做支撐,第一次使用request.getSession()時session被創建,並且會爲該session創建一個獨一無二的sessionid存放到cookie中,然後發送會瀏覽器端,瀏覽器端每次請求時,都會帶着這個sessionid,服務器就會認識該sessionid,知道了sessionid就找得到哪個session。以此來達到共享數據的目的。 這裏需要注意的是,session不會隨着瀏覽器的關閉而死亡,而是等待超時時間。

8.3 session與cookies的聯繫與區別

cookie機制採用的是在客戶端保持狀態的方案
session機制採用的是在服務器端保持狀態的方案,同進session機制可能需要藉助於cookie機制來達到保存標識的目的,session在保存一個sesionid在cookie中。

以上都是傳統的項目,比如前後端不分離,前端和後端在同一個域名下的情況

二、cookies的同源策略,導致cookes跨域寫入失敗的原因

1.協議相同

2.域名相同

3.端口相同

cookes跨域寫入失敗

當後端項目向瀏覽器寫入cookies時,後端項目協議、域名、端口必須相同時才能寫到瀏覽器
比如我訪問一個地址爲 http://test.clock.bone:8080的頁面地址,這個頁面地址 請求一個後端服務如:http://test.clock.bone:8080/getinfo ,這個接口向瀏覽器寫入了cookie 。 只有當瀏覽器地址和協議、域名、端口和請求的後端服務的協議、域名、端口一致時,這個cookie才能寫成功,即使其它都 一樣 ,但端口不一樣也不會成功。
所以如果 瀏覽器訪問的是 http://test.clock.bone:8080,這個頁面請求了後端服務http://test.clock.bone:8081/getinfo 寫入了cookies也不會成功的原因。

session跨域每次獲取sessionid不一樣

我們知道session也依賴於cookie,當服務端創建了sessionid 要寫入瀏覽器cookies時,如果不同源,那麼sessionid會寫入失敗,下次請求時 瀏覽器無法攜帶session,服務端沒有獲取到sessionid ,於是又會重新創建一個sessionId,這就是爲什麼跨域請求 每次得到的sessionid不一致的原因。

三、多服務器共享session

再回顧一下 ,服務端創建session的過程:
1、瀏覽器請求服務器
2、服務端getsession,檢查瀏覽器是否攜帶sessionid,
如果有sessionid (我們知道這些屬性是存儲在每個服務端的文件中的) ,證明用戶已經訪問過
如果沒有sessionid,那麼會創建一個新的,證明瀏覽器是第一次訪問
3、我們通常通過sessionid來保存用戶登錄信息,根據sessionid 能取到用戶信息 那麼登錄了,如果沒有取到就沒有登錄

這時一個網站部署了多臺服務器,多臺服務配置映射同一個域名,
瀏覽器隨機訪問服務A,是第一次訪問,沒有攜帶了sessionId, 於是服務器創建了一個sessionid,根據sessionid 獲取用戶信息,發現沒有取到 於是要求用戶登錄,
用戶登錄後 通過getsession.setAttribute(“user”,userinfo)將用戶信息寫到服務器session。
用戶 繼續訪問網站,此時 隨機跳轉到了服務B,
此時瀏覽器有sessionid,服務B不會再新創建sessionid,於是通過getsession.getattrite(“user”)查找用戶信息,沒有找到,因爲此時session是存儲在服務器A上的,在服務器B上找 肯定找不到。於是認爲用戶沒有登錄,又要求用戶去登錄。但我分明已經登錄過了。於是就出現了session不共享的問題。

解決session的共享的方案通常是把這個sessionid存儲到第三方存儲系統比如redis。
可以引用spring-session-redis,這個依賴。在創建了sessionid後,會把session存到redis中。當我們getsessionId,框架自動會先去redis中查找sessionid,這樣就實現了多服務session共享了。

當然你可以簡單配置一個策略:就是相同的ip 一直訪問同一個後端服務器,這個session不用存儲在第三方redis中 ,也能保證session不丟失。

在這裏插入圖片描述

將session存儲到redis

在這裏插入圖片描述

四、如何解決跨域問題

3.1、nginx

後端spring boot
前端vue
nginx部署

upstream clock-server {
    //後端服務
    server 10.2.22.45:8098;
}
server {
    listen 80;
    server_name  clock.bone.com;
   
     location  =/ {
	  add_header Access-Control-Allow-Origin *;
          add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
          add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
          //允許攜帶cookie
          proxy_set_header   Cookie $http_cookie; 
          //vue 打包後的路徑
          root   /data/vue/;
        }
        
   location ~ .*\.(css|js|ico|gif|jpg|jpeg|png|bmp|swf|ico|html|htm)$
    {
    root  /data/vue/;  
    }
    //以server開頭的服務都會轉發到後端服務
   location ~^/server/ {
       //攜帶cookes
        proxy_set_header   Cookie $http_cookie;
        //轉發到後端服務上
        proxy_pass   http://clock-server;
   }
}

現在所有前端項目訪問後端服務不用指向ip,直接用 http://clock.bone.com/server/getinfo 就可以了。 寫cookies正常。

3.2、前後端允許跨域訪問,並攜帶cookie

後端:

官網

@Override
public void addCorsMappings(CorsRegistry registry) {
	registry.addMapping("/api/**")
		.allowedOrigins("http://domain2.com")
		.allowedMethods("PUT", "DELETE")
			.allowedHeaders("header1", "header2", "header3")
		.exposedHeaders("header1", "header2")
		.allowCredentials(true);
}

前端:

設置{‘withCredentials’:true}

五、題外

這次cookes問題主要是由於前後端分離後圖片驗證碼 校驗問題來的

5.1、流程

流程是這樣的:要做一個用戶登錄的接口。在登錄頁面,前端先請求圖片驗證碼,然後輸入用戶名密碼和驗證碼之後,請求登錄接口。
這裏存在兩個接口,驗證碼接口和登錄接口。在驗證碼接口中我用session保存驗證碼,在登錄接口中我從session取出驗證碼進行校驗。
或者用cookies保存一個verifyid, 根據這個verifyid 去redis中獲取驗證碼

5.2、代碼session實現

@RequestMapping("/getverifyCode")
    public void getverifyCode(HttpServletRequest request,
                             HttpServletResponse response) throws IOException {
        response.setDateHeader("Expires", 0);
        response.setHeader("Cache-Control",
                "no-store, no-cache, must-revalidate");
        response.addHeader("Cache-Control", "post-check=0, pre-check=0");
        response.setHeader("Pragma", "no-cache");
        response.setContentType("image/jpeg");

        String capText = captchaProducer.createText();
        request.getSession().setAttribute(Constants.KAPTCHA_SESSION_KEY,capText);
        logger.info("code is "+capText+" session id is "+request.getSession().getId());
        BufferedImage bi = captchaProducer.createImage(capText);
        ServletOutputStream out = response.getOutputStream();
        ImageIO.write(bi, "jpg", out);
        try {
            out.flush();
        } finally {
            out.close();
        }
    }
    @RequestMapping(value = "/login",method = RequestMethod.POST)
    public Response login(HttpServletRequest request){
        String userName = request.getParameter("userName");
        String password = request.getParameter("password");
        String verifyCode= request.getParameter("verifyCode");
        String sessionCode = (String) request.getSession().getAttribute(Constants.KAPTCHA_SESSION_KEY);
        logger.info("input code is "+verifyCode+" session id is "+request.getSession().getId());
        if(StringUtils.isEmpty(verifyCode)){
            Response.setMsg("驗證碼不能爲空");
            return Response;
        }
        if(!verifyCode.equals(sessionCode)){
            Response.setMsg("驗證碼不能爲空");
            return Response;
        }
        try {
            User user = userService.checkLogin(userName, password);
            if (user == null) {
                Response.setMsg("用戶不存在");
            return Response;
            }
            Response.setMsg("登錄成功");
            Response.setData(user);
            request.getSession().setAttribute("user",user);
        }catch (GeneralException g){
            g.printStackTrace();
        }catch (Exception e){
            e.printStackTrace();
        }
        return Response;
    }

5.3、通過cookie,redis實現

@PostMapping("getVerifyCode")
    @ResponseBody
    public ResponseEntity getverifyCode(HttpServletResponse response,HttpServletRequest request) throws IOException{
        String verifyCode = VerifyCodeUtils.generateVerifyCode(4);
        String verifyId = UUID.randomUUID().toString();
        CookieUtil.addCookie(response, RedisKeyEnum.COOKIE_KEY_VERIFY, verifyId);
        //存到redis中
        cacheService.set(verifyId, verifyCode, 120);
        String base64 = VerifyCodeUtils.outputImageAsBase64(100, 38, verifyCode);
        return ResponseEntity.ok(base64);

    }


@PostMapping("login")
@ResponseBody
@ApiOperation(notes = "登錄", value = "登錄")
public ResponseEntity login(HttpServletRequest request, HttpServletResponse response
, @Validated String userName,String pwd ,String verifyCode, 
,@CookieValue(value = RedisKeyEnum.COOKIE_KEY_VERIFY) String verifyId                  
) throws IOException {
 //如果從cookeis獲取不到verifyId,那麼會報錯,根據verifyid 從redis中獲取生成的verifycode
String ckCode = cacheService.getAndDel(verifyId), StringUtils.EMPTY);
if (StringUtils.isEmpty(dto.getVerify()) || StringUtils.isEmpty(ckCode) || !ckCode.equalsIgnoreCase(dto.getVerify())) {
    return ResponseEntity.errorMsg("驗證碼輸入錯誤或已失效").build();
}
return Respoinse.ok();

這種方法在跨域的情況下都無法實現,因爲sessionId也用到了cookies。

需要解決跨域的問題

參考:
https://www.cnblogs.com/whgk/p/6422391.html
https://blog.csdn.net/zhaoenweiex/article/details/77814918

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章