實戰 用戶登錄、session 校驗、分佈式存儲 session

實現登錄功能

然後再創建 login.css 存放於在 static 下,css 目錄中,id 爲 content 的 樣式;

#content {
        margin-left: 220px;
        margin-right: 1420px;
        margin-top: 100px;
        margin-bottom: auto;
        background-color: orange;
    }

創建 login.html 登錄頁面

<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登錄</title>
    <!-- 如何引入本地css文件-->
    <link rel="stylesheet" th:href="@{/css/login.css}"/>
</head>
<body>
<div id="content">
    <!-- 錯誤是提示-->
    <label id="errorMsg" style="color: crimson">[[${errorMsg}]]</label>
    <form id="login_form" action="/login" method="post">
        姓名:<input type="text" id="uname" ><br/>
        密碼:<input type="password" id="password" ><br/>
        <button onclick="login()">登錄</button>
    </form>
</div>
</body>
</html>

前面的這一部分是前端的,下面來把後端代碼給寫完:

UserRepository 中添加方法的定義:

//通過用戶名和密碼查找用戶
List<User> findByUnameAndPassword(String uname, String password);

UserService 和實現類中添加方法如下:

/通過用戶名和密碼查找用戶
List<User> findByUnameAndPassword(String uname, String password);
UserService和實現類中添加方法如下:

// UserService 
User login(User user);

@Service
//把事務註解放在類上了,這樣下面就不需要每次都在方法寫這個註解了
@Transactional(rollbackFor = Exception.class)
public class UserServiceImpl implements UserService {
    //......
    @Override
    public User login(User user) {
        List<User> userList = userRepository.findByUnameAndPassword(user.getUname(), user.getPassword());
        //防止有多個用戶名相同,並且密碼也相同的用戶
        if (!CollectionUtils.isEmpty(userList)) {
            return userList.get(0);
        }
        return null;
    }
}

UserController 中添加方法如下:

@RequestMapping(value = "/loginPage"method = RequestMethod.GET)
public String loginPage(Model model) {
    return "login";
}

@RequestMapping(value = "/login"method = RequestMethod.POST)
public String login(Model model, User user) {
    User result = userService.login(user);
    if (result != null) {
        //登錄成功,跳轉到用戶列表
        return "redirect:/userList";
    }
    //不成功,提示
    model.addAttribute("errorMsg""用戶名或密碼不正確");
    return "login";
}

啓動項目,訪問

http://localhost:8080/loginPage

進入登錄頁面。

輸入用戶名密碼。密碼錯誤:

輸入正確的用戶名和密碼,那麼跳轉到用戶列表。

這樣,我們一個簡單的登錄功能就搞定了。

如果我們需要在修改用戶信息的時候,校驗是否已經登錄,怎麼辦呢?

攔截器

創建自定義的攔截器並實現 HandlerInterceptor 接口 。

import org.springframework.lang.Nullable;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class SessionInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //session校驗
        Object object = request.getSession().getAttribute("users");
        if (null == object) {
            response.sendRedirect("/loginPage");
            return false;
        }
        return true;
    }
}

創建一個 java 類繼承 WebMvcConfiguraeAdapter 並重寫 addInterceptor 方法(該類用來添加配置攔截器在該類中添加配置攔截器,以及配置過濾)。

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
public class MyInterceptor extends WebMvcConfigurerAdapter {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //可以添加多個攔截
        registry.addInterceptor(new SessionInterceptor())
            //也可以添加多個攔截路徑,"/**"攔截所有
                .addPathPatterns("/update/**");
    }
}

再把登錄 Controller 方法調整,把 session 信息存進去。

@RequestMapping(value = "/login"method = RequestMethod.POST)
public String login(Model model, User user, HttpServletRequest request) {
    User result = userService.login(user);
    if (result != null) {
        //用戶信息保存在session
        request.getSession().setAttribute("users", user.getUname());
        return "redirect:/userList";
    }
    model.addAttribute("errorMsg""用戶名或密碼不正確");
    return "login";
}

再次訪問用戶列表:

http://localhost:8080/userList

這時候,我們訪問修改用戶信息這個功能,跳轉到了登錄頁面。

登錄後,再次訪問修改用戶信息這個功能。

這樣便來到用戶信息修改頁面。

到此,我們就實現了一個簡單的 session 來接校驗。

如果,我們服務器重啓後,session 就沒了,因爲 session 是保存在我們服務端的,並且還是在服務器內存裏的。

session 分佈式有四種方案

方案一:客戶端存儲

直接將信息存儲在 cookie 中,cookie 是存儲在客戶端上的一小段數據,客戶端通過 http 協議和服務器進行 cookie 交互,通常用來存儲一些不敏感信息

缺點

方案二:session 複製

session 複製是小型企業應用使用較多的一種服務器集羣 session 管理機制,在真正的開發使用的並不是很多,通過對 web 服務器 (例如 Tomcat) 進行搭建集羣。

缺點

session 同步的原理是在同一個局域網裏面通過發送廣播來異步同步 session 的,一旦服務器多了,併發上來了,session 需要同步的數據量就大了,需要將其他服務器上的 session 全部同步到本服務器上,會帶來一定的網路開銷,在用戶量特別大的時候,會出現內存不足的情況。

優點

服務器之間的 session 信息都是同步的,任何一臺服務器宕機的時候不會影響另外服務器中 session 的狀態,配置相對簡單

Tomcat 內部已經支持分佈式架構開發管理機制,可以對 tomcat 修改配置來支持 session 複製,在集羣中的幾臺服務器之間同步 session 對象,使每臺服務器上都保存了所有用戶的 session 信息,這樣任何一臺本機宕機都不會導致 session 數據的丟失,而服務器使用 session 時,也只需要在本機獲取即可。

如何配置?

在 Tomcat 安裝目錄下的 config 目錄中的 server.xml 文件中,將註釋打開,tomcat 必須在同一個網關內,要不然收不到廣播,同步不了 session,在 web.xml 中開啓 session 複製:。

方案三:session 綁定:

Nginx 是一款自由的、開源的、高性能的 http 服務器和反向代理服務器

Nginx 能做什麼?

反向代理、負載均衡、http 服務器(動靜代理)、正向代理

如何使用 nginx 進行 session 綁定

我們利用 nginx 的反向代理和負載均衡,之前是客戶端會被分配到其中一臺服務器進行處理,具體分配到哪臺服務器進行處理還得看服務器的負載均衡算法 (輪詢、隨機、ip-hash、權重等),但是我們可以基於 nginx 的 ip-hash 策略,可以對客戶端和服務器進行綁定,同一個客戶端就只能訪問該服務器,無論客戶端發送多少次請求都被同一個服務器處理。

缺點

優點

方案四:基於 redis 存儲 session 方案

優點

缺點

多了一次網絡調用,web 容器需要向 redis 訪問。

一般會將 web 容器所在的服務器和 redis 所在的服務器放在同一個機房,減少網絡開銷,走內網進行連接。

實現基於 redis 分佈式存儲 session 方案

安裝 Redis,這裏就不說了,不會安裝可以聯繫我。

集成 Redis

添加依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 連接池-->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

添加 Redis 配置

# Redis數據庫索引(默認爲0)
spring.redis.database=0
# Redis服務器地址
spring.redis.host=127.0.0.1
# Redis服務器連接端口
spring.redis.port=6379
# Redis服務器連接密碼(默認爲空)
spring.redis.password=
# 連接池最大連接數(使用負值表示沒有限制)
spring.redis.jedis.pool.max-active=20
# 連接池最大阻塞等待時間(使用負值表示沒有限制)
spring.redis.jedis.pool.max-wait=-1
# 連接池中的最大空閒連接
spring.redis.jedis.pool.max-idle=10
# 連接池中的最小空閒連接
spring.redis.jedis.pool.min-idle=0
# 連接超時時間(毫秒)
spring.redis.timeout=1000

將 session 添加入 Redis 中

在啓動類上添加 @EnableRedisHttpSession 註解。

@SpringBootApplication
@EnableRedisHttpSession
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

啓動項目,然後,再次登錄後,便可以在 Redis 裏查到了

再次重啓項目後,發現修改用戶信息的時候,並不需要重新登錄了。

到此,基於 Redis 分佈式存儲 session 方案就已經搞定了。

總結

本文首先是實戰了登錄功能,其次接着實現了校驗 session 攔截處理,然後總結出 session 分佈式四種方案,最後實現了基於 redis 存儲 session 的方案。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/V-wBH0Zz6UjOCcdruNgrIQ