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


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

今天我們就使用這種方式對 Session 存儲方式進行改造,將其統一存儲到 Redis 中。

實現方案

我們先來想一下,如果我們不依靠任何框架,自己如何實現后端 Session 集中存儲。

這里我們假設我們的網站除了某些頁面,比如首頁可以直接訪問以外,其他任何頁面都需要登錄之后才能訪問。

如果需要實現這個需求,這就需要我們對每個請求都進行鑒權,鑒權目的是為了判斷用戶是否登錄,判斷用戶角色。

如果用戶沒有登錄,我們需要將請求強制跳轉到登錄頁面進行登錄。

用戶登錄之后,我們需要將登錄獲取到的用戶信息存儲到 Session 中,這樣后面請求鑒權只需要判斷 Session 中是否存在即可。

知道整個流程之后,其實實現原理就不是很難了。

我們可以使用類似 AOP 的原理,在每個請求進來之后,都先判斷 Session 中是否存在用戶信息,如果不存在就跳轉到登錄頁。

整個流程如下所示:

我們可以利用 Servelt Filter 實現上述流程,不過上述整套流程,Spring 已經幫我們實現了,那我們就不用重復造輪子了。

我們可以使用 Spring-SessionSpring-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="${_csrf.token}"/>
            <label>remember me: </label>
            <input type="checkbox" name="remember-me"/>
            <div class="form-actions">
                <button type="submit" class="btn">Log in</button>
            </div>
        </fieldset>
    </form>
</div>
</body>
</html>

這里需要注意一點,這里 form 表單的請求地址使用 /auth/login,我們需要在下面配置中修改,默認情況下登錄請求的地址需要為 /login

接着我們在上面的 SecurityConfig 類增加相應配置方法:

/**
 * 自定義處理登錄處理
 *
 * @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 信息實現免登錄的功能。

最后我們需要配置一些頁面的跳轉地址:

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        // 首頁
        registry.addViewController("/").setViewName("home");
        // 登錄之后跳轉到 home 頁
        registry.addViewController("/login").setViewName("login");
    }

}

總結

到此為止,我們已經集成 Spring-SessionSpring-security 完成完整的網站的登錄鑒權功能。從這個例子可以看到,引入這個兩個框架之后,我們只需要按照 Spring 規范開發即可,其他復雜實現原理我們都不需要自己實現了,這樣真的很方便。

上面只是一個簡單的小例子,小黑只是拋轉引玉一下,真實開發中可能需要修改配置會更多,這里需要使用小伙伴自己在深入研究了。

參考

  1. https://creaink.github.io/post/Backend/SpringBoot/Spring-boot-security.html
  2. https://github.com/spring-projects/spring-session

歡迎關注我的公眾號:程序通事,獲得日常干貨推送。如果您對我的專題內容感興趣,也可以關注我的博客:studyidea.cn


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM