前言
原來分享過一篇文章,Java自定義注解及應用,當時為了能突出重點,直接在url中傳了使用者的所屬角色,并寫了一般的做法。加上最近看了一些人的履歷,發現神奇的相似,都有類似商城的項目,為了不至于問些特别Low的問題,便總結了一下登入這個子產品所涉及的東西
單機Session
Http協定使用的是無狀态連接配接,這樣會造成什麼問題呢?看如下Demo
@RestController
public class LoginController {
@RequestMapping("login")
public String login(HttpServletRequest request, @RequestParam("username") String username) {
request.setAttribute("username", username);
return "success";
}
@RequestMapping("shoppingcar")
public String showProduct(HttpServletRequest request) {
String usename = (String)request.getAttribute("username");
return "username is " + usename;
}
}
複制
測試

HttpServletRequest對象代表用戶端的請求,當用戶端通過HTTP協定通路伺服器時,HTTP請求頭中的所有資訊都封裝在這個對象中,當在一個請求中時HttpServletRequest中的資訊可以共享,而在不同的請求中HttpServletRequest并不能共享,這樣就會造成使用者确實進行過登入操作,但是跳到購物車頁面時發現并沒有東西,因為應用并不知道通路這個頁面的使用者是誰
我們可以用一個HttpSession對象儲存跨多個請求的會話狀态,上面的例子就是儲存使用者名,看下圖了解為什麼HttpSession可以跨請求儲存狀态
對客戶的第一個請求,容器會生成一個唯一的會話ID,并通過響應把它傳回給客戶。客戶再在以後的每一個請求中發回這個會話ID。容器看到ID後,就會找到比對的會話,并把這個會話與請求關聯
将上面代碼改成如下,再測試
@RestController
public class LoginController {
@RequestMapping("login")
public String login(HttpSession session, @RequestParam("username") String username) {
session.setAttribute("username", username);
return "success";
}
@RequestMapping("shoppingcar")
public String showProduct(HttpSession session) {
String usename = (String)session.getAttribute("username");
return "username is " + usename;
}
}
複制
果然能儲存會話狀态了,客戶和容器如何交換會話ID資訊呢?其實是通過cookie實作的
看上面能儲存會話的代碼,我們并沒有對cookie進行操作啊,其實是容器幾乎會做cookie的所有工作,從最開始的Servlet開始講這些操作是如何實作的,先看一下Servlet執行過程
- 使用者點選頁面發送請求->Web伺服器應用(如Apache)->Web容器應用(如tomcat)
- 容器建立兩個對象HttpServletRequest和HttpServletResponse
- 根據URL找到servlet,并為請求建立或配置設定一個線程,将請求和響應對象傳遞給這個servlet線程
- 容器調用Servlet的service()方法,根據請求的不同類型,service()方法會調用doGet()和doPost()方法,假如請求是HTTP GET請求
- doGet()方法生成動态頁面,并把這個對象塞到響應對象裡。容器有響應對象的一個引用
- 線程結束,容器把響應對象裝換為一個HTTP請求,把它發回給客戶,然後删除請求和響應對象
容器使用部署描述檔案把URL映射到Servlet ,一個Servlet可以有3個名字,(1)使用者知道的URL名,(2)部署人員知道的内部名,(3)實際的檔案名
加入使用Spring MVC時要在web.xml中配置如下内容
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<servlet>
<!--部署人員知道的内部名字-->
<servlet-name>dispatcher</servlet-name>
<!--實際的檔案名字-->
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<!--使用者知道的URL名字,注意這裡有/-->
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
複制
根據url-pattern->servlet-name->servlet-class的三級映射關系,容器即可根據使用者輸入的URL找到對應的Servlet
從這個就可以看出其實Spring MVC架構其實在Servlet上面封裝了一層,當我們自己用Servlet編寫程式時,可以從HttpServletRequest中擷取HttpSession,如下
public class LoginServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
HttpSession session = req.getSession();
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
}
複制
在響應中發送一個會話cookie
HttpSession session = req.getSession();
複制
我們隻需要寫上述一行代碼即可,來看看容器幫我們做了哪些事情
- 建立一個新的HttpSession對象
- 生成唯一的會話ID
- 建立新的Cookie對象
- 把會話Id放到cookie中
- 在響應中設定cookie
從請求得到會話ID
HttpSession session = req.getSession();
複制
與響應生成會話ID和cookie時用的方法一樣
if (請求包含一個會話ID cookie) {
找到與該ID比對的會話
} else if (沒有會話Id cookie OR 沒有與此會話ID比對的目前會話) {
建立一個新會話
}
複制
如上面用的方法,我們并沒有直接從HttpServletRequest 中擷取HttpSession
public String login(HttpSession session, @RequestParam("username") String username)
複制
能直接擷取到HttpSession,其實是架構幫我們執行了HttpSession session = req.getSession(),然後設定進來的。我們可以設定session的過期時間,以保證使用者登入後長期不操作需要重新登入
分布式Session
當整個服務是分布式的該怎麼處理呢?使用者在伺服器A上登入,結果在伺服器B上檢視購物車資訊,因為在A上登入,HttpSession存在A伺服器上,當通路B伺服器上的購物車資訊因為擷取不到使用者登入的HttpSession,就會認為使用者沒有登入,這種情況該怎麼處理呢?
實作分布式Session有多種方式,這裡就介紹一下用Redis實作分布式Session,其實Spring Session項目就使用Redis實作Session共享的
了解了單機Session,分布式Session也不難了解,主要步驟如下
- 使用者登入以後,先生成類似于sessionId的唯一辨別,我們把它叫token
- new一個cookie,将token寫到cookie當中傳遞到用戶端,并将以key=token,value=使用者資訊的hash放到redis中,當然cookie和這個hash都可以設定過期時間
- 用戶端在随後的通路中伺服器從cookie中拿到這個token,根據這個token去Redis中取到使用者資訊
當使用者登出時隻要删除key為token的hash,并且将cookie的最長時間設定為0,重新放回HttpServletResponse即可,鑒于篇幅限制,就不寫具體代碼了
為什麼要在密碼中加鹽
直接存儲
以前系統存儲密碼時都是類似如下形式
假如使用者資訊洩露,使用者的賬号安全将受到威脅,參考CSDN密碼洩露事件
加密存儲
既然明文存儲會有安全問題,那就加密存儲,一般常用的加密算法是MD5和SHA,當使用者注冊時,資料庫中儲存的密碼是加密後的密碼,當使用者登入時先對登入的密碼進行MD5,然後和資料庫中的密碼比對,正确則登入成功,失敗則登入失敗
以為這樣就足夠安全了?其實遠遠不夠,有的人将各種密碼的MD5值都算出來,做成一個字典,前面說的洩露的CSDN的密碼就是一個很好的素材,這樣就可以通過
洩露密碼的MD5值->MD5字典->原始的字元串的映射關系,得到洩露的密碼,針對這種情況,有2種做法,一種是将密碼多次進行MD5,即對加密後的MD5值再次進行MD5,另一種就是加鹽
加鹽存儲
由于鹽值時随機生成的,我們算一下破解一個使用者的密碼需要多長時間,假如資料庫中密碼是如此生成的MD5(明文密碼+Salt),MD5的方式也被壞人知道了,假如壞人有600w個字典,得先對這些字典加Salt做一次MD5再比對,而且還有可能比對不出來,破解一個賬号的成本就這麼高,而且鹽值和密碼的方式進行MD5的方式也多種多樣啊,Salt可以插中間,Salt倒序再進行MD5。當然還可以這樣啊MD5(Salt[0] + 明文密碼 + Salt[5])。如果還覺得不夠安全,還可以對加鹽生成的MD5值再次MD5啊,次數由你定,這樣幾乎是破解不了