天天看點

叢集下session共享問題的解決方案.

這一篇部落格來講解下babasport這個項目中使用的Login功能, 當然這裡說的隻是其中的一些簡單的部分, 記錄在此 友善以後查閱.

一: 去登入頁面

首先我們登入需要注意的事項是, 當使用者點選登入按鈕時,轉入登入頁面時也要記住之前使用者是從哪個頁面發送請求過來的, 這樣登入成功後還能繼續跳回到使用者之前浏覽的那個頁面.

我們頁面展示顯示的登入按鈕都是內建在一個common的jsp中, 前台每個頁面都是引用的這個jsp, 是以需要在這個common的jsp中直接添加點選登入按鈕跳轉的頁面.

叢集下session共享問題的解決方案.
叢集下session共享問題的解決方案.

這裡點選登入按鈕後 就會使用window.location.href="http://localhost:8081/login.aspx?returnUrl="+encodeURIComponent(window.location.href);跳轉到新的頁面, 且這裡傳入的參數 是浏覽器的url, 這個就是為了登入成功後 還能繼續跳轉到這個頁面來. 而encodeURIComponent是 js自帶的轉義類, 轉義的好處是能夠在url中帶中文重定向後無法接收 且url帶多參數解決&被轉義而無效的情況.

下圖就是跳轉到login頁面前的window.location.href屬性:

叢集下session共享問題的解決方案.

二, 處理登入操作

到了登入界面後, 檢視登陸界面圖, 這裡的url參數是經過轉義的:

叢集下session共享問題的解決方案.

點選登入按鈕 會進入到LoginController.java中:

1 //去登入頁面
 2     @RequestMapping(value="/login.aspx",method=RequestMethod.GET)
 3     public String login(){
 4         return "login";
 5     }
 6     
 7     @Autowired
 8     private BuyerService buyerService;
 9     
10     @Autowired
11     private SessionProviderService sessionProviderService;
12     //執行登入操作
13     @RequestMapping(value="/login.aspx",method=RequestMethod.POST)
14     public String login(String username, String password, String returnUrl,Model model,
15             HttpServletRequest request, HttpServletResponse response){
16         //1: 判斷使用者名不能為空
17         if(null != username){
18             //2:判斷密碼不能為空
19             if (null != password){
20                 //3:使用者名必須正确
21                 Buyer buyer = buyerService.selectBuyerByusername(username);
22                 if(buyer != null){
23                     //4:密碼必須正确
24                     if(encodePassword(password).equals(buyer.getPassword())){
25                         //5:設定使用者到Session
26                         sessionProviderService.setAttributerForUsername(RequestUtils.getCSessionId(request, response), buyer.getUsername());
27                         //6:回跳之前通路頁面
28                         if(null != returnUrl){
29                             return "redirect:"+returnUrl;
30                         }else{
31                             return "redirect:http://localhost:8082/";
32                         }
33                     }else {
34                         model.addAttribute("error", "密碼輸入錯誤!");
35                     }
36                 }else {
37                     model.addAttribute("error", "使用者名輸入錯誤!");
38                 }
39                 
40             }else {
41                 model.addAttribute("error", "密碼不能為空!");
42             }
43         }else {
44             model.addAttribute("error", "使用者名不能為空!");
45         }
46         
47         
48         return "login";
49     }
50     
51     //加密
52     public String encodePassword(String password){
53         
54         String algorithm = "MD5";
55         char[] encodeHex = null;
56         //MD5
57         try {
58             MessageDigest instance = MessageDigest.getInstance(algorithm);
59             byte[] digest = instance.digest(password.getBytes());
60             
61             //十六進制, 在MD5加密的基礎上再次加密
62             encodeHex = Hex.encodeHex(digest);
63         } catch (NoSuchAlgorithmException e) {
64             // TODO Auto-generated catch block
65             e.printStackTrace();
66         }
67         
68         return new String(encodeHex);
69     }           

複制

這裡使用了MD5加密, 經過MD5加密後在使用十六進制進行加密.

如果登陸成功, 調用sessionProviderService.setAttributerForUsername(RequestUtils.getCSessionId(request, response), buyer.getUsername());

SessionProviderImpl.java:

1 //session存活時間, 機關是分鐘.
 2     private Integer exp = 30;
 3     public void setExp(Integer exp) {
 4         this.exp = exp;
 5     }
 6 
 7 
 8     @Autowired
 9     private Jedis jedis;
10     //儲存使用者到redis中  注冊: 儲存使用者到mysql的同時儲存使用者名作為Key 使用者Id當做value 到redis中
11     //jessionId  value==使用者名
12     public void setAttributerForUsername(String jessionId, String value){
13         jedis.set(jessionId + ":USER_NAME", value);
14         jedis.expire(jessionId + ":USER_NAME", 60*exp);
15     }           

複制

将username資訊儲存到Redis中, key是CSessionId:USER_NAME, value是username.

叢集下session共享問題的解決方案.

三: 驗證使用者是否登入

首先看下沒有Login的時候最原始的頁面:

叢集下session共享問題的解決方案.

那麼顯然這裡就不對了, 如果沒有登入, 那麼就隻應該顯示[登入]和[免費注冊], 後面的[退出]和[我的訂單]就不應該顯示的, 那麼怎麼來驗證是否登入呢?

這裡頭部顯示的内容全都是引用的同一個common的jsp檔案, 首先在頁面加載的時候我們應該判斷使用者是否登入:

如果這裡我們直接使用ajax異步去調用擷取使用者是否已經登入, 這裡dataType暫時使用json(jsonp是為了解決跨域問題)

叢集下session共享問題的解決方案.
叢集下session共享問題的解決方案.

如果我們代碼中也是這樣改動的, 那麼會發生什麼事情呢?

叢集下session共享問題的解決方案.

這裡提示不能夠跨域通路? 那麼該怎麼去做呢?

上面的截圖已經給出了, 我們傳遞的dataType類型是jsonp, 就意味着我們這個ajax請求時跨域請求.

這裡又引出一個新問題, 關于多伺服器的問題, 如果使用者登入時所處的伺服器是Tomcat1, 那麼登入後當使用者再次通路頁面時同樣會做登入驗證, 這個時候如果是Tomcat2呢?

是以這裡就引出了抛棄使用jesseionId的想法,具體的解決方法如圖:

叢集下session共享問題的解決方案.

我們自己建立一個CsessionId, 當使用者第一次通路時, CsessionId為空, 那麼 在Tomcat1總建立一個CsessionId, 并且将此CsessionId儲存到Redis伺服器中, 且傳回給浏覽器.

當使用者第二次通路, 且由Tomcat2 負責處理時, Tomcat2 通過CsessionId去Redis伺服器中查找已存在, 然後就知道了此使用者已經登入.

下面就看看對于這個CsessionId是如何操作的:

跨域請求後, isLogin接收的參數有一個callBack屬性, 如果是跨域請求, 那麼這個參數就會有值.

1 //是否登入
 2     @RequestMapping(value="/isLogin.aspx")
 3     public @ResponseBody MappingJacksonValue isLogin(String callback, HttpServletRequest request, HttpServletResponse response) throws IOException{
 4         Integer result = 0;
 5         //判斷使用者是否登入
 6         String username = sessionProviderService.getAttributterForUsername(RequestUtils.getCSessionId(request, response));
 7         if (null != username) {
 8             result = 1;
 9         }
10         
11         //傳回<script> 類, 這個類支援跨域請求
12         MappingJacksonValue mjv = new MappingJacksonValue(result);
13         //設定jsonpFunction
14         mjv.setJsonpFunction(callback);
15         return mjv;
16     }           

複制

這個地方 是先通過RequestUtils擷取CSessionId, 然後再通過CSessionId去擷取到對應的username.

RequestUtils.java:

1 public class RequestUtils {
 2 
 3     //擷取CSessionID
 4     public static String getCSessionId(HttpServletRequest request, HttpServletResponse response){ 6         //1, 從Request中取Cookie
 7         Cookie[] cookies = request.getCookies();
 8         //2, 從Cookie資料中周遊查找, 并取CSessionID
 9         if (null != cookies && cookies.length > 0) {
10             for (Cookie cookie : cookies) {
11                 if ("CSESSIONID".equals(cookie.getName())) {
12                     //有, 直接傳回
13                     return cookie.getValue();
14                 }
15             }
16         }
17         //沒有, 建立一個CSessionId, 并且放到Cookie再傳回浏覽器.傳回新的CSessionID
18         String csessionid = UUID.randomUUID().toString().replaceAll("-", "");
19         //并且放到Cookie中
20         Cookie cookie = new Cookie("CSESSIONID", csessionid);
21         //cookie  每次都帶來, 設定路徑
22         cookie.setPath("/");
23         //0:關閉浏覽器  銷毀cookie. 0:立即消失.  >0 存活時間,秒
24         cookie.setMaxAge(-1);
25         
26         return csessionid;
27     }
28 }           

複制

先檢視cookies中是否儲存的有CSessionId, 如果沒有就新建立一個, 且儲存到Cookies中.

SessionProviderImpl.java:

1 public class SessionProviderImpl implements SessionProviderService {
 2 
 3     //session存活時間, 機關是分鐘.
 4     private Integer exp = 30;
 5     public void setExp(Integer exp) {
 6         this.exp = exp;
 7     }
 8 
 9 
10     @Autowired
11     private Jedis jedis;
12     //儲存使用者到redis中  注冊: 儲存使用者到mysql的同時儲存使用者名作為Key 使用者Id當做value 到redis中
13     //jessionId  value==使用者名
14     public void setAttributerForUsername(String jessionId, String value){
15         jedis.set(jessionId + ":USER_NAME", value);
16         jedis.expire(jessionId + ":USER_NAME", 60*exp);
17     }
18     
19     
20     //擷取
21     public String getAttributterForUsername(String jessionId){
22         String value = jedis.get(jessionId + ":USER_NAME");
23         if(null != value){
24             //計算session過期時間是 使用者最後一次請求開始計時.
25             jedis.expire(jessionId + ":USER_NAME", 60*exp);
26             return value;
27         }
28         return null;
29     }
30 }           

複制

這裡的getAttributterForUsername 是通過傳遞進來的CSessionId 去Redis伺服器中查找 相應的結果, 如果已經儲存了這個 CSessionId, 那麼就傳回username.

如果已經登陸, 那麼就傳回1, 在ajax請求的success中再進行相應的處理.

關于登陸的再來梳理一下:

已經登陸, 校驗是否登陸

登陸成功: 會将一個CSessionId儲存到Redis中, Redis中設定的這個CSessionId的過期時間為60分鐘.

CSessionId是儲存在Cookies中的, 如果Cookies中沒有這個CSessionId則建立一個傳回.Cookies中的CSessionId的過期時間也是60分鐘.

校驗是否登入:通過ajax發送跨域請求, 此時因為已經登陸成功, 是以Cookies中存在這個CSessionId. 然後通過這個CSessionId我們可以在Redis伺服器中查出對應的username. 然後Controller将設定一個flag為1, 在ajax中接收到這個flag , 就可以根據判斷來做出相應的處理.

關于Login就這麼多, 當然這裡的權限驗證遠遠不夠, 而且這裡也省略的注冊的内容, 大緻需要注意的就是這麼多, 其中最 關鍵的就是CSession的使用, 這個可以解決多伺服器直接session的共享.