詳細解析Cookie\Session\Token\Redis原理

1. Cookie和Session

1.1 Http是無狀態的,有會話的

瀏覽器訪問服務器時,瀏覽器和服務器之間就建立了連接,連接後瀏覽器可以向服務器發送多次請求,並且這多次請求之間是沒有任何關係的,從服務器的角度而言,即便同一個瀏覽器像我發送了多次請求,我也不認爲這些請求之間有什麼關係,仍然把每次請求當做陌生人倆對待。他不對之前的請求和響應狀態進行管理,無法根據之前的狀態對本次請求進行處理。

1.2 Cookie會話機制

Cookie是由服務器創建發送到用戶瀏覽器並保存在瀏覽器端的一小塊數據,當瀏覽器再次向同一個服務器發送請求時救護攜帶上這塊數據併發送給服務器。如果不設置Cookie的生存時間,Cookie就保存在瀏覽器內存中,當瀏覽器窗口關閉時,Cookie就會失效。如果設置了Cookie的生存時間,Cookie就會保存在硬盤上,知道生存時間過了Cookie纔會過期。Cookie數據是以key=value形式存儲的。

在這裏插入圖片描述

Cookie會話管理機制:

(1)瀏覽器端第一次發送請求到服務器端
(2)服務器端創建Cookie,該Cookie中包含用戶的信息,然後在響應時在響應頭中將該Cookie發送到瀏覽器端 (3)將Cookie保存在瀏覽器端,當再次訪問服務器端時會在請求頭中攜帶服務器端創建的Cookie
(4)服務器端通過Cookie中攜帶的數據區分不同的用戶

@RequestMapping(path = "/cookie/set", method = RequestMethod.GET)
@ResponseBody
public String setCookie(HttpServletResponse response) {
    // 創建cookie 其中key:code,value:隨機字符串
    Cookie cookie = new Cookie("code", CommunityUtil.generateUUID());
    // 設置cookie生效的範圍:即哪些請求會攜帶Cookie
    cookie.setPath("/community/alpha");
    // 設置cookie的生存時間,單位s,這樣Cookie就會存在硬盤中,而不是內存中,只要過了生存時間纔會失效
    cookie.setMaxAge(60 * 10);
    // 發送cookie,將cookie響應給瀏覽器
    response.addCookie(cookie);

    return "set cookie";
}

//獲取cookie,從請求頭中獲取
@RequestMapping(path = "/cookie/get", method = RequestMethod.GET)
@ResponseBody
public String getCookie(@CookieValue("code") String code) {
    System.out.println(code);
    return "get cookie";
}

在這裏插入圖片描述

將敏感的用戶信息存放在cookie中是很不安全的,造成cookie截獲和cookie欺騙,而且cookie可以存放的內部才能很小,不能存放太多數據。一般對於比較敏感的數據不會選擇存放在cookie中,而是存放在session中。

1.3 Session會話機制

Session可以將數據存放在服務端,而不是客戶端,因此相對而言比較安全。一般可以將用戶登錄的用戶名和密碼存放在Session中。

在這裏插入圖片描述

① 瀏覽器訪問服務器,服務器就會創建一個Session對象,保存在服務器端,瀏覽器和Session之間的對應關係依賴於Cookie。

② 響應數據時,服務器底層自動創建一個Cookie,通過這個Cookie攜帶sessionId,這個sessionId就是這個對象的唯一標識,然後將Cookie保存在瀏覽器端

③ 再次訪問服務器時就會發送給服務器,服務器得到sessionId後就可以去服務器端找對應的session了

Session是存在服務器的內存中, 每個會話對應一個sessionId,在響應時服務器會自動創建一個Cookie,JSESISONID作爲key,在服務端創建的Session對象的sessionId作爲value, 響應給瀏覽器端。這個SessionID就是一個唯一表示,用來區分session對象。

/**
* session中可以存放任何數據,但是cookie中只能存放字符串,
* 因爲Cookie需要來回傳,不能存放大量數據,而且客戶端其他類型數據也不能識別,但是字符串可以識別
*
* 向session中存放兩條數據
*/
@RequestMapping(path = "/session/set", method = RequestMethod.GET)
@ResponseBody
public String setSession(HttpSession session) {
    session.setAttribute("id", 1);
    session.setAttribute("name", "Test");
    return "set session";
}

可以看到session對象有一個sessionId,這個sessionId通過Cookie攜帶存放在客戶端,而用戶信息存放在服務端

在這裏插入圖片描述

從Session中取值:

@RequestMapping(path = "/session/get", method = RequestMethod.GET)
@ResponseBody
public String getSession(HttpSession session) {
    System.out.println(session.getAttribute("id"));
    System.out.println(session.getAttribute("name"));
    return "get session";
}

在這裏插入圖片描述

1.4 Cookie和Session的區別?

(1)數據存放位置不同:Cookie存放在瀏覽器上,session存放在服務器上

(2)安全程度不同:cookie並不安全,別人可以解析本地的cookie進行cookie欺騙,對於登錄等重要的信息可以存放在session中,而對於不重要的信息可以存放在cookie中。

(3)性能使用程度不同:session會在一定時間內保存在服務器上。當訪問增多,會比較佔用你服務器的性能,考慮到減輕服務器性能方面,應當使用cookie。

(4)數據存儲大小不同:單個cookie保存的數據不能超過4K,很多瀏覽器都限制一個站點最多保存20個cookie,而session則存儲與服務端,瀏覽器對其沒有限制。

2. 爲什麼要用token驗證,不用session?

2.1 Session會話

session表示會話,會話是指一個瀏覽器和一個服務器進行通信的過程。session存儲於服務器,可以理解爲一個狀態列表,擁有一個唯一識別符號sessionId。

session的使用方式:客戶端的cookie存放session_id,服務端的session保存用戶信息,瀏覽器再次訪問服務器時根據session_id到session中查找用戶信息。

具體流程爲:

① 當用戶第一次通過瀏覽器使用用戶名和密碼訪問服務器請求登錄時,服務器會驗證用戶信息

② 登錄成功後,會將用戶信息保存在服務器端的session中,將session_id通過cookie響應給瀏覽器

③ 當用戶再次登錄時,會攜帶session_id,服務器拿着session_id找到session對象,查詢用戶信息,查詢出後將用戶信息返回給瀏覽器,從而使用戶保持登錄狀態。

在這裏插入圖片描述

session像是這個用戶登錄了應用,用戶把全部信息存放在此應用,應用擁有完整的用戶信息。

session:註冊登錄->服務端將user存入session->將sessionid存入瀏覽器的cookie->再次訪問時根據cookie裏的sessionid找到session裏的user。

2.2 Session會話的弊端

(1)服務器壓力增大

通常session是存儲在內存中的,每個用戶通過認證之後都會將session數據保存在服務器的內存中,而當用戶量增大時,服務器的壓力增大。

(2)CSRF跨站僞造請求攻擊

session是基於cookie進行用戶識別的, cookie如果被截獲,用戶就會很容易受到跨站請求僞造的攻擊。

(3)無法實現session共享。

如果將來搭建了多個服務器,雖然每個服務器都執行的是同樣的業務邏輯,但是session數據是保存在內存中的(不是共享的),用戶第一次訪問的是服務器1,當用戶再次請求時可能訪問的是另外一臺服務器2,服務器2獲取不到session信息,就判定用戶沒有登陸過。

2.3 token令牌機制

token一般翻譯成令牌,一般是用於驗證用戶身份的數據,可以用url傳參,也可以用post提交,也可以夾在http的header中。它相當於session中的session_id,是一個字符串,存放在瀏覽器中。

注意:session數據默認存放在服務器的內存中,因此當用戶量變多時,服務器壓力就會增大。如果將session數據存放在數據庫中或者redis緩存中,就要建立session_id相關的數據庫表,把session數據通過session_id存放在數據庫中比較複雜,不如直接使用token代替session_id,將session數據存放在數據庫中。

token的驗證方式:由於服務器內存中並沒有存儲token數據,因此需要先從數據庫中查詢當前token,服務器再驗證否有效;

具體流程爲:

① 當瀏覽器第一次通過用戶名和密碼訪問服務器時,服務器收到請求後會去驗證用戶信息。

② 登錄成功後,服務端會簽發一個token,再將這個token響應給瀏覽器,同時將token和對應的用戶信息保存在數據庫或緩存中

③ 瀏覽器收到token後,把它存儲在Cookie或者localStorage中,以後每次向服務端請求資源的時候需要帶着服務端簽發的 token

④ 服務器收到請求後,會從數據庫或者緩存中查詢對應的token,如果驗證成功,就向瀏覽器返回請求的數據,保持登錄的狀態。

在這裏插入圖片描述

這種方式也是我在項目中使用的方式,我並沒有使用session存放用戶信息,一是因爲session數據默認存放在服務器內存中,爲了減小服務器的壓力,二是因爲session需要依賴於Cookie,會造成CSRF(跨站請求僞造)的風險,三是因爲用戶信息存放在服務器內存中,爲了避免session不能共享的問題。因此考慮到這些問題,我在項目中一開始將token和用戶信息存放在了mysql數據庫中,每次用戶請求登錄時攜帶token,通過token去數據庫中認證,如果認證成功,那麼就將請求數據返回給瀏覽器實現登錄狀態的保持。後來爲了提高性能,我又對項目做了優化,將token和用戶信息存在了redis緩存中,避免頻繁訪問數據庫。因此方用戶請求登錄時,通過token去redis緩存中查詢token實現認證。

token的優勢:①無狀態、可擴展 ②支持移動設備 ③跨程序調用 ④安全

2.4 token如何出現的?

token其實借鑑了cookie和session的工作原理,解決session依賴於單個服務器不能實現session共享的問題:單體應用時用戶信息保存在session中,不會出現問題,但是如果有多太服務器就會出現問題。比如用戶在A服務器登錄後,session就存在A服務器中,但是之後第二次請求分到了B服務器,由於B服務器沒有用戶的session數據,因此用戶還要重新登錄。

① 在session會話管理中,cookie中保存的是session_id,這是一個具有唯一性標識的字符串,因爲我們也可以使用一個具有唯一性標識的字符串返回給瀏覽器,保存在瀏覽器內存中。

② 服務器端存放的是session數據,每個session對象包括session_id和session數據中的鍵值對,這不就是redis中的哈希表嗎?因此可以使用redis的哈希類型來模擬服務器的session。

③ 因此我們可以在用戶第一次請求是生成一個全局唯一的token返回給瀏覽器,同時將用戶信息保存在redis中並設置過期時間(session的過期時間爲30分鐘),之後瀏覽器的每次請求都帶着這個token,服務器根據每次請求到redis中查找對應的用戶信息。

2.5 JWT(Json web token)

jwt的由三部分組成,官網:https://jwt.io/

頭部(Header)
    用於描述關於該JWT的最基本的信息,例如其類型以及簽名所用的算法等。這也可以被表示成一個JSON對象。
    然後將其進行base64編碼,得到第一部分
{
"typ": "JWT",
"alg": "HS256"
}

載荷(Payload)
    一般添加用戶的相關信息或其他業務需要的必要信息。但不建議添加敏感信息,因爲該部分在客戶端可解密
    (base64是對稱解密的,意味着該部分信息可以歸類爲明文信息)
    然後將其進行base64編碼,得到第二部分

{ "iss": "JWT Builder", 
  "iat": 1416797419, 
  "exp": 1448333419, 
  "aud": "www.example.com", 
  "sub": "[email protected]", 
  "Email": "[email protected]", 
  "Role": [ "admin", "user" ] 
}
iss:該JWT的簽發者,是否使用是可選的;
sub:該JWT所面向的用戶,是否使用是可選的;
aud:接收該JWT的一方,是否使用是可選的;
exp(expires):什麼時候過期,這裏是一個Unix時間戳,是否使用是可選的;
iat(issued at):在什麼時候簽發的(UNIX時間),是否使用是可選的;
nbf (Not Before):如果當前時間在nbf裏的時間之前,則Token不被接受;是否使用是可選的;
jti:JWT的唯一身份標識,主要用來作爲一次性token,從而回避重放攻擊。

簽名(Signature)
    需要base64加密後的header和base64加密後的payload使用"."連接組成的字符串,
    然後通過header中聲明的加密方式進行加鹽secret組合加密(在加密的時候,我們還需要提供一個密鑰(secret),加鹽secret組合加密)
    然後就構成了jwt的第三部分。
最後,將這一部分簽名也拼接在被簽名的字符串後面,我們就得到了完整的JWT

jwt認證方式:註冊登錄->服務端將生成一個token,並將token與user加密生成一個密文->將token+user+密文數據 返回給瀏覽器->再次訪問時傳遞token+user+密文數據,後臺會再次使用token+user生成新密文,與傳遞過來的密文比較,一致則正確。

具體流程:

①認證成功後,會對當前用戶數據進行加密,生成一個加密字符串token,返還給客戶端(服務器端並不進行保存)

②瀏覽器會將接收到的token值存儲在Local Storage中,(通過js代碼寫入Local Storage,通過js獲取,並不會像cookie一樣自動攜帶)

③再次訪問時服務器端對token值的處理:服務器對瀏覽器傳來的token值進行解密,解密完成後進行用戶數據的查詢,如果查詢成功,則通過認證,實現狀態保持,所以,即時有了多臺服務器,服務器也只是做了token的解密和用戶數據的查詢,它不需要在服務端去保留用戶的認證信息或者會話信息,這就意味着基於token認證機制的應用不需要去考慮用戶在哪一臺服務器登錄了,這就爲應用的擴展提供了便利,解決了session無法共享的弊端。
在這裏插入圖片描述

3. 存放驗證碼

3.1 將驗證碼存放在Session中

@RequestMapping(path = "/kaptcha", method = RequestMethod.GET)
public void getKaptcha(HttpServletResponse response, HttpSession session) {
    // 生成驗證碼
    String text = kaptchaProducer.createText();
    BufferedImage image = kaptchaProducer.createImage(text);

    // 將驗證碼存入session
    session.setAttribute("kaptcha", text);

    // 將突圖片輸出給瀏覽器
    response.setContentType("image/png");
    try {
        OutputStream os = response.getOutputStream();
        ImageIO.write(image, "png", os);
    } catch (IOException e) {
        logger.error("響應驗證碼失敗:" + e.getMessage());
    }
}

在這裏插入圖片描述

將生成的驗證碼放在session中,當響應時就會自動生成一個Cookie,Cookie中攜帶了這個session對象的sessiod_id,並保存在瀏覽器中,當登錄時就會在請求頭中通過cookie攜帶session_id,通過session_id找到對應的session對象,進而取出對應的驗證碼。

@RequestMapping(path = "/login", method = RequestMethod.POST)
public String login(String username, String password, String code, boolean rememberme, Model model, HttpSession session, HttpServletResponse response) {

    // 檢查驗證碼,用戶生成的驗證碼一開始是是放在session中的,需要通過session將驗證碼取出來
    String kaptcha = (String) session.getAttribute("kaptcha");

    if (StringUtils.isBlank(kaptcha) || StringUtils.isBlank(code) || !kaptcha.equalsIgnoreCase(code)) {
        model.addAttribute("codeMsg", "驗證碼不正確!");
        return "/site/login";
    }
}

3.2 將驗證碼存放在redis中

使用redis存放驗證碼,提高性能:

① 驗證碼需要頻繁的訪問和刷新,對性能要求較高。

② 驗證碼不會永久保存,通常在很短的時間內就會失效

③ 驗證碼存在session中,分佈式部署時存在session共享的問題

//構建redis的key,key中存放的用戶的唯一標識owner
public class RedisKeyUtil {
    private static final String SPLIT = ":";
    private static final String PREFIX_KAPTCHA = "kaptcha";

    // 登錄驗證碼
    public static String getKaptchaKey(String owner) {
        //驗證碼和用戶是相關的,需要識別用戶,但是又不可能和userId綁定到一起,因爲此時用戶還沒登錄
        //在用戶訪問登錄頁面的時候,給他發一個憑證owner,讓他存在Cookie中,我們用這個字符串owner來臨時標識這個用戶
        return PREFIX_KAPTCHA + SPLIT + owner;
    }
}

@Controller
public class LoginController implements CommunityConstant {
	@Autowired
    private UserService userService;

    @Autowired
    private Producer kaptchaProducer;

    @Value("${server.servlet.context-path}")
    private String contextPath;

    @Autowired
    private RedisTemplate redisTemplate;

    @RequestMapping(path = "/kaptcha", method = RequestMethod.GET)
    public void getKaptcha(HttpServletResponse response/*, HttpSession session*/) {
        // 生成驗證碼
        String text = kaptchaProducer.createText();
        BufferedImage image = kaptchaProducer.createImage(text);

        // 驗證碼的歸屬,用來將驗證碼和用戶關聯起來的owner
        String kaptchaOwner = CommunityUtil.generateUUID();
        //這個owner需要存放在Cookie中發送給客戶端
        Cookie cookie = new Cookie("kaptchaOwner", kaptchaOwner);
        cookie.setMaxAge(60);
        cookie.setPath(contextPath);
        response.addCookie(cookie);

        // 將驗證碼存入Redis中,redis的key存放的是驗證碼的owner,value存放的是owner對應的驗證碼
        String redisKey = RedisKeyUtil.getKaptchaKey(kaptchaOwner);
        redisTemplate.opsForValue().set(redisKey, text, 60, TimeUnit.SECONDS);

        // 將圖片輸出給瀏覽器
        response.setContentType("image/png");
        try {
            OutputStream os = response.getOutputStream();
            ImageIO.write(image, "png", os);
        } catch (IOException e) {
            logger.error("響應驗證碼失敗:" + e.getMessage());
        }
    }

    @RequestMapping(path = "/login", method = RequestMethod.POST)
    public String login(String username, String password, String code, boolean rememberme, Model model,HttpServletResponse response) {
        // 登錄的時候先從cookie中取出驗證碼的歸屬kaptchaOwner
        String kaptcha = null;
        if (StringUtils.isNotBlank(kaptchaOwner)) {
            //得到redis的key
            String redisKey = RedisKeyUtil.getKaptchaKey(kaptchaOwner);
            //通過redis的key得到驗證碼
            kaptcha = (String) redisTemplate.opsForValue().get(redisKey);
        }

        if (StringUtils.isBlank(kaptcha) || StringUtils.isBlank(code) || !kaptcha.equalsIgnoreCase(code)) {
            model.addAttribute("codeMsg", "驗證碼不正確!");
            return "/site/login";
        }
    }
}

總結:

將驗證碼存放在session中時,驗證碼就是和session_id關聯起來的,當用戶首次訪問登錄頁面的時候,就會在後端生成一個驗證碼,並將session_id通過cookie響應給瀏覽器,當用戶登錄的時候就會在請求頭中通過cookie攜帶這個session_id,通過session_id找到對應的session對象,從而獲得驗證碼。用戶和驗證碼是通過具有唯一性標識的session_id關聯起來的,因此當我們向把驗證碼存放在redis中,也需要模仿這個原理和過程。

我們使用的owner這個唯一標識就相當於session_id,也可以把它當成token,總之就是一個全局唯一的字符串。當驗證碼生成後,服務器就生成一個owner(token),然後通過Cookie將這個owner返回給瀏覽器,並在瀏覽器中保存下來,當用戶登錄的時候,就會通過cookie攜帶owner,然後通過owner找到對應的redis的key,進而驗證驗證碼是否正確。這個過程其實就是token的驗證過程。只不過換了一個名字而已。

4. 存放標識用戶信息的token

4.1 將token存放在數據庫中

在這裏插入圖片描述

首先用戶請求登錄,服務器會進行驗證,驗證成功後會生成一個登錄憑證LoginTicket對象,將登錄憑證存入數據庫中,同時將代表session_id的ticket通過Cookie返回給客戶端,並存放在客戶端。當下次登錄時在請求頭中攜帶ticket,服務器從數據庫表中查詢對應的ticket並驗證是否正確,驗證成功後就可以得到用戶信息了。

@Service
public class UserService implements CommunityConstant {

    @Autowired
    private UserMapper userMapper;
    @Autowired
    private LoginTicketMapper loginTicketMapper;
    
	//用戶登錄
    public Map<String, Object> login(String username, String password, int expiredSeconds){
        Map<String, Object> map = new HashMap<>();

        // 服務器端進行驗證,驗證成功後生成登錄憑證.....

        // 生成登錄憑證
        LoginTicket loginTicket = new LoginTicket();
        loginTicket.setUserId(user.getId());
        loginTicket.setTicket(CommunityUtil.generateUUID());
        //登錄狀態時status=0,代表憑證有效
        loginTicket.setStatus(0);
        //憑證的過期時間
        loginTicket.setExpired(new Date(System.currentTimeMillis() + expiredSeconds * 1000));
      	//將登錄憑證和用戶信息都存入數據庫中
        loginTicketMapper.insertLoginTicket(loginTicket);

        map.put("ticket", loginTicket.getTicket());
        return map;
    }
    
    //退出登錄
    public void logout(String ticket) {
        loginTicketMapper.updateStatus(ticket, 1);
    }
}

@Controller
public class LoginController implements CommunityConstant {
    @Autowired
    private UserService userService;

    @Value("${server.servlet.context-path}")
    private String contextPath;

    //用戶登錄
    @RequestMapping(path = "/login", method = RequestMethod.POST)
    public String login(String username, String password, String code, boolean rememberme,Model model, HttpServletResponse response) {

        // 檢查賬號,密碼
        int expiredSeconds = rememberme ? REMEMBER_EXPIRED_SECONDS : DEFAULT_EXPIRED_SECONDS;
        Map<String, Object> map = userService.login(username, password, expiredSeconds);
        //如果map中包含了ticket,說明登錄成功
        if (map.containsKey("ticket")) {
            //創建Cookie,將tiket存放在Cookie中返回給客戶端
            Cookie cookie = new Cookie("ticket", map.get("ticket").toString());
            cookie.setPath(contextPath);
            cookie.setMaxAge(expiredSeconds);
            //將cookie響應給客戶端
            response.addCookie(cookie);
            return "redirect:/index";
        } else {
            model.addAttribute("usernameMsg", map.get("usernameMsg"));
            model.addAttribute("passwordMsg", map.get("passwordMsg"));
            return "/site/login";
        }
    }
    
	//用戶登出
    @RequestMapping(path = "/logout", method = RequestMethod.GET)
    public String logout(@CookieValue("ticket") String ticket) {
        userService.logout(ticket);
        return "redirect:/login";
    }
}

用戶首次登錄—》驗證用戶信息----》生成登錄憑證----》將登錄憑證和用戶信息保存在數據庫中—》創建Cookie對象,將ticket通過cookie響應給客戶端。注意:這裏只寫了首次登錄的邏輯,至於再次登錄時通過憑證查詢用戶信息,這個邏輯需要用到攔截器,下次繼續討論。

4.2 將token存放在redis中

使用redis存儲登錄憑證,因此處理每次請求時,都需要查詢用戶的登錄憑證,訪問的頻率較高,可以通過redis提高性能。因此將上面存放在數據庫中的登錄憑證改爲存在redis中。

//構建redis的key,因爲需要通過ticket查詢用戶信息,因此將ticket作爲redis的key
public class RedisKeyUtil {
    private static final String SPLIT = ":";
    private static final String PREFIX_TICKET = "ticket";
    // 登錄的憑證
    public static String getTicketKey(String ticket) {
        return PREFIX_TICKET + SPLIT + ticket;
    }
}

@Service
public class UserService implements CommunityConstant {
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private RedisTemplate redisTemplate;

    public Map<String, Object> login(String username, String password, int expiredSeconds) {
        Map<String, Object> map = new HashMap<>();
        // 驗證信息

        // 驗證成功後,生成登錄憑證
        LoginTicket loginTicket = new LoginTicket();
        loginTicket.setUserId(user.getId());
        loginTicket.setTicket(CommunityUtil.generateUUID());
        loginTicket.setStatus(0);
        loginTicket.setExpired(new Date(System.currentTimeMillis() + expiredSeconds * 1000));
        //構建redis的key
        String redisKey = RedisKeyUtil.getTicketKey(loginTicket.getTicket());
        //將ticket和用戶信息存在redis緩存中,key=ticket,value = loginTicket
        redisTemplate.opsForValue().set(redisKey, loginTicket);

        map.put("ticket", loginTicket.getTicket());
        return map;
    }

    public void logout(String ticket) {
        //登出時構建redis的key
        String redisKey = RedisKeyUtil.getTicketKey(ticket);
        //從redis中查詢出loginTicket
        LoginTicket loginTicket = (LoginTicket) redisTemplate.opsForValue().get(redisKey);
        //將用戶登錄狀態設置爲失效
        loginTicket.setStatus(1);
        //再將loginTicket存入redis緩存中
        redisTemplate.opsForValue().set(redisKey, loginTicket);
    }
}

@Controller
public class LoginController implements CommunityConstant {
    @Autowired
    private UserService userService;

    @Value("${server.servlet.context-path}")
    private String contextPath;

    @Autowired
    private RedisTemplate redisTemplate;
    
    //用戶登錄
    @RequestMapping(path = "/login", method = RequestMethod.POST)
    public String login(String username, String password, String code, boolean rememberme, Model model, HttpServletResponse response) {

        // 檢查賬號,密碼
        int expiredSeconds = rememberme ? REMEMBER_EXPIRED_SECONDS : DEFAULT_EXPIRED_SECONDS;
        Map<String, Object> map = userService.login(username, password, expiredSeconds);
        if (map.containsKey("ticket")) {
            Cookie cookie = new Cookie("ticket", map.get("ticket").toString());
            cookie.setPath(contextPath);
            cookie.setMaxAge(expiredSeconds);
            response.addCookie(cookie);
            return "redirect:/index";
        } else {
            model.addAttribute("usernameMsg", map.get("usernameMsg"));
            model.addAttribute("passwordMsg", map.get("passwordMsg"));
            return "/site/login";
        }
    }
	
    //用戶登出
    @RequestMapping(path = "/logout", method = RequestMethod.GET)
    public String logout(@CookieValue("ticket") String ticket) {
        userService.logout(ticket);
        return "redirect:/login";
    }
}

用戶首次登錄—》服務器驗證—》驗證成功後生成登錄憑證存放在redis中,key = ticket,value=loginTicket----》創建Cookie對象,通過cookie將ticket返回給客戶端並保存下來。

5. redis緩存用戶信息

處理每次請求時都需要通過憑證查詢用戶信息,因此可以通過將用戶信息放在redis緩存中。

使用redis緩存的步驟:
①優先從緩存中取值
②取不到值時初始化緩存數據
③數據變更時清除緩存數據

// 構建redis的key,因爲獲取憑證後查詢用戶信息是通過userId插詢的,因此將userId設置爲key
public static String getUserKey(int userId) {
    return PREFIX_USER + SPLIT + userId;
}

@Service
public class UserService implements CommunityConstant {
    
    // 1.優先從緩存中取值:查詢用戶時嘗試從緩存中取值,先不去訪問mysql
    private User getCache(int userId) {
        //構建redis的key=userId
        String redisKey = RedisKeyUtil.getUserKey(userId);
        //通過userId從緩存中取出用戶信息User
        return (User) redisTemplate.opsForValue().get(redisKey);
    }

    // 2.取不到時初始化緩存數據:取不到用戶信息時說明緩存中沒有初始化,初始化
    private User initCache(int userId) {
        //數據來源於mysql
        User user = userMapper.selectById(userId);
        //構建redis的key
        String redisKey = RedisKeyUtil.getUserKey(userId);
        //向redis中存入用戶信息,key= userId,value=user
        redisTemplate.opsForValue().set(redisKey, user, 3600, TimeUnit.SECONDS);
        return user;
    }

    // 3.數據變更時清除緩存數據:用戶數據發生變化時,清除緩存
    private void clearCache(int userId) {
        String redisKey = RedisKeyUtil.getUserKey(userId);
        redisTemplate.delete(redisKey);
    }

    //每次獲取登錄憑證後,都要調用這個方法,因此訪問非常頻繁
    public User findUserById(int id) {
		//先從緩存中查詢用戶信息
        User user = getCache(id);
        if (user == null) {
            //查不到的時候,就會初始化redis緩存
            user = initCache(id);
        }
        return user;
    }

    public int activation(int userId, String code) {
        User user = userMapper.selectById(userId);
        if (user.getStatus() == 1) {
            return ACTIVATION_REPEAT;
        } else if (user.getActivationCode().equals(code)) {
            userMapper.updateStatus(userId, 1);
            //激活的時候對用戶狀態進行了修改,因此需要清除緩存
            clearCache(userId);
            return ACTIVATION_SUCCESS;
        } else {
            return ACTIVATION_FAILURE;
        }
    }

    public int updateHeader(int userId, String headerUrl) {
        int rows = userMapper.updateHeader(userId, headerUrl);
        //修改用戶信息,清除緩存
        clearCache(userId);
        return rows;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章