Spring Boot 3中将JWT与Spring Security 6集成

在我们的 Spring Boot 应用程序中将 JWT(JSON Web 令牌)与 Spring Security 集成。这将使我们能够通过使用 JWT 整合强大的身份验证和授权机制来增强我们的安全框架。

目标:确保只有使用有效的 JWT 令牌才能访问关键端点。在我们的 Spring Boot 项目中,我们主要有两个关键的 REST 端点:一个用于获取所有员工数据,另一个用于添加新员工,这些端点是安全的,并且需要基于 JWT 的身份验证才能进行访问。

什么是JWT?
JWT(即 JSON Web 令牌)就像数字通行证一样,有助于确保 Web 应用程序的安全。当有人登录应用程序时,服务器会向他们提供 JWT。
这就像您每次回来时都会获得一枚徽章来证明您是谁。每次用户想要访问应用程序的安全部分时,他们都会显示此徽章,服务器知道让他们进入是安全的。
这就是 JWT 如此出色的原因:

  1. 易于管理:它们是简单的文本字符串,使其易于在计算机和设备之间处理和发送。
  2. 一体化:服务器验证用户所需的所有内容都打包到 JWT 中。这意味着服务器不必不断向数据库询问信息,从而加快速度。
  3. 安全:它们可以被加密和签名,使未经授权的人很难弄乱它们。

JWT 令牌的组成部分:
JWT 由三个主要部分组成,每个部分用点 ( .) 分隔。这是一个简单的细分:

  1. 标头
    • 目的:标头通常告诉我们令牌的类型(JWT)以及用于签名令牌的算法,例如HMAC SHA256 或 RSA。
    • 例子:{"alg": "HS256", "typ": "JWT"}
    • 说明:这是一个简单的 JSON 对象,表明 JWT 正在使用“HS256”算法进行加密。它以 Base64Url 格式进行编码,使其成为 URL 安全的。
  • 有效载荷
    • 目的:有效负载包含声明。声明是关于实体(通常是用户)和附加数据的声明。
    • 例子:{"sub": "1234589670", "name": "Gaurav Sharma", "admin": true}
    • 说明:此 JSON 对象包含有关用户和其他元数据的信息。它表示 JWT 的主体(用户)的 ID 为“ 1234589670”,名称为“ Gaurav Sharma”,并且具有管理权限。
  • 签名
    • 目的:签名用于验证 JWT 的发送者是否是其所说的发送者,并确保消息在此过程中没有被更改。
    • 制作方法:要创建签名,您需要获取编码标头、编码有效负载、秘密、标头中指定的算法(如 HMAC SHA256),然后对其进行签名。
    • 说明:例如,如果您使用 HMAC SHA256 算法,则将使用密钥将 HMAC SHA256 算法应用于 Base64Url 编码标头和负载的组合字符串来创建签名。

    这三个部分共同构成了 JWT:header.payload.signature.

    发送 JWT 时,它显示为由三个 Base64Url 编码部分组成的字符串,用点分隔,看起来像这样:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c。
    这本质上就是 JWT 的组成部分以及每个部分的功能。

    • 注意: JWT 已编码但未加密,因此如果被拦截则可以读取。因此,避免将密码等敏感数据直接存储在 JWT 中,因为任何有权访问的人都可以解码和提取此信息。
    • 相反,JWT 应仅包含用户识别和会话管理所需的必要信息,例如用户 ID 或用户名,以及有助于访问控制决策的权限或角色。为了确保令牌的完整性和真实性,它使用只有服务器知道的密钥进行数字签名。尽管可以查看内容,但签名有助于确保令牌在颁发后不会被更改。

    为什么选择JWT?
    有人可能会想:如果我们可以简单地使用用户名和密码来访问 Web 应用程序中的安全端点,那么为什么我们选择将 JWT 与 Spring Security 结合使用呢?

    选择带有 Spring Security 的 JWT(JSON Web 令牌)而不是仅仅使用用户名和密码有几个好处,特别是对于现代 Web 应用程序:

    1. 无需持续签入:JWT 包含所有用户信息,因此服务器可以验证您的身份,而无需在您每次发出请求时询问数据库。这对于处理大量用户来说非常有用,因为它减少了数据库流量。
    2. 额外的安全性:当您仍然使用用户名和密码登录时,JWT 提供了一种更安全的方式来在您登录后继续检查您的身份。令牌受到保护并具有内置的过期时间,这有助于防止以免它们被滥用。
    3. 无处不在:JWT 擅长跨不同系统和设备工作。这意味着一旦您登录到系统的某一部分,您也可以使用其他部分,而无需再次登录。
    4. 更快:因为您的服务器并不总是询问数据库您是谁,所以事情可以运行得更快。使用 JWT,您的服务器只需要检查您随请求发送的令牌,从而加快速度。
    5. 携带更多信息:JWT 还可以包含额外的详细信息,例如您拥有哪些权限,这有助于服务器知道您可以执行哪些操作,而无需一直查找。

    简而言之,JWT 使事情变得更安全、更快速、更易于管理,特别是当您有大量用户或需要跨系统的不同部分工作时。这使它们成为现代 Web 应用程序的有力选择。

    对于微服务,涉及多个服务器:

    •  JWT(JSON Web 令牌)广泛用于微服务架构,其中 Web 应用程序是使用处理不同服务的多个服务器构建的。在此类环境中,每次用户发出请求时都从特定服务器检索用户名和密码可能效率低下,并且会降低系统速度。
    •  JWT 通过将用户的身份和授权详细信息封装在安全令牌中来解决这一挑战。该令牌通常在用户登录后发出一次,然后用于跨不同服务的后续请求。此方法无需连续查询中央身份验证服务器或数据库,从而简化了跨多个服务器的身份验证流程并增强了整体系统性能。

    在多个小型服务协同工作的微服务设置中,JWT(JSON Web 令牌)为有效管理安全性提供了一些明显的好处:

    • 独立验证:每个微服务都可以使用 JWT 中的信息自行检查您是谁。无需不断要求中央服务器验证您的身份,从而减少了延迟。
    • 更少的网络流量:由于 JWT 在其内部携带所有需要的用户信息,因此服务不需要不断地相互通信或与中央数据库通信来检查用户详细信息。这减少了网络流量并加快了速度。
    • 跨服务工作:JWT 非常适合跨越不同区域或系统的设置。一旦您登录并拥有 JWT,系统中的任何服务都可以识别并信任它。
    • 不需要内存:JWT 有助于保持系统简单,因为服务不需要记住用户会话。所需的一切都在 JWT 中,它支持微服务的无状态操作。

    总体而言,JWT 使微服务架构中的安全性变得更顺畅、更快速且更具可扩展性,非常适合每个服务独立且安全地运行的需求。


    本文分为三个部分:

    1. 第一部分涉及开发一个基本 API 来添加员工和检索所有员工。
    2. 第二部分重点介绍在 Spring Security 中实现用户相关的类。
    3. 第三部分介绍了 JWT 与 Spring Security 的集成,以及在请求标头中使用 jwt 访问项目的关键端点。

    第 1 章:在 Spring Boot 中构建员工管理 API案例
    第 1 章提供了在 Spring Boot 中创建 API 以在员工管理系统中添加和检索员工的基本概述。

    第2章:在Spring Security中实现用户相关的类
    注意:将 JWT 与 Spring Boot 和 Spring Security 集成是一项艰巨的任务。

    为了设置我们的安全系统,我们需要创建一个包含用户名和密码等字段的用户类。这使我们能够将用户信息存储在数据库中并根据这些凭据对用户进行身份验证。

    然而,有一个重要的方面需要注意:Spring Security 不会自动识别这个自定义用户类。相反,它使用其预定义的UserDetails界面。

    简单来说,UserDetails是Spring Security中的一个特殊接口,旨在以Spring Security可以理解的方式处理用户信息。这意味着,为了让 Spring Security 能够使用我们的自定义用户类,我们需要调整我们的类以适应此接口。本质上,我们需要将用户类转换为实现该UserDetails接口的用户类。

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

    /**
     * UserInfo 类代表应用程序中的用户实体。
     * 用于存储用户相关数据,如姓名、密码等。
     */

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    @Entity
    @Table(name =
    "users")
    public class User {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Integer 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;
    import java.util.Set;

    public class UserPrincipal implements UserDetails {
        /**
         *
         */

        private static final long serialVersionUID = 1L;
        String userName = null;
        String password = null;
        Set<SimpleGrantedAuthority> authorities;

        public UserPrincipal(User user) {
            userName = user.getUserName();
            password = user.getPassword();
            authorities = Collections.singleton(new SimpleGrantedAuthority(
    "USER"));
        }

        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            return authorities;
        }

        @Override
        public String getPassword() {
            return password;
        }

        @Override
        public String getUsername() {
            return userName;
        }

        @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 的身份验证和授权机制集成。

    下面是其功能的详细介绍:

    • 类字段:该类有三个主要字段:userName、password 和 authorities。它们分别代表用户的登录凭证及其角色或权限。
    • 构造函数:UserPrincipal 构造函数将一个 User 对象作为参数。它从该用户对象中提取用户名和密码,并初始化用户的权限。
    • 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;

    import java.util.Optional;


    @Repository
    public interface UserRepo extends JpaRepository<User, Integer> {
        /**
         *findByName 方法用于根据用户名检索用户。
         * 返回 UserInfo 的一个可选项,如果没有找到用户,则返回空值。
         */

        Optional<User> findByUserName(String userName);
    }

    上述代码的解释:

    UserRepo 界面中的 findByUsername(String username) 方法是一个专门函数,可让你根据用户名查找和检索用户。

    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.password.PasswordEncoder;
    import org.springframework.stereotype.Service;

    import java.util.Optional;

    /**
     * UserInfoService 提供与用户相关的服务,包括加载用户详细信息
     *和管理存储库中的用户数据。
     */

    @Service
    public class UserService implements UserDetailsService {

        @Autowired
        private UserRepo userRepo;

        @Autowired
        private PasswordEncoder passwordEncoder;


       
    // 根据用户名加载用户的详细信息。
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            Optional<User> user = userRepo.findByUserName(username);
            return user.map(UserPrincipal::new)
                    .orElseThrow(() -> new UsernameNotFoundException(
    "UserName not found: " + username));
        }


       
    // 向版本库添加新用户,并在保存密码前对其进行加密。
        public String addUser(User user) {
            user.setPassword(passwordEncoder.encode(user.getPassword()));
            userRepo.save(user);
            return
    "user added successfully";
        }


    }

    上述代码的解释:

    • UserService 类实现了 Spring Security 的 UserDetailsService 接口,提供了一个从数据库加载用户详细信息的方法。这可确保该类与 Spring Security 的身份验证流程无缝协作。
    • UserService 类负责在 Spring Security 框架内处理用户信息。它包括两个主要方法:
    • loadUserByUsername: 该方法使用提供的用户名从数据库中检索用户详细信息。如果用户存在,它将返回封装在 UserPrincipal 对象中的详细信息,供 Spring Security 的身份验证流程使用。如果找不到用户名,则会抛出异常。
    • addUser:该方法获取一个 User 对象,使用 PasswordEncoder 对用户密码进行安全加密,并将更新后的用户保存到数据库中。该方法会以成功消息确认添加成功。

    第3章:将 JWT 与 Spring Security 集成
    要在 Spring Boot Maven 项目中实现用于授权的 JSON Web 标记 (JWT),我们需要在 pom.xml 文件中包含特定的依赖项。

    <dependency>
                <groupId>io.jsonwebtoken</groupId>
                <artifactId>jjwt-jackson</artifactId>
                <version>0.11.5</version>
            </dependency>
            <dependency>
                <groupId>io.jsonwebtoken</groupId>
                <artifactId>jjwt-api</artifactId>
                <version>0.11.5</version>
            </dependency>
            <dependency>
                <groupId>io.jsonwebtoken</groupId>
                <artifactId>jjwt-impl</artifactId>
                <version>0.11.5</version>
            </dependency>

    上述依赖关系的解释:

    1. jjwt-jackson作用:将 Jackson 库与 JWT 操作集成,优化 Java 应用程序对 JWT 中 JSON 数据的编码和解码。
    2. jjwt-api有什么作用?提供用于构建和验证 JWT 的基本类和接口,为 Java 中的 JWT 功能提供构建模块。
    3.  jjwt-impl作用提供 jjwt-api 接口的实际实现,可根据应用程序的安全协议生成、解析和管理 JWT。

    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>io.jsonwebtoken</groupId>
                <artifactId>jjwt-jackson</artifactId>
                <version>0.11.5</version>
            </dependency>
            <dependency>
                <groupId>io.jsonwebtoken</groupId>
                <artifactId>jjwt-api</artifactId>
                <version>0.11.5</version>
            </dependency>
            <dependency>
                <groupId>io.jsonwebtoken</groupId>
                <artifactId>jjwt-impl</artifactId>
                <version>0.11.5</version>
            </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>


    pom.xml 文件中的每个依赖项:

    • Spring Boot Starter Data JPA:将 Spring Data JPA 与 Spring Boot 集成,使用 JPA 资源库简化数据库交互。
    • Spring Boot Starter 安全:为 Spring Boot 应用程序添加身份验证和授权等安全功能。
    • Spring Boot Starter Web:提供了使用 Spring MVC 构建 Web 应用程序(包括 RESTful 应用程序)的所有必需品。
    • Spring Boot DevTools:提供自动重启工具和实时重载功能,以提高开发人员的工作效率。
    • PostgreSQL 驱动程序:实现与 PostgreSQL 数据库的 JDBC 连接,允许在应用程序和数据库之间进行数据交易。
    • Lombok:通过自动生成获取器、设置器、构造器等,减少 Java 应用程序中的模板代码。
    • jjwt-jackson:为处理 JSON 网络令牌的 JJWT 库提供 Jackson JSON 处理功能。
    • jjwt-api:为高效创建和验证 JSON 网络令牌(JWT)提供 API 支持。
    • jjwt-impl:实现 JJWT API,提供管理 JWT 的必要代码。

    现在,我们将定义一些类,以便将 jwt 集成到我们的安全机制中。

    JwtService 类用于创建、检查和验证安全令牌(JWT),以帮助确认应用程序中的用户身份。
    JwtService.java


    import io.jsonwebtoken.Claims;
    import io.jsonwebtoken.Jwts;
    import io.jsonwebtoken.SignatureAlgorithm;
    import io.jsonwebtoken.io.Decoders;
    import io.jsonwebtoken.security.Keys;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.stereotype.Component;

    import java.security.Key;
    import java.util.Date;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.function.Function;


    //JwtService 负责处理 JWT(JSON 网络令牌)操作
    //,例如令牌生成、提取声明和令牌验证。

    @Component
    public class JwtService {

       
    // 用于签署 JWT 的密钥。该密钥应保密。
        private static final String SECRET =
    "TmV3U2VjcmV0S2V5Rm9ySldUU2lnbmluZ1B1cnBvc2VzMTIzNDU2Nzg=\r\n" + "";


       
    // 为给定的用户名生成 JWT 标记。
        public String generateToken(String userName) {
           
    // 准备token声明
            Map<String, Object> claims = new HashMap<>();

           
    // 创建包含声明、主题、发布时间、过期时间和签名算法的 JWT 令牌
             
    // 令牌有效期为 3 分钟
            return Jwts.builder()
                    .setClaims(claims)
                    .setSubject(userName)
                    .setIssuedAt(new Date(System.currentTimeMillis()))
                    .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 3)) 
                    .signWith(getSignKey(), SignatureAlgorithm.HS256).compact();
        }


       
    //根据 base64 编码的密文创建签名密钥。
       
    // 返回一个用于签署 JWT 的密钥对象。
        private Key getSignKey() {
           
    // 解码 base64 编码秘钥并返回一个密钥对象
            byte[] keyBytes = Decoders.BASE64.decode(SECRET);
            return Keys.hmacShaKeyFor(keyBytes);
        }


       
    //从 JWT 标记中提取用户名。
       
    //return -> The userName contained in the token.
        public String extractUserName(String token) {
           
    // 从标记中提取并返回主题请求
            return extractClaim(token, Claims::getSubject);
        }


       
    // 从 JWT 令牌中提取到期日期。
       
    //@return The expiration date of the token.
        public Date extractExpiration(String token) {
           
    // 从令牌中提取并返回有效期声明
            return extractClaim(token, Claims::getExpiration);
        }


       
    //从 JWT 标记中提取特定的请求。
       
    // return-> The value of the specified claim.
        private <T> T extractClaim(String token, Function<Claims, T> claimResolver) {
           
    //使用提供的函数提取指定的索赔
            final Claims claims = extractAllClaims(token);
            return claimResolver.apply(claims);
        }

       
    //从 JWT 标记中提取所有请求。
       
    //return-> Claims 对象包含所有请求。
        private Claims extractAllClaims(String token) {
           
    //解析并返回令牌中的所有请求
            return Jwts.parserBuilder()
                    .setSigningKey(getSignKey())
                    .build().parseClaimsJws(token).getBody();
        }


       
    //检查 JWT 令牌是否过期。
       
    //return-> 如果令牌已过期,则返回 true,否则返回 false。
        public Boolean isTokenExpired(String token) {
           
    // 检查令牌的到期时间是否早于当前时间
            return extractExpiration(token).before(new Date());
        }

      
    //根据 UserDetails 验证 JWT 令牌。
       
    //return-> 如果令牌有效,则返回 true,否则返回 false。

        public Boolean validateToken(String token, UserDetails userDetails) {
           
    // 从令牌中提取用户名,并检查是否与 UserDetails 的用户名匹配
            final String userName = extractUserName(token);
           
    // 同时检查令牌是否过期
            return (userName.equals(userDetails.getUsername()) && !isTokenExpired(token));
        }
    }

    ‍JwtService 类中的关键方法:

    • generateToken生成令牌:为用户生成一个安全令牌,其中包括用户名和令牌的有效期。
    • getSignKey:从密钥中创建一个特殊密钥,以确保令牌的安全。

     JwtService 类中的秘钥就像一个数字签名,用于确保 JSON Web 标记(JWT)的安全。该密钥采用 base64 编码,这种方法可将密钥转换为字符串格式,以便在编程环境中更容易处理。它的主要作用是确保 JWT 中的数据不被未经授权的方篡改。对该密钥进行保密至关重要;如果它落入坏人之手,就会有人伪造令牌,从而有可能在未经授权的情况下访问用户数据和特权系统功能。因此,保持密钥的机密性对应用程序的安全至关重要。

    如果您想生成自己的密钥,请看一个小演示:
    创建密钥的有用网站是:
    https://asecuritysite.com/cryption/plain

    •  extractUserName:从令牌中获取用户名。
    •  extractExpiration:找出令牌何时停止工作。
    •  extractClaim:extractClaim类中的方法使用JwtService作为参数传递的函数从解析的 JWT 令牌中检索特定声明,例如用户名或到期日期。
    •  isTokenExpired:检查令牌是否已超时且不再有效。
    •  validateToken:确保令牌仍然有效并且与正确的用户匹配,确认它可以安全地用于登录。

    下面JwtFilter检查每个传入请求,以确保用户具有有效的登录令牌,并在一切检查完毕后设置其登录状态。


    import com.unlogged.service.JwtService;
    import com.unlogged.service.UserService;
    import jakarta.servlet.FilterChain;
    import jakarta.servlet.ServletException;
    import jakarta.servlet.http.HttpServletRequest;
    import jakarta.servlet.http.HttpServletResponse;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.ApplicationContext;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
    import org.springframework.stereotype.Component;
    import org.springframework.web.filter.OncePerRequestFilter;

    import java.io.IOException;

    @Component
    public class JwtFilter extends OncePerRequestFilter {

        @Autowired
        private JwtService jwtService;

        @Autowired
        private ApplicationContext applicationContext;

        // 从 ApplicationContext 轻松获取 UserService Bean 的方法
       
    // 这样做是为了避免循环依赖问题
        private UserService getUserService() {
            return applicationContext.getBean(UserService.class);
        }

        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
           
    // 从请求标头提取令牌
            String authHeader = request.getHeader(
    "Authorization");
            String token = null;
            String userName = null;

            if (authHeader != null && authHeader.startsWith(
    "Bearer ")) {
               
    // 从授权标头提取令牌
                token = authHeader.substring(7);
               
    // Extracting username from the token
                userName = jwtService.extractUserName(token);
            }

           
    //如果提取了用户名,且当前安全上下文中没有验证
            if (userName != null && SecurityContextHolder.getContext().getAuthentication() == null) {
               
    // 根据从令牌中提取的用户名加载 UserDetails
                UserDetails userDetails = getUserService().loadUserByUsername(userName);

               
    // 用加载的用户详情验证令牌
                if (jwtService.validateToken(token, userDetails)) {
                   
    // 使用 UserDetails 创建身份验证令牌
                    UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                   
    // 设置身份验证详细信息
                    authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                   
    // 在安全上下文中设置身份验证令牌
                    SecurityContextHolder.getContext().setAuthentication(authToken);
                }
            }

           
    // Proceeding with the filter chain
            filterChain.doFilter(request, response);
        }
    }


    JwtFilter类中的每个方法:

    1. getUserService:getUserService该类中的方法旨在JwtFilter仅UserService在需要时从 Spring 应用程序上下文中检索实例。此方法用于防止与 Spring bean 加载顺序相关的问题(称为循环依赖问题)。本质上,它确保UserService在访问时可用并完全初始化,避免由于依赖性冲突而导致任何启动错误。
    2. doFilter内部:
      • 目标用途:检查每个传入 HTTP 请求的授权标头中的 JWT。
      • 处理流程:如果找到有效的 JWT,它会提取用户名并检查以SecurityContextHolder确定用户是否已通过当前会话的身份验证。
      • 用户身份验证:如果用户未登录,则会检索用户详细信息,根据这些详细信息验证令牌,如果验证通过,则在 SecurityContextHolder 中设置用户的身份验证。
      • 会话管理:该机制集中安全信息,促进跨应用程序的用户身份验证和授权的轻松访问和管理。
      • 流程继续:允许请求前进到过滤器链中的下一个阶段或预期的最终目的地。


    SecurityContextHolder:这是 Spring Security 的一部分,用于保存当前线程的安全上下文(即有关当前用户及其权限的详细信息)。它贯穿整个应用程序,用于授予或限制对系统不同部分的访问权限。

    ‍ SecurityConfig.java 类为网络应用程序设置安全规则,以确保只有授权用户才能访问特定区域。


    import com.unlogged.filter.JwtFilter;
    import com.unlogged.service.UserService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.authentication.AuthenticationProvider;
    import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
    import org.springframework.security.config.Customizer;
    import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    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.crypto.password.PasswordEncoder;
    import org.springframework.security.web.SecurityFilterChain;
    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    import org.springframework.web.bind.annotation.CrossOrigin;

    @Configuration
    @EnableWebSecurity
    @CrossOrigin
    public class SecurityConfig {

        @Autowired
        private JwtFilter jwtFilter;

        // 定义用于用户身份验证的 UserDetailsService Bean
        @Bean
        public UserDetailsService userDetailsService() {
            return new UserService();
        }


       
    // 配置安全过滤链
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
            return httpSecurity
                    .cors(Customizer.withDefaults())
    // Apply CORS
                    .csrf(csrf -> csrf.disable())
    // Disable CSRF protection
                    .authorizeHttpRequests(auth -> auth
                            .requestMatchers(
    "/auth/addUser", "/auth/login")
                            .permitAll()
    // Permit all requests to certain URLs
                            .anyRequest().authenticated())
    // Require authentication for all other requests
                    .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
    // Set session management to stateless
                    .authenticationProvider(authenticationProvider())
    // Register the authentication provider
                    .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
    // Add the JWT filter before processing the request
                    .build();
        }


       
    //创建 DaoAuthenticationProvider 以处理用户身份验证
        @Bean
        public AuthenticationProvider authenticationProvider() {
            DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
            authenticationProvider.setUserDetailsService(userDetailsService());
            authenticationProvider.setPasswordEncoder(passwordEncoder());
            return authenticationProvider;
        }


       
    //定义 PasswordEncoder Bean,默认使用 bcrypt 哈希算法对密码进行编码
       @Bean
        PasswordEncoder passwordEncoder() {
            return PasswordEncoderFactories.createDelegatingPasswordEncoder();
        }


       
    //定义 AuthenticationManager Bean 以管理身份验证流程
        @Bean
        public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
            return config.getAuthenticationManager();
        }

    }


    上述代码的解释:

    SecurityConfig 类中定义的每个方法:

    • userDetailsService():初始化用于加载用户特定数据的服务。它将应用程序连接到用户服务,而用户服务会检索必要的用户信息,以便进行身份验证。

    securityFilterChain(HttpSecurity httpSecurity):
    为应用程序中的 HTTP 请求配置安全策略。它设置了如何对请求进行身份验证和授权。主要配置包括
    • CORS 配置:应用跨起源资源共享的默认设置,以管理不同起源之间的资源交互。
    • CSRF 保护:禁用跨站请求伪造保护,通常用于使用基于令牌的身份验证而不是 cookie 的 API。
    • 授权规则:定义访问规则,允许不受限制地访问某些路径,如 /auth/addUser 和 /auth/login,并要求对所有其他请求进行身份验证。
    • 会话管理:将会话设置为无状态,即服务器不会在请求之间维护任何会话状态。
    • 身份验证提供程序:集成 DaoAuthenticationProvider,使用用户详情服务和密码编码器来验证用户身份。
    • JWT 过滤器集成:在标准用户名-密码身份验证过滤器之前集成一个 JWT 过滤器,以处理和验证请求中的 JWT。

    authenticationProvider():
    创建一个使用数据库处理用户身份验证的身份验证提供程序。该提供程序会检索用户详细信息,并使用指定的编码器验证密码。

    passwordEncoder():
    建立密码编码方法,默认使用 BCrypt 算法,该算法在存储密码前会对密码进行安全散列。

    authenticationManager(AuthenticationConfiguration config):身份验证管理器:
    提供一个管理器来查看身份验证过程,这对处理身份验证请求至关重要,也是 Spring Security 身份验证框架的核心。