使用Spring Security 6.1及更高版本保护Spring Boot 3应用

在本文中,我们将探讨如何利用 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="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 = "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("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("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("/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("/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("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("/register")
                                .permitAll().anyRequest()
                                .authenticated())
                .sessionManagement(session ->
                        session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .httpBasic(Customizer.withDefaults());


        return httpSecurity.build();
    }

通过这些步骤,您可以确保在 Spring Boot 应用程序中使用 Spring Security 适当地处理 CORS,从而实现来自托管在不同域上的前端应用程序的安全跨源请求。与单独使用 @CrossOrigin 注解相比,这种配置允许更灵活、更安全的设置。