使用Spring Security 6.1及更高版本保護Spring Boot 3應用

banq發表於2024-04-18

在本文中,我們將探討如何利用 Spring Security 的最新更新來保護使用最新版本的 Spring Boot 開發的 Web 應用程式的安全。我們的旅程將引導我們建立一個 Spring Boot Web 專案、透過 Spring Data JPA 與 PostgreSQL 資料庫整合,以及應用更新的 Spring Security 框架提供的安全措施。

本文主要分為兩部分。

第一部分: 開發具有CRUD操作的員工管理系統
在這個初始階段,我們將專注於打造一個具有基本 CRUD(建立、讀取、更新、刪除)操作的員工管理系統。我們將為我們的系統奠定基礎,為後續的增強奠定基礎。

第二部分:使用 Spring Security 增強端點保護的安全性
我們文章的關鍵在於利用 Spring Security 提供的強大安全措施來強化我們的端點。
我們將深入研究保護應用程式的安全,確保只有授權使用者才能訪問敏感端點。這一步驟提高了我們員工管理系統的完整性和保密性,增強了其整體可靠性和可信度。

第一章:開發一個簡單的基於員工的管理系統
我們不會詳細介紹構建 CRUD 應用程式的細節,因為本文的重點是保護應用程式。不過,為了更好地理解,提供了基本概述。
我們在專案中包含了以下依賴項:

  1. Spring Web:支援使用 Spring 構建 Web 應用程式,包括 RESTful 服務。
  2. PostgreSQL 驅動程式:將您的應用程式連線到 PostgreSQL 資料庫以進行資料儲存。
  3. Spring Data JPA:透過 JPA 儲存庫簡化資料訪問和操作。
  4. Lombok:透過自動生成 getter、setter 和其他常用方法來減少樣板程式碼。
  5. Spring Boot DevTools:提供快速應用程式重啟、實時重新載入和配置選項,以實現更順暢的開發過程。

第二章:使用預設 Spring Security 配置保護我們的 Web 應用程式
自動將 Spring Security 新增到您的 Spring Boot 專案中可以使其更安全。這是因為 Spring 的建立者決定希望每個應用程式從一開始就是安全的。

一旦您將 Spring Security 新增到您的專案中,它就會立即為您設定一些安全功能。這意味著您的應用程式將具有基本的安全級別,而無需您執行任何額外操作。

為了保護我們的 Web 應用程式,我們需要另一個依賴項,即 Spring Security:
Spring Security:新增身份驗證和授權功能以保護您的應用程式。
鑑於我們的專案是使用 Maven 和 Spring Boot 構建的,Spring Security 的依賴關係將出現在檔案中,pom.xml如下所示:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

最終pom.xml檔案的結構如下

<?xml version=<font>"1.0" encoding="UTF-8"?>
<project xmlns=
"http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation=
"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.unlogged</groupId>
    <artifactId>EmployeeManagementSystem</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>EmployeeManagementSystem</name>
    <description>EmployeeManagementSystem</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

注意:

  • 合併安全依賴項並啟動 spring boot 專案後,會自動提供預設的登入表單,其中包含使用者名稱和密碼欄位。
  • 當嘗試透過瀏覽器訪問任何 API 時,將顯示預設登入表單
  • Spring Security 提供的預設使用者名稱是“ user ”,而密碼是自動生成的,可以在控制檯中找到。

依賴預設的 Spring Security 設定會帶來一些限制:

  1. 保護一切:預設情況下,它會鎖定您的所有端點,甚至是您可能想要保持開啟狀態的端點。
  2. 不夠靈活:預設的安全設定非常籠統。如果您的應用程式需要特定的安全調整,您可能會發現這些設定有點限制。
  3. 容易配置錯誤:如果您不小心,堅持使用預設設定可能會導致安全漏洞或棘手的錯誤。
  4. 一刀切:從安全形度來看,它對所有應用程式一視同仁,這可能不適用於具有獨特安全需求的應用程式。

為了更精確地控制我們應用程式的安全機制,例如我們自己的使用者名稱、密碼和密碼加密以更好地進行身份驗證,以及訪問某些 API 的授權,我們需要建立一個自定義安全配置檔案來管理所有這些。 Spring Security 擅長為此類定製提供靈活性。

使用我們自己的自定義安全配置保護 Web 應用程式
為了設定我們的安全系統,我們需要建立一個包含使用者名稱和密碼等欄位的使用者類。這使我們能夠將使用者資訊儲存在資料庫中並根據這些憑據對使用者進行身份驗證。

然而,有一個重要的方面需要注意:Spring Security 不會自動識別這個自定義使用者類。相反,它使用其預定義的UserDetails介面。

簡單來說,UserDetails是Spring Security中的一個特殊介面,旨在以Spring Security可以理解的方式處理使用者資訊。這意味著,為了讓 Spring Security 能夠使用我們的自定義使用者類,我們需要調整我們的類以適應此介面。本質上,我們需要將使用者類轉換為實現該UserDetails介面的使用者類。


import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Table(name = <font>"users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;
    private String username;
    private String password;

}

對上述程式碼的解釋:
此程式碼設定一個簡單的user類來將使用者資訊儲存在資料庫中,特別是他們的 ID、使用者名稱和密碼

實現spring security提供的UserDetails介面:
UserPrincipal.java


import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.Collections;

public class UserPrincipal implements UserDetails {

    private User user;

    public UserPrincipal(User user) {
        this.user = user;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Collections.singleton(new SimpleGrantedAuthority(<font>"USER"));
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

UserPrincipal 類是 Spring Security 的 UserDetails 介面的自定義實現,旨在將我們自己的使用者模型與 Spring Security 的身份驗證機制整合在一起。

該類是使用者類與 Spring Security 所期望的使用者詳細資訊之間的介面卡。

下面是它的功能分解:

  • 建構函式:它接收 User 類的例項。這樣,UserPrincipal 就可以訪問特定使用者的詳細資訊,如使用者名稱和密碼。
  • getAuthorities():該方法指定授予使用者的角色或許可權。在本例中,每個使用者都被授予 "USER "的單一許可權。
  • getPassword() 和 getUsername():這些方法只是分別從使用者例項中獲取密碼和使用者名稱。
  • 賬戶狀態方法:isAccountNonExpired()、isAccountNonLocked()、isCredentialsNonExpired() 和 isEnabled() 方法都被過載為返回 true。Spring Security 使用這些方法來確定賬戶是否仍處於活動狀態、是否已鎖定、憑據是否過期或是否已啟用。所有這些方法都返回 true 表明,在這個簡單的實現中,這些檢查並不是用來限制使用者訪問的。

UserRepo.java


import com.unlogged.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepo extends JpaRepository<User, Integer> {

    public User findByUsername(String username);
}

UserRepo 介面中的 findByUsername(String username) 方法是一個專門函式,可讓您根據使用者的使用者名稱查詢和檢索使用者。透過設定該方法,Spring Data JPA 可以自動處理資料庫搜尋,這意味著您無需編寫任何額外的 SQL 程式碼。如果找到匹配的使用者,它將返回 User 物件;如果沒有該使用者名稱的使用者,則返回 null。

UserService.java

import com.unlogged.model.User;
import com.unlogged.model.UserPrincipal;
import com.unlogged.repo.UserRepo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

@Service
public class UserService implements UserDetailsService {
    @Autowired
    private UserRepo userRepo;
    private final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12);

    public User saveUser(User user) {
        user.setPassword(encoder.encode(user.getPassword()));
        return userRepo.save(user);
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepo.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException(<font>"Error 404");
        } else {
            return new UserPrincipal(user);
        }
    }
}

我們應用程式中的 UserService 類有兩項主要工作:管理使用者資訊和幫助確保登入安全。

UserDetailsService 是 Spring Security 提供的一個介面,用於檢索使用者相關資料。它只有一個方法,即 loadUserByUsername(String username),必須實現該方法才能根據使用者名稱獲取 UserDetails 物件。UserDetails 介面本身就是 Spring Security 的核心部分,它提供了安全檢查所需的基本資訊(如使用者名稱、密碼和授權)。

下面我們就來快速瞭解一下它是如何工作的:

  • 使用者儲存庫和密碼編碼器:該類連線到我們的資料庫以訪問使用者資訊,並使用名為 BCryptPasswordEncoder 的工具確保密碼安全。該工具會擾亂密碼,使其不易被猜測或竊取。
  • saveUser 方法:saveUser 方法:每當我們需要儲存一個新使用者的資訊時,該方法首先會對使用者的密碼進行雜湊,然後將其詳細資訊儲存到我們的資料庫中。這樣,即使有人進入了我們的資料庫,也不會輕易解密密碼。
  • loadUserByUsername 方法:這種方法主要是在有人嘗試登入時找到正確的使用者。它透過使用者名稱搜尋使用者。如果找到了使用者,它就會以一種特殊格式準備使用者資訊,以便在登入時核對使用者身份。如果找不到使用者,它就會丟擲錯誤通知我們,這有助於防止陌生人登入。

總之,UserService 是保證使用者詳細資訊保安和確保正確人員使用正確密碼登入的關鍵。


import com.unlogged.model.User;
import com.unlogged.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
@CrossOrigin
public class UserController {

    @Autowired
    private UserService userService;

    @PostMapping(<font>"/register")
    public ResponseEntity<String> userRegister(@RequestBody User user) {
        if (userService.saveUser(user) != null) {
            return new ResponseEntity<>(
"User Registered Successfully", HttpStatus.OK);
        } else {
            return new ResponseEntity<>(
"Oops! User not registered", HttpStatus.OK);
        }
    }
}

UserController 類有一個名為 userRegister 的方法,用於管理註冊新使用者的過程。當使用者註冊成功時,它會傳送一條資訊 "User Registered Successfully(使用者註冊成功)";如果註冊失敗,它會回覆 "Oops!使用者未註冊"。

最重要的類是 SecurityConfig.java,我們在這裡定義了所有與安全相關的最新 Bean。

SecurityConfig.java

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@EnableWebSecurity
@Configuration
public class SecurityConfig {

    @Autowired
    private UserDetailsService userDetailsService;

    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService);
        provider.setPasswordEncoder(new BCryptPasswordEncoder(12));
        return provider;
    }


    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.csrf(AbstractHttpConfigurer::disable)
        .cors(Customizer.withDefaults())
                .authorizeHttpRequests(auth ->
                        auth
                                .requestMatchers(<font>"/register")
                                .permitAll().anyRequest()
                                .authenticated())
                .sessionManagement(session ->
                        session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .httpBasic(Customizer.withDefaults());


        return httpSecurity.build();
    }


}

與 Spring Security 以前的版本(即 Spring Security 6 以下版本)進行比較:

  • 從 AntMatchers 到 RequestMatchers 的過渡:

以前,AntMatchers 用於指定 URL 模式,以控制 Spring Security 的訪問。現在,我們使用的是 RequestMatchers,這是一種用途更廣的工具,它不僅能匹配 URL,還能考慮 HTTP 請求型別等其他因素。這樣就能進行更詳細、更靈活的安全配置
  • 淘汰 WebSecurityConfigurerAdapter:

設定安全配置的舊方法涉及擴充套件 WebSecurityConfigurerAdapter 類。Spring Security 不再使用這種方法,而是鼓勵使用更現代的方法,如直接配置 SecurityFilterChain Bean。這種方法不那麼死板,模組化程度更高,更容易根據需要自定義安全設定。
  • 採用 DSL lambdas:

配置程式碼現在使用 DSL(特定域語言)lambdas,它更簡潔、更靈活。這種方法利用 lambda 表示式(本質上是可以傳遞的匿名函式或程式碼塊)以更直接和可讀的方式配置設定。
  • 引入 SecurityFilterChain:

我們現在不再在一個大類中全域性配置安全設定,而是定義一個 SecurityFilterChain Bean,以類似鏈的方式處理安全問題。這種方法將安全配置分解為更小、更易於管理的部分,從而增強了定製性和清晰度。例如,在我們的設定中,我們可以明確說明哪些端點對所有使用者開放,哪些需要身份驗證,並以無狀態方式管理會話的處理方式

上述程式碼說明:- SecurityConfig 類

1. authenticationProvider() 方法
該方法用於設定規則,以檢查誰在嘗試訪問我們的應用程式:

  • User Details Service使用者詳細資訊服務:將其視為查詢使用者資訊的一種方式。當有人嘗試登入時,該服務會透過檢查他們提供的使用者名稱和密碼來幫助系統驗證他們的身份。
  • Password Encoder密碼編碼器:這部分設定使用一種特殊方法(BCrypt)來安全處理密碼。當使用者建立密碼時,這種方法會將密碼擾亂成一種很難解碼的格式。這意味著即使有人在未經授權的情況下獲取了加擾密碼,也很難找出實際密碼。

從本質上講,authenticationProvider() 方法為我們的應用程式做好準備,以安全地檢查使用者是否是他們聲稱的那個人,並安全地處理他們的密碼。

2. securityFilterChain(HttpSecurity httpSecurity) 方法
該方法設定了允許在應用程式中使用的內容以及安全管理方式的規則:

  • 關閉 CSRF 保護:CSRF 是一種攻擊型別,它會誘使使用者執行他們本不想執行的操作。對於許多應用程式,尤其是那些不與使用者保持持續對話的應用程式(如應用程式介面),關閉這種保護是安全的。
  • 控制訪問:我們明確規定,任何人都可以訪問 /register 端點,而無需登入(這對新使用者註冊非常有用)。應用程式中的其他所有請求(或操作)都需要使用者登入。
  • 會話管理:我們將應用程式配置為不保留任何使用者會話記錄。這意味著向伺服器發出的每個請求都必須包含憑據,從而使應用程式介面等無狀態應用程式更加安全。
  • 基本身份驗證:這是一種簡單的安全措施,要求使用者在請求時提供使用者名稱和密碼。

透過設定 SecurityFilterChain,我們可以一步步告訴應用程式如何處理安全檢查和使用者訪問。這種配置有助於保證應用程式的安全,並確保只有授權使用者才能訪問某些功能。

第 3 章:一些附加內容:
1、在 Spring Security 中增強 CORS 配置
使用 React 或 Angular 等框架開發 Web 應用程式時,在使用 API 時會遇到一個常見問題,即跨源資源共享 (CORS) 問題。

CORS 是瀏覽器實施的一種安全策略,用於防止在其他域託管的頁面上執行的指令碼向您的伺服器發出請求,除非明確允許。

在 Spring Boot 中的 REST 控制器類上簡單地使用 @CrossOrigin 註解,最初似乎是啟用跨源請求的解決方案。該註解配置了必要的 HTTP 標頭,以允許特定控制器進行跨源互動。

但是,當 Spring Security 整合到 Spring Boot 應用程式中時,配置 CORS 就變得稍微複雜一些。Spring Security 對 CORS 和安全標頭的處理更為嚴格,這意味著僅使用 @CrossOrigin 註解可能不足以完全處理 CORS 問題。

要在受 Spring Security 保護的應用程式中有效配置 CORS,您需要擴充套件安全配置以明確允許跨源請求。

這就需要在 SecurityConfig.java 類中定義一個額外的 Bean,或者調整安全過濾器鏈以包含正確的 CORS 配置。

下面是如何做到這一點的詳細說明:

為 CORS 擴充套件 Spring 安全配置:
定義 CORS 配置源:這是一個關鍵步驟,您可以在此指定允許使用的起源、HTTP 方法和標頭。它包括建立一個概述這些策略的 CorsConfigurationSource Bean。要讓在埠 3000 上執行的 React 應用程式成功使用受 Spring Security 保護的後端 API,您需要在 Spring Boot 應用程式中適當配置 CORS。這種設定可確保 React 應用程式能夠向安全的後端發出跨源請求。

@Bean
CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration configuration = new CorsConfiguration();
    configuration.setAllowedOrigins(Arrays.asList(<font>"http://localhost:3000"));
    configuration.setAllowedMethods(Arrays.asList(
"GET", "POST", "PUT", "DELETE", "OPTIONS"));
    configuration.setAllowCredentials(true);
    configuration.setAllowedHeaders(Arrays.asList(
"Authorization", "Content-Type"));
    configuration.setExposedHeaders(Arrays.asList(
"Authorization"));

    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration(
"/**", configuration);
    return source;
}

將 CORS 與 Spring Security 整合:定義 CORS 配置源後,必須將此配置與 Spring Security 整合。具體方法是在 SecurityFilterChain 方法中修改 HttpSecurity 物件,以應用 CORS 設定。
現在,我們最終的 SecurityFilterChain Bean 將是這樣的:

@Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.csrf(AbstractHttpConfigurer::disable)
                .cors(c -> c.configurationSource(corsConfigurationSource()))
                .authorizeHttpRequests(auth ->
                        auth
                                .requestMatchers(<font>"/register")
                                .permitAll().anyRequest()
                                .authenticated())
                .sessionManagement(session ->
                        session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .httpBasic(Customizer.withDefaults());


        return httpSecurity.build();
    }

透過這些步驟,您可以確保在 Spring Boot 應用程式中使用 Spring Security 適當地處理 CORS,從而實現來自託管在不同域上的前端應用程式的安全跨源請求。與單獨使用 @CrossOrigin 註解相比,這種配置允許更靈活、更安全的設定。


 

相關文章