1. 程式人生 > >實戰開發,使用 Spring Session 與 Spring security 完成網站登入改造!!

實戰開發,使用 Spring Session 與 Spring security 完成網站登入改造!!

上次小黑在文章中介紹了[四種分散式一致性 Session 的實現方式](https://mp.weixin.qq.com/s/8HgFYgrJDC3bi5MY0icJfg),在這四種中最常用的就是後端集中儲存方案,這樣即使 web 應用重啟或者擴容,Session 都沒有丟失的風險。 ![](https://img2020.cnblogs.com/other/1419561/202008/1419561-20200819073732307-816857285.jpg) 今天我們就使用這種方式對 Session 儲存方式進行改造,將其統一儲存到 Redis 中。 ## 實現方案 我們先來想一下,如果我們不依靠任何框架,自己如何實現後端 **Session** 集中儲存。 這裡我們假設我們的網站除了某些頁面,比如首頁可以直接訪問以外,其他任何頁面都需要登入之後才能訪問。 如果需要實現這個需求,這就需要我們對每個請求都進行鑑權,鑑權目的是為了判斷使用者是否登入,判斷使用者角色。 如果使用者沒有登入,我們需要將請求強制跳轉到登入頁面進行登入。 使用者登入之後,我們需要將登入獲取到的使用者資訊儲存到 **Session** 中,這樣後面請求鑑權只需要判斷 **Session** 中是否存在即可。 知道整個流程之後,其實實現原理就不是很難了。 我們可以使用類似 **AOP** 的原理,在每個請求進來之後,都先判斷 Session 中是否存在使用者資訊,如果不存在就跳轉到登入頁。 整個流程如下所示: ![](https://img2020.cnblogs.com/other/1419561/202008/1419561-20200819073732698-806807378.jpg) 我們可以利用 **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 資訊,所以我們需要下述依賴即可。 ```xml org.springframework.session spring-session-data-redis ``` 如果不是 Spring Boot 專案,那主要需要引入如下依賴: ```xml org.springframework.data spring-data-redis 2.3.0.RELEASE
org.springframework.session spring-session-core 2.3.0.RELEASE ``` 引入依賴之後,我們首先需要在 `application.properties`增加 Session 相關的配置: ```properties ## 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 資訊,下面我們來測試一下: ```java @ResponseBody @GetMapping("/hello") public String hello() { return "Hello World"; } ``` 當我們訪問上面地址之後,訪問 Redis ,可以看到儲存的 Session 資訊。 >
推薦大家一個 Redis 客戶端「Another Redis DeskTop Manager」,這個客戶端 UI 頁面非常漂亮,操作也很方便,下載地址: > > https://github.com/qishibo/anotherredisdesktopmanager/releases ![](https://img2020.cnblogs.com/other/1419561/202008/1419561-20200819073733056-1836698601.jpg) 預設情況下,Session 預設使用HttpSession 序列化方式,這種值看起來不夠直觀。我們可以將其修改成 json 序列化方式,儲存到 redis 中。 ```java @Configuration public class HttpSessionConfig implements BeanClassLoaderAware { private ClassLoader loader; @Bean public RedisSerializer 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 鍵值如下所示:![](https://img2020.cnblogs.com/other/1419561/202008/1419561-20200819073733751-1772629628.jpg) > ps:這裡 Redis 鍵值含義,下次分析原始碼的時候,再做分析。 Spring Session 還存在一個 **@EnableRedisHttpSession**,我們可以在這個註解上配置 Spring Session 相關配置。 ```java @EnableRedisHttpSession(redisNamespace = "test:session") ``` 需要注意的是,如果使用這個註解,將會導致 `application.properties` Session 相關配置失效,也就是說 Spring Session 將會直接使用註解上的配置。 ![](https://img2020.cnblogs.com/other/1419561/202008/1419561-20200819073734092-78406027.jpg) 這裡小黑比較推薦大家使用配置檔案的方式。 好了,Spring Session 到這裡我們就接入完成了。 ## Spring security 上面我們集成了 Spring Session,完成 Session 統一 Redis 儲存。接下來主要需要實現請求的登陸鑑權。 這一步我們使用 Spring security 實現統一的登陸鑑權服務,同樣的框架的還有 Shiro,這裡我們就使用 Spring 全家桶。 首先我們需要依賴的相應的依賴: ```xml org.springframework.boot
spring-boot-starter-security
``` 引入上面的依賴之後,應用啟動之後將會生成一個隨機密碼,然後所有的請求將會跳轉到一個 Spring security 的頁面。 ![預設密碼](https://img2020.cnblogs.com/other/1419561/202008/1419561-20200819073734490-45924016.jpg) ![登入頁面](https://img2020.cnblogs.com/other/1419561/202008/1419561-20200819073734796-843952487.jpg) 這裡我們需要實現自己業務的登陸頁,所以我們需要自定義登入校驗邏輯。 在 Spring security 我們只需要實現 `UserDetailsService`介面,重寫 `loadUserByUsername`方法邏輯。 ```java @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` 中。 ```java @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](https://github.com/spring-projects/spring-session.git) 頁面。 ```html Login
Please Login - Invalid username and password. You have been logged out.
``` 這裡需要注意一點,這裡 **form** 表單的請求地址使用 `/auth/login`,我們需要在下面配置中修改,預設情況下登入請求的地址需要為 `/login`。 接著我們在上面的 `SecurityConfig` 類增加相應配置方法: ```java /** * 自定義處理登入處理 * * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests((authorize) -> authorize .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // 靜態資源,比如 css,js 無需登入鑑權 .anyRequest().permitAll() // 其他頁面需要登入鑑權 ).formLogin((formLogin) -> formLogin // 自定義登入頁面 .loginPage("/login") // 登入頁 .loginProcessingUrl("/auth/login") // 自定義登入請求地址 .permitAll()// 登入頁當然無需鑑權了,不然不就套娃了嗎? ).logout(LogoutConfigurer::permitAll // 登出頁面 ).rememberMe(rememberMe -> rememberMe .rememberMeCookieName("test-remember") // 自定義記住我 cookie 名 .key("test") // 鹽值 .tokenValiditySeconds(3600 * 12)) // 記住我,本地生成 cookie 包含使用者資訊 ; } ``` 這個方法可能比較長,重點解釋一下: - `authorizeRequests`方法內需要指定那些頁面需要鑑權,這裡我們指定靜態資源無需登入鑑權,其他請求我們都需要登入鑑權 - `formLogin` 方法內修改預設的登入頁面地址,以及登入的請求地址。 - `logout`在這裡面可以配置登出的相關配置。 - `rememberMe`開啟這個功能之後,當內部 Session 過期之後,使用者還可以根據使用者瀏覽器中的 Cookie 資訊實現免登入的功能。 最後我們需要配置一些頁面的跳轉地址: ```java @Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { // 首頁 registry.addViewController("/").setViewName("home"); // 登入之後跳轉到 home 頁 registry.addViewController("/login").setViewName("login"); } } ``` ## 總結 到此為止,我們已經整合 **Spring-Session** 與 **Spring-security** 完成完整的網站的登入鑑權功能。從這個例子可以看到,引入這個兩個框架之後,我們只需要按照 Spring 規範開發即可,其他複雜實現原理我們都不需要自己實現了,這樣真的很方便。 上面只是一個簡單的小例子,小黑只是拋轉引玉一下,真實開發中可能需要修改配置會更多,這裡需要使用小夥伴自己在深入研究了。 ## 參考 1. https://creaink.github.io/post/Backend/SpringBoot/Spring-boot-security.html 2. https://github.com/spring-projects/spring-session > 歡迎關注我的公眾號:程式通事,獲得日常乾貨推送。如果您對我的專題內容感興趣,也可以關注我的部落格:[studyidea.cn](https://studyidea.cn)