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


這一篇博客來講解下babasport這個項目中使用的Login功能, 當然這里說的只是其中的一些簡單的部分, 記錄在此 方便以后查閱.


一: 去登錄頁面
首先我們登錄需要注意的事項是, 當用戶點擊登錄按鈕時,轉入登錄頁面時也要記住之前用戶是從哪個頁面發送請求過來的, 這樣登錄成功后還能繼續跳回到用戶之前瀏覽的那個頁面.
我們頁面展示顯示的登錄按鈕都是集成在一個common的jsp中, 前台每個頁面都是引用的這個jsp, 所以需要在這個common的jsp中直接添加點擊登錄按鈕跳轉的頁面.


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


二, 處理登錄操作
到了登錄界面后, 查看登陸界面圖, 這里的url參數是經過轉義的:


點擊登錄按鈕 會進入到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.



三: 驗證用戶是否登錄
首先看下沒有Login的時候最原始的頁面:


那么顯然這里就不對了, 如果沒有登錄, 那么就只應該顯示[登錄]和[免費注冊], 后面的[退出]和[我的訂單]就不應該顯示的, 那么怎么來驗證是否登錄呢?
這里頭部顯示的內容全都是引用的同一個common的jsp文件, 首先在頁面加載的時候我們應該判斷用戶是否登錄:
如果這里我們直接使用ajax異步去調用獲取用戶是否已經登錄, 這里dataType暫時使用json(jsonp是為了解決跨域問題)


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

這里提示不能夠跨域訪問? 那么該怎么去做呢?
上面的截圖已經給出了, 我們傳遞的dataType類型是jsonp, 就意味着我們這個ajax請求時跨域請求.

這里又引出一個新問題, 關於多服務器的問題, 如果用戶登錄時所處的服務器是Tomcat1, 那么登錄后當用戶再次訪問頁面時同樣會做登錄驗證, 這個時候如果是Tomcat2呢?
所以這里就引出了拋棄使用jesseionId的想法,具體的解決方法如圖:

我們自己創建一個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的共享.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM