上次小黑在文章中介紹了四種分布式一致性 Session 的實作方式,在這四種中最常用的就是后端集中存盤方案,這樣即使 web 應用重啟或者擴容,Session 都沒有丟失的風險,

今天我們就使用這種方式對 Session 存盤方式進行改造,將其統一存盤到 Redis 中,
實作方案
我們先來想一下,如果我們不依靠任何框架,自己如何實作后端 Session 集中存盤,
這里我們假設我們的網站除了某些頁面,比如首頁可以直接訪問以外,其他任何頁面都需要登錄之后才能訪問,
如果需要實作這個需求,這就需要我們對每個請求都進行鑒權,鑒權目的是為了判斷用戶是否登錄,判斷用戶角色,
如果用戶沒有登錄,我們需要將請求強制跳轉到登錄頁面進行登錄,
用戶登錄之后,我們需要將登錄獲取到的用戶資訊存盤到 Session 中,這樣后面請求鑒權只需要判斷 Session 中是否存在即可,
知道整個流程之后,其實實作原理就不是很難了,
我們可以使用類似 AOP 的原理,在每個請求進來之后,都先判斷 Session 中是否存在用戶資訊,如果不存在就跳轉到登錄頁,
整個流程如下所示:

我們可以利用 Servelt Filter 實作上述流程,不過上述整套流程,Spring 已經幫我們實作了,那我們就不用重復造輪子了,
我們可以使用 Spring-Session 與 Spring-security 實作上述網站的流程,
Spring-Session 是 Spring 提供一套管理用戶 Session 的實作方案,使用 Spring-Session 之后,默認 WEB 容器,比如 Tomcat,產生的 Session 將會被 Spring-Session 接管,
除此之外,Spring-Session 還提供幾種常見后端存盤實作方案,比如 Redis,資料庫等,
有了 Spring-Session 之后,它只是幫我們解決了 Session 后端集中存盤,但是上述流程中我們還需要登錄授權,而這一塊我們可以使用 Spring-security 來實作,
Spring-security 可以維護統一的登錄授權方式,同時它可以結合 Spring-Session 一起使用,用戶登錄授權之后,獲取的用戶資訊可以自動存盤到 Spring-Session 中,
好了,不說廢話了,我們來看下實作代碼,
下述使用 Spring Boot 實作, Spring-Boot 版本為:2.3.2.RELEASE
Spring Session
首先我們引入 Spring Session 依賴,這里我們使用 Redis 集中存盤 Session 資訊,所以我們需要下述依賴即可,
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
如果不是 Spring Boot 專案,那主要需要引入如下依賴:
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>2.3.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-core</artifactId>
<version>2.3.0.RELEASE</version>
</dependency>
引入依賴之后,我們首先需要在 application.properties增加 Session 相關的配置:
## Session 存盤方式
spring.session.store-type=redis
## Session 過期時間,默認單位為 s
server.servlet.session.timeout=600
## Session 存盤到 Redis 鍵的前綴
spring.session.redis.namespace=test:spring:session
## Redis 相關配置
spring.redis.host=127.0.0.1
spring.redis.password=****
spring.redis.port=6379
配置完成之后,Spring Session 就會開始管理 Session 資訊,下面我們來測驗一下:
@ResponseBody
@GetMapping("/hello")
public String hello() {
return "Hello World";
}
當我們訪問上面地址之后,訪問 Redis ,可以看到存盤的 Session 資訊,
推薦大家一個 Redis 客戶端「Another Redis DeskTop Manager」,這個客戶端 UI 頁面非常漂亮,操作也很方便,下載地址:
https://github.com/qishibo/anotherredisdesktopmanager/releases

默認情況下,Session 默認使用HttpSession 序列化方式,這種值看起來不夠直觀,我們可以將其修改成 json 序列化方式,存盤到 redis 中,
@Configuration
public class HttpSessionConfig implements BeanClassLoaderAware {
private ClassLoader loader;
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer(objectMapper());
}
/**
* Customized {@link ObjectMapper} to add mix-in for class that doesn't have default
* constructors
*
* @return the {@link ObjectMapper} to use
*/
private ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModules(SecurityJackson2Modules.getModules(this.loader));
return mapper;
}
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
this.loader = classLoader;
}
}
修改之后 Redis 鍵值如下所示:
ps:這里 Redis 鍵值含義,下次分析原始碼的時候,再做分析,
Spring Session 還存在一個 @EnableRedisHttpSession,我們可以在這個注解上配置 Spring Session 相關配置,
@EnableRedisHttpSession(redisNamespace = "test:session")
需要注意的是,如果使用這個注解,將會導致 application.properties Session 相關配置失效,也就是說 Spring Session 將會直接使用注解上的配置,

這里小黑比較推薦大家使用組態檔的方式,
好了,Spring Session 到這里我們就接入完成了,
Spring security
上面我們集成了 Spring Session,完成 Session 統一 Redis 存盤,接下來主要需要實作請求的登陸鑒權,
這一步我們使用 Spring security 實作統一的登陸鑒權服務,同樣的框架的還有 Shiro,這里我們就使用 Spring 全家桶,
首先我們需要依賴的相應的依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
引入上面的依賴之后,應用啟動之后將會生成一個隨機密碼,然后所有的請求將會跳轉到一個 Spring security 的頁面,


這里我們需要實作自己業務的登陸頁,所以我們需要自定義登錄校驗邏輯,
在 Spring security 我們只需要實作 UserDetailsService介面,重寫 loadUserByUsername方法邏輯,
@Service
public class UserServiceImpl implements UserDetailsService {
@Autowired
PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 簡單起見,直接內部校驗
String uname = "admin";
String passwd = "1234qwer";
// 如果是正式專案,我們需要從資料庫資料資料,然后再校驗,形式如下:
// User user = userDAO.query(username);
if (!username.equals(uname)) {
throw new UsernameNotFoundException(username);
}
// 封裝成 Spring security 定義的 User 物件
return User.builder()
.username(username)
.passwordEncoder(s -> passwordEncoder.encode(passwd))
.authorities(new SimpleGrantedAuthority("user"))
.build();
}
}
上面代碼實作,這里主要在記憶體固定用戶名與密碼,真實環境下,我們需要修改成從資料庫查詢用戶資訊,
接著我們需要把 UserServiceImpl 配置到 Spring security 中,
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserServiceImpl userService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 使用自定義用戶服務校驗登錄資訊
*
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 用戶登錄資訊校驗使用自定義 userService
// 還需要注意密碼加密與驗證需要使用同一種方式
auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
}
}
上面的配置中,密碼部分我們使用 BCrypt 演算法加密,這里需要注意,加密與解密需要使用同一種方式,
接著我們需要實作一個自定義的登陸頁面,這里就懶得自己寫了,直接使用 spring-session-data-redis 頁面,
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org" xmlns:layout="https://github.com/ultraq/thymeleaf-layout-dialect"
layout:decorate="~{layout}">
<head>
<title>Login</title>
</head>
<body>
<div layout:fragment="content">
<!-- 自定義登錄的請求 -->
<form name="f" th:action="@{/auth/login}" method="post">
<fieldset>
<legend>Please Login -</legend>
<div th:if="${param.error}" class="alert alert-error">Invalid username and password.</div>
<div th:if="${param.logout}" class="alert alert-success">You have been logged out.</div>
<label for="username">Username</label>
<input type="text" id="username" name="username"/>
<label for="password">Password</label>
<input type="password" id="password" name="password"/>
<input type="hidden" th:name="${_csrf.parameterName}" th:value=https://www.cnblogs.com/goodAndyxublog/p/"${_csrf.token}"/>
