结合Spring Security的两种用户登陆认证以及授权方案

方案一:在 SecurityFilterChain 中使用 form.login 进行有状态的用户登录与认证

方案一关键点

  • 自动启用默认的UsernamePasswordAuthenticationFilter(只要在在 SecurityFilterChain 中使用 form.login就会自动加入)
  • 默认的UsernamePasswordAuthenticationFilter内部会调用AuthenticationManager的authenticate() 方法
  • 因为Spring Security默认配置了DaoAuthenticationProvider(用于处理UsernamePasswordAuthenticationToken 类型的对象),所以当调用AuthenticationManager的authenticate() 方法,就会调用DaoAuthenticationProvider(这里需要传入一个UsernamePasswordAuthenticationToken 类型的参数)
  • 因为Spring Security默认配置了DaoAuthenticationProvider,所以在方案二中也可以通过authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password))的方式来创建认证过的Authentication类型的对象。
  • Spring Security默认配置的DaoAuthenticationProvider中需要通过UserDetailsService的实现类与PasswordEncoder来验证用户的身份(是否在数据库中存在,密码是否对的上)所以还要实现这两个接口。

DaoAuthenticationProvider 的自动配置条件:

  • 存在 UserDetailsService Bean :当你在Spring上下文中定义了一个实现了 UserDetailsService 接口的Bean时,Spring Security会自动配置一个 DaoAuthenticationProvider
  • 存在 PasswordEncoder Bean :为了确保密码的安全性,通常需要配置一个 PasswordEncoder Bean(如 BCryptPasswordEncoder),这也是 DaoAuthenticationProvider 所依赖的。

1. 项目依赖配置

确保项目的 pom.xml(Maven)或 build.gradle(Gradle)中包含以下必要的依赖。这些依赖提供了 Spring Boot 的 Web、Security、模板引擎(如 Thymeleaf)、数据访问(如 JPA)等功能。

Maven 示例:

XML 复制代码
<dependencies>
    <!-- Spring Boot Starter Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Spring Boot Starter Security -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    <!-- Spring Boot Starter Thymeleaf(如果使用 Thymeleaf 模板引擎) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>

    <!-- Spring Data JPA(假设使用 JPA) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>

    <!-- 数据库驱动(例如 H2 数据库) -->
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>

说明:

  • spring-boot-starter-web:提供构建 Web 应用所需的基础设施。
  • spring-boot-starter-security:引入 Spring Security 进行认证与授权。
  • spring-boot-starter-thymeleaf:如果使用 Thymeleaf 作为模板引擎,用于渲染登录页面。
  • spring-boot-starter-data-jpa:引入 Spring Data JPA,用于数据持久化。
  • 数据库驱动:根据实际使用的数据库选择相应的驱动,例如 H2、MySQL、PostgreSQL 等。

2. 配置 SecurityFilterChain

创建一个配置类 SecurityConfig,配置 Spring Security 的过滤器链和认证管理器。通过在 SecurityFilterChain 中使用 .formLogin() 方法,自动启用默认的 UsernamePasswordAuthenticationFilter,并配置 DaoAuthenticationProvider 所需的 UserDetailsServicePasswordEncoder

java 复制代码
package com.aqian.wenlike.config;

import com.aqian.wenlike.common.JwtAuthenticationFilter;
import com.aqian.wenlike.service.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;


@Configuration
public class SecurityConfig {

    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;

    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                // 使用 csrf -> disable() 的新方法
                .csrf(csrf -> csrf.disable())

                // 使用 formLogin -> disable() 的新方法
                .formLogin(form -> form.disable())

                // 使用新的授权配置方式
                .authorizeHttpRequests(authz -> authz
                        .requestMatchers("/auth/login", "/auth/register").permitAll() // 使用requestMatchers替代antMatchers
                        .anyRequest().authenticated()
                )

                // 会话管理配置保持不变
                .sessionManagement(session -> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                )

                // JWT过滤器配置保持不变
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }


}

关键配置说明:

禁用 CSRF

  • 说明:根据应用需求决定是否启用 CSRF 防护。在基于表单登录的传统 Web 应用中,通常启用 CSRF 防护;在无状态 API 应用中,可能会禁用。
  • 配置http.csrf().disable() 禁用 CSRF 防护。

授权规则

  • 说明

    • /auth/login/auth/register:允许所有用户(包括未认证用户)访问。
    • anyRequest().authenticated():其他所有请求均需要用户认证。
  • 配置

    java 复制代码
    .authorizeHttpRequests(authz -> authz
        .antMatchers("/auth/login", "/auth/register").permitAll()
        .anyRequest().authenticated()
    )

表单登录配置

  • 说明

    • loginPage("/auth/login"):指定自定义的登录页面路径。
    • defaultSuccessUrl("/dashboard", true) :用户成功登录后重定向到 /dashboard 页面。
    • permitAll():允许所有用户访问登录页面及其处理路径。
  • 配置

    java 复制代码
    .formLogin(form -> form
        .loginPage("/auth/login")
        .defaultSuccessUrl("/dashboard", true)
        .permitAll()
    )

登出配置

  • 说明

    • logoutUrl("/auth/logout"):指定自定义的登出请求路径。
    • logoutSuccessUrl("/auth/login?logout") :用户成功登出后重定向到登录页面,并附加 ?logout 参数以显示登出成功的消息。
    • permitAll():允许所有用户访问登出功能。
  • 配置

    java 复制代码
    .logout(logout -> logout
        .logoutUrl("/auth/logout")
        .logoutSuccessUrl("/auth/login?logout")
        .permitAll()
    )

会话管理

  • 说明

    • SessionCreationPolicy.IF_REQUIRED :仅在需要时创建 HTTP Session。这是默认策略,适用于有状态的认证场景。
  • 配置

    java 复制代码
    .sessionManagement(session -> session
        .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
    )

3. 实现 UserDetailsService

UserDetailsService 接口用于加载用户的详细信息。Spring Security 在认证过程中会调用该服务来获取用户信息。

准确来说是Spring Security默认配置的DaoAuthenticationProvider 内部默认通过UserDetailsService 的实现类(注册为bean)从数据库中加载相应的用户信息,所以只要使用了AuthenticationManagerauthenticate() 方法**,** 并传入了UsernamePasswordAuthenticationToken 类型的参数**,** 就会调用DaoAuthenticationProvider, 就需要配置UserDetailsService的实现类(注册为bean)

java 复制代码
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.stereotype.Service;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserRepository userRepository; // 假设有一个 UserRepository 接口

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 从数据库加载用户信息
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("User not found"));

        // 将应用的 User 转换为 Spring Security 的 UserDetails
        return org.springframework.security.core.userdetails.User.builder()
                .username(user.getUsername())
                .password(user.getPassword())
                .authorities(user.getRoles()) // 假设 User 有角色集合,格式如 "ROLE_USER"
                .build();
    }
}

说明:

  • UserRepository:用于从数据库中查找用户,确保用户存在。
  • GrantedAuthority :用户的权限集合,通常以 ROLE_ 前缀表示角色(例如,ROLE_USERROLE_ADMIN)。
  • 异常处理 :若用户不存在,抛出 UsernameNotFoundException

4.配置密码加密器PasswordEncoder

用户的密码被加密后才会存储到数据库中,所以DaoAuthenticationProvider 内部需要通过这个PasswordEncoder将用户输入的密码进行加密之后再与数据库中的密码进行对比来验证用户身份。

java 复制代码
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();  // 使用BCrypt进行密码加密
}

5. 实现用户实体类 User

创建 User 实体类,用于持久化用户信息。

java 复制代码
import javax.persistence.*;

@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true)
    private String username;

    private String password;

    private String email;

    private String roles; // 简化为逗号分隔的角色字符串,如 "ROLE_USER,ROLE_ADMIN"

    // Getters 和 Setters

    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }

    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }

    public String getPassword() { return password; }
    public void setPassword(String password) { this.password = password; }

    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }

    public String getRoles() { return roles; }
    public void setRoles(String roles) { this.roles = roles; }
}

说明:

  • 字段定义
    • username:唯一标识用户的用户名。
    • password:加密后的用户密码。
    • email:用户的电子邮件地址。
    • roles :用户的角色,简化为逗号分隔的字符串,例如 "ROLE_USER,ROLE_ADMIN"

6. 实现 UserRepository

创建 UserRepository 接口,继承自 JpaRepository,用于数据访问。

java 复制代码
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;

public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByUsername(String username);
}

说明:

  • findByUsername :根据用户名查找用户,返回 Optional<User> 以处理用户不存在的情况。

7. 实现用户服务 UserService

创建 UserService,负责用户注册逻辑。

java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository; // 假设有一个 UserRepository 接口

    @Autowired
    private PasswordEncoder passwordEncoder;

    public void registerNewUser(RegisterRequest registerRequest) {
        // 检查用户名是否已存在
        if (userRepository.findByUsername(registerRequest.getUsername()).isPresent()) {
            throw new RuntimeException("Username is already taken");
        }

        // 创建新用户实体
        User user = new User();
        user.setUsername(registerRequest.getUsername());
        user.setPassword(passwordEncoder.encode(registerRequest.getPassword()));
        user.setEmail(registerRequest.getEmail());
        user.setRoles("ROLE_USER"); // 假设默认角色为 ROLE_USER

        // 保存用户到数据库
        userRepository.save(user);
    }
}

说明:

  • 密码编码 :使用 PasswordEncoder(如 BCryptPasswordEncoder)对用户密码进行加密,增强安全性。
  • 角色设置 :默认为 ROLE_USER,可根据需求调整。
  • 异常处理:若用户名已存在,抛出运行时异常(可根据需求自定义异常类型)。

8. 实现控制器 AuthController

创建控制器,处理登录和注册请求。由于在情况一中使用的是基于表单的登录,登录逻辑由 Spring Security 的 UsernamePasswordAuthenticationFilter 自动处理,无需在 Controller 中显式处理登录请求。

java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/auth")
public class AuthController {

    @Autowired
    private UserService userService; // 处理用户相关操作

    @PostMapping("/register")
    public ResponseEntity<?> register(@RequestBody RegisterRequest registerRequest) {
        // 注册新用户
        userService.registerNewUser(registerRequest);
        return ResponseEntity.ok("User registered successfully");
    }

    // DTO 类示例
    public static class RegisterRequest {
        private String username;
        private String password;
        private String email;

        // Getters 和 Setters
        public String getUsername() { return username; }
        public void setUsername(String username) { this.username = username; }

        public String getPassword() { return password; }
        public void setPassword(String password) { this.password = password; }

        public String getEmail() { return email; }
        public void setEmail(String email) { this.email = email; }
    }
}

说明:

  • 注册端点/auth/register 接收用户注册请求,调用 UserService 进行用户注册。
  • 登录逻辑 :在基于表单登录的情况下,登录请求由 Spring Security 的 UsernamePasswordAuthenticationFilter 自动处理,无需在 Controller 中显式处理登录逻辑。

注意:若需要自定义登录响应(例如返回特定的响应格式),可以通过自定义过滤器或 Controller 来实现,但这需要额外的配置。

9. 实现登录页面 login.html

假设使用 Thymeleaf 模板引擎,创建登录页面 login.html。该页面包含一个表单,用户输入用户名和密码后提交至 /auth/login,由 UsernamePasswordAuthenticationFilter 处理。

login.html

html 复制代码
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Login</title>
</head>
<body>
    <h1>Login</h1>
    <form th:action="@{/auth/login}" method="post">
        <div>
            <label>Username:</label>
            <input type="text" name="username" required />
        </div>
        <div>
            <label>Password:</label>
            <input type="password" name="password" required />
        </div>
        <div>
            <button type="submit">Login</button>
        </div>
        <div th:if="${param.error}">
            Invalid username or password.
        </div>
        <div th:if="${param.logout}">
            You have been logged out.
        </div>
    </form>
</body>
</html>

说明:

  • 表单提交 :表单的 action 指向 /auth/login,由 UsernamePasswordAuthenticationFilter 处理。
  • 输入验证 :使用 required 属性确保用户输入了用户名和密码。
  • 错误与登出提示:根据请求参数显示错误信息或登出提示。
  • 示例:用户提交错误的用户名或密码后,页面会显示 "Invalid username or password."。

10. Spring Security 过滤器链解析

在情况一中,Spring Security 自动配置并添加了一系列过滤器,其中关键的过滤器包括:

SecurityContextPersistenceFilter

  • 作用 :在请求开始时,从 HTTP Session 中加载 SecurityContext,在请求结束时,将 SecurityContext 保存回 HTTP Session
  • 有状态认证:负责维护会话中的认证信息。

UsernamePasswordAuthenticationFilter

  • 作用 :拦截登录请求(默认 /login,通过 .loginPage("/auth/login") 改为 /auth/login),处理用户名和密码的认证。
  • 流程
    • 接收表单提交的用户名和密码。
    • 创建 UsernamePasswordAuthenticationToken(未认证状态)。
    • 调用 AuthenticationManager.authenticate() 进行认证。
    • 认证成功后,设置 Authentication 对象到 SecurityContextHolder,并将 SecurityContext 保存到 HTTP Session

关键点:

  • 自动配置:Spring Security 根据配置自动添加并管理这些过滤器。
  • 会话维护SecurityContextPersistenceFilter 负责在有状态认证中维护 SecurityContext

11. 认证流程详解

以下是基于上述配置和组件的完整认证流程:

(1)用户访问受保护资源

  • 用户尝试访问需要认证的资源,例如 /dashboard
  • Spring Security 检查当前请求是否已认证。
  • 未认证 :自动重定向用户到登录页面 /auth/login

(2)用户提交登录表单

  • 用户在 /auth/login 页面输入用户名和密码,并提交表单。
  • 请求POST /auth/login,包含 usernamepassword 参数。

(3)UsernamePasswordAuthenticationFilter 处理登录请求

  • 拦截请求UsernamePasswordAuthenticationFilter 拦截到 /auth/login 的 POST 请求。
  • 提取凭证 :从请求中提取 usernamepassword
  • 创建 Authentication 对象 :创建一个包含用户名和密码的 UsernamePasswordAuthenticationToken(未认证状态)。
  • 调用 AuthenticationManager.authenticate() :将 Authentication 对象传递给 AuthenticationManager 进行认证。

(4)AuthenticationManager 进行认证

  • 遍历 AuthenticationProviderAuthenticationManager 遍历已注册的 AuthenticationProvider
  • DaoAuthenticationProvider 支持 UsernamePasswordAuthenticationToken,因此由其处理认证请求。

(5)DaoAuthenticationProvider 验证用户信息

  • 加载用户信息 :调用 UserDetailsService.loadUserByUsername(username),加载用户的详细信息(包括加密后的密码和权限)。
  • 密码验证 :使用 PasswordEncoder.matches(rawPassword, encodedPassword) 验证用户提交的密码是否正确。
  • 创建已认证的 Authentication 对象 :如果密码验证成功,创建一个包含用户详细信息和权限的 UsernamePasswordAuthenticationToken(已认证状态)。

(6)认证成功后

  • 设置 AuthenticationSecurityContextHolder :将已认证的 Authentication 对象存入 SecurityContextHolder
  • 保存 SecurityContextHTTP SessionSecurityContextPersistenceFilterSecurityContext 保存到 HTTP Session 中。
  • 重定向用户 :用户被重定向到登录成功后的页面,例如 /dashboard

(7)后续请求的认证与授权

  • 请求开始:用户在会话期间访问其他受保护资源。
  • 加载 SecurityContextSecurityContextPersistenceFilterHTTP Session 中加载 SecurityContext,并绑定到当前线程的 SecurityContextHolder 中。
  • 访问控制 :根据 Authentication 对象的 GrantedAuthority 集合,Spring Security 决定是否允许访问特定资源。
  • 请求处理:如果授权通过,用户可以访问资源;否则,返回 403 Forbidden 或重定向到错误页面。

(8)请求结束

  • 保存 SecurityContextSecurityContextPersistenceFilterSecurityContextHolder 中的 SecurityContext 保存回 HTTP Session
  • 清理 SecurityContextHolder :清除当前线程的 SecurityContextHolder,避免数据泄露到其他请求。

12. 关键组件与其职责

为了更好地理解整个认证流程,以下是关键组件及其职责的详细说明:

SecurityFilterChain

  • 职责:定义 Spring Security 的过滤器链,配置安全策略和认证流程。
  • 配置 :通过 HttpSecurity 对象配置授权规则、登录和登出行为、会话管理等。

UsernamePasswordAuthenticationFilter

  • 职责 :处理基于表单的登录请求,提取用户名和密码,创建 Authentication 对象,并调用 AuthenticationManager 进行认证。
  • 配置 :通过 .formLogin() 方法自动添加到过滤器链中,可自定义登录页面路径和认证成功后的跳转路径。

AuthenticationManager

  • 职责 :认证的核心接口,负责协调多个 AuthenticationProvider 来执行认证逻辑。
  • 配置 :通过 AuthenticationConfiguration 自动配置,集成了所有已注册的 AuthenticationProvider
  • 使用 :在 UsernamePasswordAuthenticationFilter 中调用 authenticate() 方法,传递 Authentication 对象进行认证。

AuthenticationProvider

  • 职责 :具体实现认证逻辑的组件,负责验证 Authentication 对象,并返回认证后的 Authentication 对象。
  • 默认提供者DaoAuthenticationProvider,使用 UserDetailsServicePasswordEncoder 进行用户名和密码的验证。
  • 配置 :自动注册,只要定义了 UserDetailsServicePasswordEncoder

UserDetailsService

  • 职责:加载用户的详细信息(如用户名、密码、权限)。
  • 实现 :需要实现 UserDetailsService 接口,定义如何从数据源加载用户信息。
  • 使用 :由 DaoAuthenticationProvider 调用,获取用户信息以进行认证。

PasswordEncoder

  • 职责:负责密码的加密和验证。
  • 实现 :推荐使用 BCryptPasswordEncoder,提供强大的密码哈希功能。
  • 配置 :通过 Bean 定义,Spring Security 会自动注入到 DaoAuthenticationProvider 中。

SecurityContextPersistenceFilter

  • 职责 :在请求开始时加载 SecurityContext,在请求结束时保存 SecurityContext
  • 作用:确保每个请求都有其对应的认证信息,维护用户的会话状态。

SecurityContextHolder

  • 职责 :存储当前线程的 SecurityContext,通过 ThreadLocal 实现线程绑定。
  • 使用:在请求处理期间,任何组件都可以访问当前用户的认证信息。

13. 授权决策与 SecurityContextHolder 的关系

在情况一中,Spring Security 的授权机制(如 @PreAuthorize)依赖于 SecurityContextHolder 中的 Authentication 对象进行权限判断。

(1) 使用授权注解

示例:

java 复制代码
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class DashboardController {

    @PreAuthorize("hasRole('ADMIN')")
    @GetMapping("/admin/dashboard")
    public String adminDashboard() {
        return "admin_dashboard";
    }

    @PreAuthorize("isAuthenticated()")
    @GetMapping("/dashboard")
    public String userDashboard() {
        return "user_dashboard";
    }
}

说明:

  • @PreAuthorize 注解:在方法执行前进行授权检查,确保用户具备所需的权限。
  • 权限来源 :基于 SecurityContextHolder 中的 Authentication 对象的 GrantedAuthority 集合。
  • 评估表达式
    • hasRole('ADMIN') :检查用户是否具有 ROLE_ADMIN 权限。
    • isAuthenticated():检查用户是否已认证。
(2)授权决策流程
  1. 方法调用前 :Spring Security 解析 @PreAuthorize 注解中的表达式。

  2. 获取 Authentication 对象 :从 SecurityContextHolder 中获取当前线程绑定的 Authentication 对象。

  3. 评估权限 :根据 GrantedAuthority 集合,评估表达式是否为真。

  4. 访问控制

    • 通过:方法执行,返回相应视图或数据。
    • 拒绝:抛出访问拒绝异常,通常返回 403 Forbidden 状态码。

关键点:

  • SecurityContextHolder 的重要性 :授权决策依赖于 SecurityContextHolder 中的 Authentication 对象,确保在整个请求处理期间,任何地方都可以访问和评估用户的权限。
  • 一致性 :由于 SecurityContextHolder 通过 ThreadLocal 绑定到当前线程,确保了请求处理过程中的一致性和安全性。

方案二:通过自定义的 Controller 与 Service 处理用户登录并使用 JWT 进行无状态认证

方案二关键点

  • 只要使用Spring Security框架,就默认配置了AuthenticationManager与DaoAuthenticationProvider
  • 在Controller层中需要依赖注入AuthenticationManager的bean,通过调用AuthenticationManager的authenticate() 方法来验证用户的身份
  • AuthenticationManager的authenticate() 方法的参数得是个Authentication实现类的对象,所以这里你可以选择使用UsernamePasswordAuthenticationToken这个实现类的对象(因为默认配置了DaoAuthenticationProvider到列表里),也可以使用JwtAuthenticationToken实现类的对象(但这个实现类需要自定义)
  • UsernamePasswordAuthenticationToken这个实现类中只能通过username与password去创建一个Authentication,但你自定义的JwtAuthenticationToken可以接收其他的参数并将其包装进去生成一个Authentication,然后使用接受这个JwtAuthenticationToken类型的ManagerProvider(JwtAuthenticationProvider)来生成一个认证的Authentication。
  • AuthenticationManager的authenticate() 方法是用来验证用户身份的,所以一般只有在用户登陆的时候才会调用(在Controller层),后续的请求一般都是通过从前端发来的请求的请求头中获取JWT来验证用户的身份,所以这里的自定义的JwtAuthenticationProvider要接受的参数是JwtAuthenticationToken类型的对象(里面包装了JWT),但是用户在登陆的时候就是通过用户名与密码作为用户身份验证参考的,所以在这里还是推荐使用默认配置的DaoAuthenticationProvider。(直接通过authenticationManager.authenticate()就会自动使用,记得传参数传UsernamePasswordAuthenticationToken类型的对象)

DaoAuthenticationProvider 的自动配置条件:

  • 存在 UserDetailsService Bean :当你在Spring上下文中定义了一个实现了 UserDetailsService 接口的Bean时,Spring Security会自动配置一个 DaoAuthenticationProvider
  • 存在 PasswordEncoder Bean :为了确保密码的安全性,通常需要配置一个 PasswordEncoder Bean(如 BCryptPasswordEncoder),这也是 DaoAuthenticationProvider 所依赖的。

1. 项目依赖配置

除了情况一中的依赖外,情况二还需要引入 JWT 相关的依赖,例如 jjwt。这些依赖用于生成和验证 JWT。

Maven 示例:

XML 复制代码
<dependencies>
    <!-- 前述情况一的依赖 -->

    <!-- JWT 依赖 -->
    <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>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-jackson</artifactId> <!-- 或 jjwt-gson,根据需要选择 -->
        <version>0.11.5</version>
        <scope>runtime</scope>
    </dependency>
</dependencies>

说明:

  • jjwt-apijjwt-impljjwt-jackson :Java JWT 处理库,用于生成和验证 JWT。jjwt-jackson 用于支持 JSON 序列化和反序列化。
  • 版本选择:确保使用与项目兼容的版本。

2. 实现 UserDetailsService

UserDetailsService 接口用于加载用户的详细信息。Spring Security 在认证过程中会调用该服务来获取用户信息。

准确来说是Spring Security默认配置的DaoAuthenticationProvider 内部默认通过UserDetailsService 的实现类(注册为bean)从数据库中加载相应的用户信息,所以只要使用了AuthenticationManagerauthenticate() 方法**,** 并传入了UsernamePasswordAuthenticationToken 类型的参数**,** 就会调用DaoAuthenticationProvider, 就需要配置UserDetailsService的实现类(注册为bean)

java 复制代码
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.stereotype.Service;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserRepository userRepository; // 假设有一个 UserRepository 接口

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 从数据库加载用户信息
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("User not found"));

        // 将应用的 User 转换为 Spring Security 的 UserDetails
        return org.springframework.security.core.userdetails.User.builder()
                .username(user.getUsername())
                .password(user.getPassword())
                .authorities(user.getRoles()) // 假设 User 有角色集合,格式如 "ROLE_USER"
                .build();
    }
}

说明:

  • UserRepository:用于从数据库中查找用户,确保用户存在。
  • GrantedAuthority :用户的权限集合,通常以 ROLE_ 前缀表示角色(例如,ROLE_USERROLE_ADMIN)。
  • 异常处理 :若用户不存在,抛出 UsernameNotFoundException

3.配置密码加密器PasswordEncoder

用户的密码被加密后才会存储到数据库中,所以DaoAuthenticationProvider 内部需要通过这个PasswordEncoder将用户输入的密码进行加密之后再与数据库中的密码进行对比来验证用户身份。

java 复制代码
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();  // 使用BCrypt进行密码加密
}

4. 实现 JWT 服务 JwtService

创建一个服务类,负责生成和验证 JWT。该服务将负责签发 JWT、解析 JWT 并验证其有效性。

java 复制代码
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.security.Key;
import java.util.Date;

@Service
public class JwtService {

    private Key key;
    private final long jwtExpirationInMs = 86400000; // 1 天

    @PostConstruct
    public void init() {
        // 使用安全的密钥生成器
        this.key = Keys.secretKeyFor(SignatureAlgorithm.HS512);
    }

    // 生成 JWT
    public String generateToken(Authentication authentication) {
        UserDetails userPrincipal = (UserDetails) authentication.getPrincipal();

        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + jwtExpirationInMs);

        return Jwts.builder()
                .setSubject(userPrincipal.getUsername())
                .setIssuedAt(now)
                .setExpiration(expiryDate)
                .signWith(key)
                .compact();
    }

    // 从 JWT 获取用户名
    public String getUsernameFromJWT(String token) {
        Claims claims = Jwts.parserBuilder()
                            .setSigningKey(key)
                            .build()
                            .parseClaimsJws(token)
                            .getBody();

        return claims.getSubject();
    }

    // 验证 JWT
    public boolean validateToken(String authToken) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(authToken);
            return true;
        } catch (JwtException ex) {
            // 记录异常或处理不同类型的异常
            return false;
        }
    }
}

说明:

  • 密钥管理Keys.secretKeyFor(SignatureAlgorithm.HS512) 生成一个安全的密钥。实际应用中,应将密钥安全地存储,例如使用环境变量或配置管理工具。
  • 生成 JWT:包含用户名(主题)、发行时间、过期时间,并使用密钥签名。
  • 解析 JWT:从 JWT 中提取用户名,并验证其有效性和签名。

5. 实现自定义的 JWT 过滤器 JwtAuthenticationFilter

创建一个过滤器,负责拦截请求,提取并验证 JWT。如果 JWT 有效,则将认证信息设置到 SecurityContextHolder 中。

java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtService jwtService;

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        String jwt = getJwtFromRequest(request);

        if (jwt != null && jwtService.validateToken(jwt)) {
            String username = jwtService.getUsernameFromJWT(jwt);

            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            UsernamePasswordAuthenticationToken authentication =
                    new UsernamePasswordAuthenticationToken(
                            userDetails, null, userDetails.getAuthorities());

            // 将认证信息设置到 SecurityContext
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        filterChain.doFilter(request, response);
    }

    // 从请求头中提取 JWT
    private String getJwtFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        // 检查是否以 "Bearer " 开头
        if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7); // 去除 "Bearer " 前缀
        }
        return null;
    }
}

说明:

  • 继承自 OncePerRequestFilter:确保过滤器在每个请求中只执行一次。
  • 提取 JWT :从请求头中的 Authorization 字段提取 JWT。
  • 验证 JWT :使用 JwtService 验证 JWT 的有效性和签名。
  • 设置 Authentication 对象
    • 加载用户信息 :通过 UserDetailsService 加载用户的详细信息。
    • 创建 UsernamePasswordAuthenticationToken:包含用户详细信息和权限。
    • 设置到 SecurityContextHolder :将 Authentication 对象存入 SecurityContextHolder,使得后续的授权决策能够访问到认证信息。

6. 实现控制器 AuthController

创建控制器,处理用户登录请求,生成 JWT 并返回给客户端。不同于情况一,登录逻辑由自定义的 Controller 处理,而非 Spring Security 的过滤器自动处理。

java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/auth")
public class AuthController {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private JwtService jwtService;

    @Autowired
    private UserService userService; // 处理用户相关操作

    @PostMapping("/register")
    public ResponseEntity<?> register(@RequestBody RegisterRequest registerRequest) {
        // 注册新用户
        userService.registerNewUser(registerRequest);
        return ResponseEntity.ok("User registered successfully");
    }

    @PostMapping("/login")
    public ResponseEntity<?> authenticateUser(@RequestBody LoginRequest loginRequest) {
        // 创建 Authentication 对象
        Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                        loginRequest.getUsername(),
                        loginRequest.getPassword()
                )
        );

        // 生成 JWT
        String jwt = jwtService.generateToken(authentication);

        // 返回 JWT,放在响应头或以 JSON 格式返回
        return ResponseEntity.ok(new JwtAuthenticationResponse(jwt));
    }

    // DTO 类示例
    public static class LoginRequest {
        private String username;
        private String password;

        // Getters 和 Setters
        public String getUsername() { return username; }
        public void setUsername(String username) { this.username = username; }

        public String getPassword() { return password; }
        public void setPassword(String password) { this.password = password; }
    }

    public static class JwtAuthenticationResponse {
        private String token;
        private String tokenType = "Bearer";

        public JwtAuthenticationResponse(String token) {
            this.token = token;
        }

        // Getters 和 Setters
        public String getToken() { return token; }
        public void setToken(String token) { this.token = token; }

        public String getTokenType() { return tokenType; }
        public void setTokenType(String tokenType) { this.tokenType = tokenType; }
    }
}

说明:

  • 注册端点/auth/register 接收用户注册请求,调用 UserService 进行用户注册。
  • 登录端点/auth/login 接收用户登录请求,使用 AuthenticationManager 进行认证。
    • 认证过程
      • 创建 UsernamePasswordAuthenticationToken,包含用户名和密码。
      • 调用 authenticationManager.authenticate() 进行认证。
    • 认证成功
      • 调用 JwtService.generateToken(authentication) 生成 JWT。
      • 将 JWT 返回给客户端,可以通过响应体中的 JSON 对象,如 { "token": "jwt_token", "tokenType": "Bearer" },或者将 JWT 放在响应头中的 Authorization 字段。
    • 认证失败
      • Spring Security 自动处理,返回 401 Unauthorized 状态码。

7. 在SecurityConfig中配置 SecurityFilterChain 以支持 JWT 认证 并 注册AuthenticationManager以供Controller层使用

更新 SecurityConfig,配置 Spring Security 以支持 JWT 认证,禁用基于表单的登录和会话管理,并添加自定义的 JwtAuthenticationFilter

java 复制代码
import com.aqian.wenlike.common.JwtAuthenticationFilter;
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.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;


@Configuration
public class SecurityConfig {

    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;
    

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                // 使用 csrf -> disable() 的新方法
                .csrf(csrf -> csrf.disable())

                // 使用 formLogin -> disable() 的新方法
                .formLogin(form -> form.disable())

                // 使用新的授权配置方式
                .authorizeHttpRequests(authz -> authz
                        .requestMatchers("/auth/login", "/auth/register").permitAll() // 使用requestMatchers替代antMatchers
                        .anyRequest().authenticated()
                )

                // 会话管理配置保持不变
                .sessionManagement(session -> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                )

                // JWT过滤器配置保持不变
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
    // 添加AuthenticationManager配置
    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
        return http.getSharedObject(AuthenticationManagerBuilder.class)
                .build();
    }


}

在Spring Security 6.1及以后的版本中,配置AuthenticationManager的方式是通过 http.getSharedObject(AuthenticationManagerBuilder.class) 来创建。

关键配置说明:

禁用基于表单的登录

  • 说明:由于使用 JWT 进行无状态认证,不再依赖于基于表单的登录机制。
  • 配置.formLogin().disable()

禁用会话管理,使用无状态认证

  • 说明 :JWT 认证是无状态的,不依赖于服务器端的 HTTP Session
  • 配置.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)

添加自定义的 JWT 过滤器

  • 说明 :自定义的 JwtAuthenticationFilter 负责从请求头中提取 JWT,验证其有效性,并设置 Authentication 对象到 SecurityContextHolder
  • 配置.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)

配置密码编码器

  • 说明 :负责密码的加密与验证,推荐使用 BCryptPasswordEncoder,通过 Bean 定义进行配置。

  • 配置

    java 复制代码
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

8. 实现自定义的 JwtAuthenticationProvider

为了实现更复杂的认证逻辑,并避免依赖 DaoAuthenticationProvider 处理 UsernamePasswordAuthenticationToken,可以自定义 JwtAuthenticationProvider。该提供者专门处理 JWT 相关的认证逻辑。

(1)定义自定义的 AuthenticationJwtAuthenticationToken

创建一个新的 Authentication 实现,用于封装 JWT。

java 复制代码
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;

import java.util.Collection;

public class JwtAuthenticationToken extends AbstractAuthenticationToken {
    private final String token;
    private final Object principal;

    // 构造未认证的 token
    public JwtAuthenticationToken(String token) {
        super(null);
        this.token = token;
        this.principal = null;
        setAuthenticated(false);
    }

    // 构造已认证的 token
    public JwtAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.token = null;
        setAuthenticated(true);
    }

    @Override
    public Object getCredentials() {
        return token;
    }

    @Override
    public Object getPrincipal() {
        return principal;
    }
}

说明:

  • 未认证状态 :通过构造函数接收 JWT,未设置 principal,并标记为未认证。
  • 已认证状态 :通过构造函数接收 principalGrantedAuthority,标记为已认证。
(2) 实现自定义的 AuthenticationProvider JwtAuthenticationProvider

创建一个 AuthenticationProvider,专门处理 JwtAuthenticationToken

java 复制代码
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.stereotype.Component;

@Component
public class JwtAuthenticationProvider implements AuthenticationProvider {

    private final JwtService jwtService;
    private final UserDetailsService userDetailsService;

    public JwtAuthenticationProvider(JwtService jwtService, UserDetailsService userDetailsService) {
        this.jwtService = jwtService;
        this.userDetailsService = userDetailsService;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String token = (String) authentication.getCredentials();
        if (jwtService.validateToken(token)) {
            String username = jwtService.getUsernameFromJWT(token);
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            return new JwtAuthenticationToken(userDetails, userDetails.getAuthorities());
        }
        throw new BadCredentialsException("Invalid JWT Token");
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return JwtAuthenticationToken.class.isAssignableFrom(authentication);
    }
}

说明:

  • 验证 JWT :使用 JwtService 验证 JWT 的有效性和签名。
  • 加载用户信息 :通过 UserDetailsService 加载用户的详细信息。
  • 创建认证对象 :生成 JwtAuthenticationToken(已认证状态)并返回。
  • 异常处理 :若 JWT 无效,抛出 BadCredentialsException
(3)在自定义的JwtAuthenticationFilter 中调用 AuthenticationManager 进行认证
java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtService jwtService;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        String jwt = getJwtFromRequest(request);

        if (jwt != null && jwtService.validateToken(jwt)) {
            JwtAuthenticationToken authToken = new JwtAuthenticationToken(jwt);
            Authentication authentication = authenticationManager.authenticate(authToken);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        filterChain.doFilter(request, response);
    }

    private String getJwtFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

解释流程:

  • 提取 JWTJwtAuthenticationFilter 从请求头中提取 JWT。
  • 创建 JwtAuthenticationToken :将提取到的 JWT 封装到 JwtAuthenticationToken 对象中。
  • 调用 AuthenticationManager.authenticate() :将 JwtAuthenticationToken 传递给 AuthenticationManager 进行认证。
  • 自动选择 JwtAuthenticationProviderAuthenticationManager 会根据 JwtAuthenticationToken 的类型,自动选择支持该类型的 JwtAuthenticationProvider 进行处理。
  • 设置认证信息 :认证成功后,将 Authentication 对象设置到 SecurityContextHolder 中,完成认证。

9. 用户登录与 JWT 生成流程

步骤详解:

(1)用户注册

  • 用户通过发送 POST /auth/register 请求,携带 usernamepasswordemail
  • AuthController 调用 UserService 进行用户注册,密码经过 PasswordEncoder 加密后存储到数据库。
  • 成功注册后,返回成功消息。

(2)用户登录

  • 用户通过发送 POST /auth/login 请求,携带 usernamepassword
  • AuthController 接收登录请求,使用 AuthenticationManager 进行认证:
    • 创建 UsernamePasswordAuthenticationToken(包含用户名和密码)。
    • 调用 authenticationManager.authenticate() 方法,触发 DaoAuthenticationProvider 进行认证。
  • 认证成功
    • 调用 JwtService.generateToken(authentication) 生成 JWT。
    • 将 JWT 返回给客户端,例如通过 JSON 响应 { "token": "jwt_token", "tokenType": "Bearer" }
  • 认证失败
    • Spring Security 自动处理,返回 401 Unauthorized 状态码。

(3)后续请求的认证

  • 客户端在后续需要授权的请求中,携带 JWT,例如在请求头中添加 Authorization: Bearer <token>
  • 请求处理
    • JwtAuthenticationFilter 拦截请求,提取 JWT。
    • 调用 JwtService.validateToken(token) 验证 JWT 的有效性和签名。
    • 验证成功
      • 提取用户名,通过 UserDetailsService 加载用户信息。
      • 创建 UsernamePasswordAuthenticationToken(已认证状态),设置到 SecurityContextHolder
    • 验证失败
      • 拒绝访问,返回 401 Unauthorized 状态码。

(4)授权与访问控制

  • 根据 SecurityContextHolder 中的 Authentication 对象及其 GrantedAuthority 集合,Spring Security 进行访问控制,决定是否允许访问特定资源。

10. 安全性最佳实践

在情况二中,除了常规的安全配置外,还需要关注以下安全性最佳实践:

密钥管理

  • 说明:确保 JWT 的密钥安全存储,避免在代码中硬编码。可以使用环境变量、配置管理工具或专用的密钥管理服务(如 AWS KMS)来存储密钥。

Token 过期与刷新

  • 说明:设置合理的 JWT 过期时间(例如 1 天),并实现 Token 刷新机制,提升安全性与用户体验。
  • 实现:可以创建一个刷新 Token 的端点,允许用户在旧 Token 过期前获取新的 Token。

防止 Token 滥用

  • 说明:实现 Token 黑名单机制,防止被盗用的 Token 继续使用。可以使用缓存系统(如 Redis)存储已注销或过期的 Token。

HTTPS

  • 说明:确保所有请求通过 HTTPS 传输,保护传输中的敏感信息(如用户名、密码和 JWT)。

限制 Token 的使用范围

  • 说明:根据需求设置 JWT 的权限范围,避免 Token 被滥用访问不必要的资源。

日志与监控

  • 说明:记录认证事件(成功和失败的登录尝试),便于监控和审计。实时监控认证系统的运行状态,及时发现并响应异常行为。

11. 完整认证流程

用户注册:

  1. 请求 :用户通过发送 POST /auth/register 请求,携带 usernamepasswordemail
  2. 处理AuthController 调用 UserService 进行用户注册,密码经过 PasswordEncoder 加密后存储到数据库。
  3. 响应 :成功注册后,返回成功消息 User registered successfully

用户登录:

  1. 请求 :用户通过发送 POST /auth/login 请求,携带 usernamepassword
  2. 认证处理
    • AuthController 接收登录请求,创建 UsernamePasswordAuthenticationToken(包含用户名和密码)。
    • 调用 authenticationManager.authenticate() 方法,触发 DaoAuthenticationProvider 进行认证。
  3. 认证成功
    • DaoAuthenticationProvider 使用 UserDetailsService 加载用户信息,并通过 PasswordEncoder 验证密码。
    • JwtService.generateToken(authentication) 生成 JWT。
    • 将 JWT 返回给客户端,例如通过 JSON 响应 { "token": "jwt_token", "tokenType": "Bearer" }
  4. 认证失败
    • Spring Security 自动处理,返回 401 Unauthorized 状态码。

后续请求的认证与授权:

  1. 客户端携带 JWT

    • 客户端在后续需要授权的请求中,添加 Authorization: Bearer <token> 请求头,携带 JWT。
  2. 请求处理

    • JwtAuthenticationFilter 拦截请求,提取 JWT。
    • 调用 JwtService.validateToken(token) 验证 JWT 的有效性和签名。
    • 验证成功
      • 提取用户名,通过 UserDetailsService 加载用户信息。
      • 创建 UsernamePasswordAuthenticationToken(已认证状态),设置到 SecurityContextHolder
    • 验证失败
      • 拒绝访问,返回 401 Unauthorized 状态码。
  3. 授权与访问控制

    • 根据 SecurityContextHolder 中的 Authentication 对象及其 GrantedAuthority 集合,Spring Security 进行访问控制,决定是否允许访问特定资源。

三、情况一与情况二的差异总结

尽管情况一情况二都涉及用户登录与认证,但它们在实现细节和适用场景上存在显著差异。以下是两者的主要区别:

1.认证机制与会话管理

  • 情况一(基于表单登录的有状态认证)

    • 依赖于 HTTP Session :认证信息存储在服务器端的 HTTP Session 中。
    • 有状态认证:服务器维护用户的认证状态,适用于传统的多页面 Web 应用。
    • 自动启用 UsernamePasswordAuthenticationFilter :通过在 SecurityFilterChain 中使用 .formLogin() 自动启用默认的登录过滤器。
    • SecurityContextPersistenceFilter :自动从 HTTP Session 中加载和保存 SecurityContext,确保每次请求都维护用户的认证状态。
    • 认证对象管理 :每次请求都需要单独创建 Authentication 对象,并在请求结束时将 SecurityContext 存储回 HTTP Session
  • 情况二(基于 JWT 的无状态认证)

    • 使用 JWT:认证状态由客户端携带的 JWT 维护,服务器无需存储会话信息。
    • 无状态认证:适用于 RESTful API、分布式系统和微服务架构。
    • 自定义认证流程:通过自定义 Controller 和 Service 处理登录请求,生成并返回 JWT。
    • 自定义过滤器 JwtAuthenticationFilter :拦截请求,提取并验证 JWT,设置 Authentication 对象到 SecurityContextHolder。、
    • 无需 HTTP Session :因为设置了 SessionCreationPolicy.STATELESSSecurityContextPersistenceFilter 不再维护 SecurityContextHTTP Session 的同步。

2.认证处理方式

  • 情况一

    • 登录请求处理 :由 Spring Security 的 UsernamePasswordAuthenticationFilter 自动处理,用户通过表单提交凭证。
    • 认证成功后 :将 Authentication 对象存入 SecurityContextHolderHTTP Session,无需手动生成和管理 JWT。
  • 情况二

    • 登录请求处理:由自定义的 Controller 处理,用户通过 API 请求提交凭证。
    • 认证成功后:在 Controller 逻辑中生成 JWT,并将其返回给客户端,客户端在后续请求中使用 JWT 进行认证。

3.自定义认证逻辑

  • 情况一

    • 依赖于 Spring Security 的默认配置 :利用内置的 DaoAuthenticationProviderUserDetailsService 进行用户名和密码的验证。
    • 无需自定义认证提供者:除非需要特殊的认证逻辑。
  • 情况二

    • 可扩展性强 :如果需要更复杂的认证逻辑,不再依赖于 DaoAuthenticationProvider 处理 UsernamePasswordAuthenticationToken,可以自定义 JwtAuthenticationProvider
    • 专门处理 JWTJwtAuthenticationProvider 接收 JwtAuthenticationToken,验证 JWT 并设置认证信息。

4.依赖注入与配置

  • 情况一

    • 自动配置 :只需配置 UserDetailsServicePasswordEncoder,Spring Security 自动配置 DaoAuthenticationProvider 和相关过滤器。
    • 简化配置:利用 Spring Security 的自动化特性,减少手动配置的工作量。
  • 情况二

    • 需要自定义配置 :需要手动添加自定义的 JwtAuthenticationFilterJwtAuthenticationProviderSecurityFilterChainAuthenticationManager
    • 更高的灵活性:通过自定义过滤器和提供者,可以实现更复杂和定制化的认证逻辑。

5.响应与前端交互

  • 情况一

    • 基于表单提交 :用户通过浏览器表单提交登录凭证,认证信息通过 HTTP Session 维护。
    • 后端重定向 :认证成功后,后端将用户重定向到指定页面(例如 /dashboard)。
  • 情况二

    • 基于 API 请求:用户通过 API 请求提交登录凭证,后端返回 JWT。
    • 前端存储 JWT :前端需要将 JWT 存储在安全的地方(如 localStorageHttpOnly Cookie),并在后续请求中携带 JWT 进行认证。

6.授权决策依赖

  • 情况一

    • 依赖 SecurityContextHolder 中的 Authentication 对象 :通过 @PreAuthorize 等注解进行授权判断。
    • 有状态 :每次请求都通过 HTTP Session 维持认证信息,授权决策基于会话中的认证状态。
  • 情况二

    • 同样依赖 SecurityContextHolder 中的 Authentication 对象 :通过自定义的过滤器将认证信息设置到 SecurityContextHolder
    • 无状态:每次请求通过 JWT 重新验证和设置认证信息,授权决策基于当前请求携带的 JWT。

四、总结与建议

情况一情况二各有优缺点,选择哪种方式取决于应用的具体需求和架构设计。

  • 情况一(基于表单登录的有状态认证)

    • 适用场景:传统的多页面 Web 应用,用户在浏览器中通过表单提交凭证,认证状态通过服务器端会话维护。
    • 优势
      • 利用 Spring Security 的自动化配置,开发和配置较为简便。
      • 适用于需要服务器端维护用户会话状态的应用。
    • 劣势
      • 在分布式系统中扩展性有限,需要处理会话共享的问题。
      • 依赖于服务器端的会话管理,可能增加服务器负担。
  • 情况二(基于 JWT 的无状态认证)

    • 适用场景:RESTful API、单页面应用(SPA)、分布式系统和微服务架构,用户通过 API 请求提交凭证,认证状态通过 JWT 维护,无需服务器端会话。
    • 优势
      • 无状态认证,易于扩展和部署在分布式环境中。
      • 客户端可以独立地管理认证状态,无需依赖服务器端会话。
      • 适用于跨域请求和移动应用场景。
    • 劣势
      • 需要手动配置和管理 JWT 的生成、验证和安全性。
      • 复杂的安全性考虑,如密钥管理、Token 过期与刷新、防止 Token 滥用等。
相关推荐
無限進步D6 小时前
Java 运行原理
java·开发语言·入门
難釋懷6 小时前
安装Canal
java
是苏浙6 小时前
JDK17新增特性
java·开发语言
不光头强6 小时前
spring cloud知识总结
后端·spring·spring cloud
GetcharZp9 小时前
告别 Python 依赖!用 LangChainGo 打造高性能大模型应用,Go 程序员必看!
后端
阿里加多9 小时前
第 4 章:Go 线程模型——GMP 深度解析
java·开发语言·后端·golang
likerhood9 小时前
java中`==`和`.equals()`区别
java·开发语言·python
小小李程序员10 小时前
Langchain4j工具调用获取不到ThreadLocal
java·后端·ai