方案一:在 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 示例:
<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
所需的 UserDetailsService
和 PasswordEncoder
。
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 {@Autowiredprivate JwtAuthenticationFilter jwtAuthenticationFilter;@Autowiredprivate UserDetailsServiceImpl userDetailsService;@Beanpublic 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()
:其他所有请求均需要用户认证。
- 配置:
.authorizeHttpRequests(authz -> authz.antMatchers("/auth/login", "/auth/register").permitAll().anyRequest().authenticated() )
表单登录配置:
- 说明:
loginPage("/auth/login")
:指定自定义的登录页面路径。defaultSuccessUrl("/dashboard", true)
:用户成功登录后重定向到/dashboard
页面。permitAll()
:允许所有用户访问登录页面及其处理路径。
- 配置:
.formLogin(form -> form.loginPage("/auth/login").defaultSuccessUrl("/dashboard", true).permitAll() )
登出配置:
- 说明:
logoutUrl("/auth/logout")
:指定自定义的登出请求路径。logoutSuccessUrl("/auth/login?logout")
:用户成功登出后重定向到登录页面,并附加?logout
参数以显示登出成功的消息。permitAll()
:允许所有用户访问登出功能。
- 配置:
.logout(logout -> logout.logoutUrl("/auth/logout").logoutSuccessUrl("/auth/login?logout").permitAll() )
会话管理:
- 说明:
SessionCreationPolicy.IF_REQUIRED
:仅在需要时创建HTTP Session
。这是默认策略,适用于有状态的认证场景。
- 配置:
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) )
3. 实现 UserDetailsService
UserDetailsService
接口用于加载用户的详细信息。Spring Security 在认证过程中会调用该服务来获取用户信息。
准确来说是Spring Security默认配置的DaoAuthenticationProvider内部默认通过UserDetailsService的实现类(注册为bean)从数据库中加载相应的用户信息,所以只要使用了AuthenticationManager的authenticate() 方法,并传入了UsernamePasswordAuthenticationToken类型的参数,就会调用DaoAuthenticationProvider,就需要配置UserDetailsService的实现类(注册为bean)
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 {@Autowiredprivate UserRepository userRepository; // 假设有一个 UserRepository 接口@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 从数据库加载用户信息User user = userRepository.findByUsername(username).orElseThrow(() -> new UsernameNotFoundException("User not found"));// 将应用的 User 转换为 Spring Security 的 UserDetailsreturn org.springframework.security.core.userdetails.User.builder().username(user.getUsername()).password(user.getPassword()).authorities(user.getRoles()) // 假设 User 有角色集合,格式如 "ROLE_USER".build();}
}
说明:
UserRepository
:用于从数据库中查找用户,确保用户存在。GrantedAuthority
:用户的权限集合,通常以ROLE_
前缀表示角色(例如,ROLE_USER
、ROLE_ADMIN
)。- 异常处理:若用户不存在,抛出
UsernameNotFoundException
。
4.配置密码加密器PasswordEncoder
用户的密码被加密后才会存储到数据库中,所以DaoAuthenticationProvider内部需要通过这个PasswordEncoder将用户输入的密码进行加密之后再与数据库中的密码进行对比来验证用户身份。
@Bean
public PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder(); // 使用BCrypt进行密码加密
}
5. 实现用户实体类 User
创建 User
实体类,用于持久化用户信息。
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 和 Setterspublic 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
,用于数据访问。
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
,负责用户注册逻辑。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;@Service
public class UserService {@Autowiredprivate UserRepository userRepository; // 假设有一个 UserRepository 接口@Autowiredprivate 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 中显式处理登录请求。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;@RestController
@RequestMapping("/auth")
public class AuthController {@Autowiredprivate 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 和 Setterspublic 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
:
<!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
,包含username
和password
参数。
(3)UsernamePasswordAuthenticationFilter
处理登录请求:
- 拦截请求:
UsernamePasswordAuthenticationFilter
拦截到/auth/login
的 POST 请求。 - 提取凭证:从请求中提取
username
和password
。 - 创建
Authentication
对象:创建一个包含用户名和密码的UsernamePasswordAuthenticationToken
(未认证状态)。 - 调用
AuthenticationManager.authenticate()
:将Authentication
对象传递给AuthenticationManager
进行认证。
(4)AuthenticationManager
进行认证:
- 遍历
AuthenticationProvider
:AuthenticationManager
遍历已注册的AuthenticationProvider
。 DaoAuthenticationProvider
支持UsernamePasswordAuthenticationToken
,因此由其处理认证请求。
(5)DaoAuthenticationProvider
验证用户信息:
- 加载用户信息:调用
UserDetailsService.loadUserByUsername(username)
,加载用户的详细信息(包括加密后的密码和权限)。 - 密码验证:使用
PasswordEncoder.matches(rawPassword, encodedPassword)
验证用户提交的密码是否正确。 - 创建已认证的
Authentication
对象:如果密码验证成功,创建一个包含用户详细信息和权限的UsernamePasswordAuthenticationToken
(已认证状态)。
(6)认证成功后:
- 设置
Authentication
到SecurityContextHolder
:将已认证的Authentication
对象存入SecurityContextHolder
。 - 保存
SecurityContext
到HTTP Session
:SecurityContextPersistenceFilter
将SecurityContext
保存到HTTP Session
中。 - 重定向用户:用户被重定向到登录成功后的页面,例如
/dashboard
。
(7)后续请求的认证与授权:
- 请求开始:用户在会话期间访问其他受保护资源。
- 加载
SecurityContext
:SecurityContextPersistenceFilter
从HTTP Session
中加载SecurityContext
,并绑定到当前线程的SecurityContextHolder
中。 - 访问控制:根据
Authentication
对象的GrantedAuthority
集合,Spring Security 决定是否允许访问特定资源。 - 请求处理:如果授权通过,用户可以访问资源;否则,返回 403 Forbidden 或重定向到错误页面。
(8)请求结束:
- 保存
SecurityContext
:SecurityContextPersistenceFilter
将SecurityContextHolder
中的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
,使用UserDetailsService
和PasswordEncoder
进行用户名和密码的验证。 - 配置:自动注册,只要定义了
UserDetailsService
和PasswordEncoder
。
UserDetailsService
- 职责:加载用户的详细信息(如用户名、密码、权限)。
- 实现:需要实现
UserDetailsService
接口,定义如何从数据源加载用户信息。 - 使用:由
DaoAuthenticationProvider
调用,获取用户信息以进行认证。
PasswordEncoder
- 职责:负责密码的加密和验证。
- 实现:推荐使用
BCryptPasswordEncoder
,提供强大的密码哈希功能。 - 配置:通过 Bean 定义,Spring Security 会自动注入到
DaoAuthenticationProvider
中。
SecurityContextPersistenceFilter
- 职责:在请求开始时加载
SecurityContext
,在请求结束时保存SecurityContext
。 - 作用:确保每个请求都有其对应的认证信息,维护用户的会话状态。
SecurityContextHolder
- 职责:存储当前线程的
SecurityContext
,通过ThreadLocal
实现线程绑定。 - 使用:在请求处理期间,任何组件都可以访问当前用户的认证信息。
13. 授权决策与 SecurityContextHolder
的关系
在情况一中,Spring Security 的授权机制(如 @PreAuthorize
)依赖于 SecurityContextHolder
中的 Authentication
对象进行权限判断。
(1) 使用授权注解
示例:
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)授权决策流程
-
方法调用前:Spring Security 解析
@PreAuthorize
注解中的表达式。 -
获取
Authentication
对象:从SecurityContextHolder
中获取当前线程绑定的Authentication
对象。 -
评估权限:根据
GrantedAuthority
集合,评估表达式是否为真。 -
访问控制:
- 通过:方法执行,返回相应视图或数据。
- 拒绝:抛出访问拒绝异常,通常返回 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 示例:
<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-api
、jjwt-impl
、jjwt-jackson
:Java JWT 处理库,用于生成和验证 JWT。jjwt-jackson
用于支持 JSON 序列化和反序列化。- 版本选择:确保使用与项目兼容的版本。
2. 实现 UserDetailsService
UserDetailsService
接口用于加载用户的详细信息。Spring Security 在认证过程中会调用该服务来获取用户信息。
准确来说是Spring Security默认配置的DaoAuthenticationProvider内部默认通过UserDetailsService的实现类(注册为bean)从数据库中加载相应的用户信息,所以只要使用了AuthenticationManager的authenticate() 方法,并传入了UsernamePasswordAuthenticationToken类型的参数,就会调用DaoAuthenticationProvider,就需要配置UserDetailsService的实现类(注册为bean)
import com.aqian.wenlike.DAO.UserRepository;
import com.aqian.wenlike.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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;import java.util.Set;
import java.util.stream.Collectors;@Service
public class UserDetailsServiceImpl implements UserDetailsService {@Autowiredprivate UserRepository userRepository; // 假设有一个 UserRepository 接口@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 从数据库加载用户信息User user = userRepository.findByUsername(username).orElseThrow(() -> new UsernameNotFoundException("User not found"));// 将用户的角色转化为 GrantedAuthority 对象Set<GrantedAuthority> grantedAuthorities = user.getRoles().stream().map(role -> new SimpleGrantedAuthority(role.getName())).collect(Collectors.toSet());//直接调用Spring Security内部User类的构造函数return new org.springframework.security.core.userdetails.User(user.getUsername(),user.getPassword(),grantedAuthorities);}
}
说明:
UserRepository
:用于从数据库中查找用户,确保用户存在。GrantedAuthority
:用户的权限集合,通常以ROLE_
前缀表示角色(例如,ROLE_USER
、ROLE_ADMIN
)。- 异常处理:若用户不存在,抛出
UsernameNotFoundException
。
3.配置密码加密器PasswordEncoder(一般配置在SecurityConfig中)
用户的密码被加密后才会存储到数据库中,所以DaoAuthenticationProvider内部需要通过这个PasswordEncoder将用户输入的密码进行加密之后再与数据库中的密码进行对比来验证用户身份。
@Bean
public PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder(); // 使用BCrypt进行密码加密
}
4. 创建JWT工具类JwtUtil
创建一个服务类,负责生成和验证 JWT。该服务将负责签发 JWT、解析 JWT 并验证其有效性。
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.util.Date;
@Component
public class JwtUtil {private final Key key;public JwtUtil(@Value("${jwt.secretKeyString}") String secretKeyString) {// 将 Base64 编码的 secretKeyString 解码为字节数组byte[] keyBytes = Decoders.BASE64.decode(secretKeyString);// 使用 Keys 工具类基于字节数组生成 SecretKeythis.key = Keys.hmacShaKeyFor(keyBytes);}private final long validityInMilliseconds = 3600000; // 1 hour/*** 生成JWT令牌* @param userId 用户ID* @param username 用户名* @return 生成的JWT令牌*/public String generateToken(Long userId, String username) {return Jwts.builder().setSubject(userId.toString()) // 设置JWT的subject为userId.claim("username", username) // 设置额外的claim(如用户名).setIssuedAt(new Date()) // 设置签发时间.setExpiration(new Date(System.currentTimeMillis() + validityInMilliseconds)) // 设置过期时间.signWith(key, SignatureAlgorithm.HS256) // 显式指定算法 // 使用HS256算法进行签名.compact(); // 返回生成的JWT令牌}/*** 从JWT中提取用户名* @param token JWT令牌* @return 用户名*/public String extractUsername(String token) {return extractClaim(token, claims -> claims.get("username", String.class)); // 提取username}/*** 从JWT中提取用户ID* @param token JWT令牌* @return 用户ID*/public Long extractUserId(String token) {return Long.parseLong(extractClaim(token, Claims::getSubject)); // 提取userId(subject部分)}/*** 从JWT中提取特定的claim(声明)* @param token JWT令牌* @param claimsExtractor 提取函数* @param <T> 提取的结果类型* @return 提取的claim值*/public <T> T extractClaim(String token, ClaimsExtractor<T> claimsExtractor) {final Claims claims = extractAllClaims(token); // 获取JWT中的所有claimsreturn claimsExtractor.extract(claims); // 提取指定的claim}/*** 解析JWT中的所有claims* @param token JWT令牌* @return JWT中的所有claims*/private Claims extractAllClaims(String token) {return Jwts.parserBuilder() // 使用新式parserBuilder.setSigningKey(key) // 设置Key对象.build() // 构建解析器.parseClaimsJws(token) // 解析JWT.getBody(); // 获取Claims}/*** 验证JWT是否过期* @param token JWT令牌* @return 是否过期*/public boolean isTokenExpired(String token) {return extractExpiration(token).before(new Date()); // 判断过期时间}/*** 提取JWT的过期时间* @param token JWT令牌* @return 过期时间*/private Date extractExpiration(String token) {return extractClaim(token, Claims::getExpiration); // 获取过期时间}/*** 验证JWT的有效性* @param token JWT令牌* @param userId 用户ID* @return JWT是否有效*/public boolean validateToken(String token, Long userId) {return (userId.equals(extractUserId(token)) && !isTokenExpired(token)); // 验证userId是否匹配并检查是否过期}/*** ClaimsExtractor接口,用于提取JWT中的具体claim* @param <T> 提取的结果类型*/@FunctionalInterfacepublic interface ClaimsExtractor<T> {T extract(Claims claims); // 从claims中提取信息}
}
说明:
- 密钥管理:
Keys.secretKeyFor(SignatureAlgorithm.HS512)
生成一个安全的密钥。实际应用中,应将密钥安全地存储,例如使用环境变量或配置管理工具。 - 生成 JWT:包含用户名(主题)、发行时间、过期时间,并使用密钥签名。
- 解析 JWT:从 JWT 中提取用户名,并验证其有效性和签名。
5. 实现自定义的 JWT 过滤器 JwtAuthenticationFilter
创建一个过滤器,负责拦截请求,提取并验证 JWT。如果 JWT 有效,则将认证信息设置到 SecurityContextHolder
中。
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 {@Autowiredprivate JwtUtil jwtUtil;@Autowiredprivate UserDetailsService userDetailsService;@Overrideprotected void doFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain filterChain) throws ServletException, IOException {String jwt = getJwtFromRequest(request);if (jwt != null && jwtUtil.validateToken(jwt, jwtUtil.extractUserId(jwt))) {String username = jwtUtil.extractUsername(jwt);UserDetails userDetails = userDetailsService.loadUserByUsername(username);UsernamePasswordAuthenticationToken authentication =new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());// 将认证信息设置到 SecurityContextSecurityContextHolder.getContext().setAuthentication(authentication);}filterChain.doFilter(request, response);}// 从请求头中提取 JWTprivate 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 的过滤器自动处理。
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 {@Autowiredprivate AuthenticationManager authenticationManager;@Autowiredprivate JwtService jwtService;@Autowiredprivate 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()));// 生成 JWTString jwt = jwtService.generateToken(authentication);// 返回 JWT,放在响应头或以 JSON 格式返回return ResponseEntity.ok(new JwtAuthenticationResponse(jwt));}// DTO 类示例public static class LoginRequest {private String username;private String password;// Getters 和 Setterspublic 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 和 Setterspublic 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
。
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.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
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 com.aqian.wenlike.common.JwtAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;import java.util.List;@Configuration
public class SecurityConfig {@Autowiredprivate JwtAuthenticationFilter jwtAuthenticationFilter;@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.cors(cors -> cors.configurationSource(request -> {CorsConfiguration configuration = new CorsConfiguration();configuration.setAllowedOriginPatterns(List.of("*")); // 允许所有源configuration.setAllowedMethods(List.of("*")); // 允许所有方法configuration.setAllowedHeaders(List.of("*")); // 允许所有请求头configuration.setAllowCredentials(true); // 注意:使用 * 时必须设置为 falsereturn configuration;})).csrf(AbstractHttpConfigurer::disable).formLogin(form -> form.disable())// 使用新的授权配置方式.authorizeHttpRequests(authz -> authz.requestMatchers( "/auth/register").permitAll() // 使用requestMatchers替代antMatchers.requestMatchers( "/auth/login").permitAll() // 使用requestMatchers替代antMatchers.anyRequest().authenticated())// 会话管理配置保持不变.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))// JWT过滤器配置保持不变.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);return http.build();}// 添加AuthenticationManager配置@Beanpublic AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {return http.getSharedObject(AuthenticationManagerBuilder.class).build();}@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder(); // 使用BCrypt进行密码加密}}
- 配置
AuthenticationManager:
在Spring Security 6.1及以后的版本中,配置AuthenticationManager
的方式是通过http.getSharedObject(AuthenticationManagerBuilder.class)
来创建。- 配置密码加密器PasswordEncoder
关键配置说明:
配置CORS跨域访问规则
http.cors(cors -> cors.configurationSource(request -> {CorsConfiguration configuration = new CorsConfiguration();configuration.setAllowedOriginPatterns(List.of("*")); // 允许所有源configuration.setAllowedMethods(List.of("*")); // 允许所有方法configuration.setAllowedHeaders(List.of("*")); // 允许所有请求头configuration.setAllowCredentials(true); // 注意:使用 * 时必须设置为 falsereturn configuration;}))
SpringMVC的全局跨域配置与这个
SecurityFilterChain中的跨域配置二选一。
禁用基于表单的登录:
- 说明:由于使用 JWT 进行无状态认证,不再依赖于基于表单的登录机制。
- 配置:
.formLogin().disable()
禁用会话管理,使用无状态认证:
- 说明:JWT 认证是无状态的,不依赖于服务器端的
HTTP Session
。 - 配置:
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
添加自定义的 JWT 过滤器:
- 说明:自定义的
JwtAuthenticationFilter
负责从请求头中提取 JWT,验证其有效性,并设置Authentication
对象到SecurityContextHolder
。 - 配置:
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
添加AuthenticationManager配置(在AuthController中依赖注入使用)
@Beanpublic AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {return http.getSharedObject(AuthenticationManagerBuilder.class).build();}
配置密码编码器:
- 说明:负责密码的加密与验证,推荐使用
BCryptPasswordEncoder
,通过 Bean 定义进行配置。 - 配置:
@Bean public PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder(); }
8. 实现自定义的 JwtAuthenticationProvider
为了实现更复杂的认证逻辑,并避免依赖 DaoAuthenticationProvider
处理 UsernamePasswordAuthenticationToken
,可以自定义 JwtAuthenticationProvider
。该提供者专门处理 JWT 相关的认证逻辑。
(1)定义自定义的 Authentication
类 JwtAuthenticationToken
创建一个新的 Authentication
实现,用于封装 JWT。
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;// 构造未认证的 tokenpublic JwtAuthenticationToken(String token) {super(null);this.token = token;this.principal = null;setAuthenticated(false);}// 构造已认证的 tokenpublic JwtAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {super(authorities);this.principal = principal;this.token = null;setAuthenticated(true);}@Overridepublic Object getCredentials() {return token;}@Overridepublic Object getPrincipal() {return principal;}
}
说明:
- 未认证状态:通过构造函数接收 JWT,未设置
principal
,并标记为未认证。 - 已认证状态:通过构造函数接收
principal
和GrantedAuthority
,标记为已认证。
(2) 实现自定义的 AuthenticationProvider
JwtAuthenticationProvider
创建一个 AuthenticationProvider
,专门处理 JwtAuthenticationToken
。
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;}@Overridepublic 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");}@Overridepublic boolean supports(Class<?> authentication) {return JwtAuthenticationToken.class.isAssignableFrom(authentication);}
}
说明:
- 验证 JWT:使用
JwtService
验证 JWT 的有效性和签名。 - 加载用户信息:通过
UserDetailsService
加载用户的详细信息。 - 创建认证对象:生成
JwtAuthenticationToken
(已认证状态)并返回。 - 异常处理:若 JWT 无效,抛出
BadCredentialsException
。
(3)在自定义的JwtAuthenticationFilter
中调用 AuthenticationManager
进行认证:
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 {@Autowiredprivate JwtService jwtService;@Autowiredprivate AuthenticationManager authenticationManager;@Overrideprotected 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;}
}
解释流程:
- 提取 JWT:
JwtAuthenticationFilter
从请求头中提取 JWT。 - 创建
JwtAuthenticationToken
:将提取到的 JWT 封装到JwtAuthenticationToken
对象中。 - 调用
AuthenticationManager.authenticate()
:将JwtAuthenticationToken
传递给AuthenticationManager
进行认证。 - 自动选择
JwtAuthenticationProvider
:AuthenticationManager
会根据JwtAuthenticationToken
的类型,自动选择支持该类型的JwtAuthenticationProvider
进行处理。 - 设置认证信息:认证成功后,将
Authentication
对象设置到SecurityContextHolder
中,完成认证。
9. 用户登录与 JWT 生成流程
步骤详解:
(1)用户注册:
- 用户通过发送
POST /auth/register
请求,携带username
、password
和email
。 AuthController
调用UserService
进行用户注册,密码经过PasswordEncoder
加密后存储到数据库。- 成功注册后,返回成功消息。
(2)用户登录:
- 用户通过发送
POST /auth/login
请求,携带username
和password
。 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. 完整认证流程
用户注册:
- 请求:用户通过发送
POST /auth/register
请求,携带username
、password
和email
。 - 处理:
AuthController
调用UserService
进行用户注册,密码经过PasswordEncoder
加密后存储到数据库。 - 响应:成功注册后,返回成功消息
User registered successfully
。
用户登录:
- 请求:用户通过发送
POST /auth/login
请求,携带username
和password
。 - 认证处理:
AuthController
接收登录请求,创建UsernamePasswordAuthenticationToken
(包含用户名和密码)。- 调用
authenticationManager.authenticate()
方法,触发DaoAuthenticationProvider
进行认证。
- 认证成功:
DaoAuthenticationProvider
使用UserDetailsService
加载用户信息,并通过PasswordEncoder
验证密码。JwtService.generateToken(authentication)
生成 JWT。- 将 JWT 返回给客户端,例如通过 JSON 响应
{ "token": "jwt_token", "tokenType": "Bearer" }
。
- 认证失败:
- Spring Security 自动处理,返回 401 Unauthorized 状态码。
后续请求的认证与授权:
-
客户端携带 JWT:
- 客户端在后续需要授权的请求中,添加
Authorization: Bearer <token>
请求头,携带 JWT。
- 客户端在后续需要授权的请求中,添加
-
请求处理:
JwtAuthenticationFilter
拦截请求,提取 JWT。- 调用
JwtService.validateToken(token)
验证 JWT 的有效性和签名。 - 验证成功:
- 提取用户名,通过
UserDetailsService
加载用户信息。 - 创建
UsernamePasswordAuthenticationToken
(已认证状态),设置到SecurityContextHolder
。
- 提取用户名,通过
- 验证失败:
- 拒绝访问,返回 401 Unauthorized 状态码。
-
授权与访问控制:
- 根据
SecurityContextHolder
中的Authentication
对象及其GrantedAuthority
集合,Spring Security 进行访问控制,决定是否允许访问特定资源。
- 根据
三、情况一与情况二的差异总结
尽管情况一和情况二都涉及用户登录与认证,但它们在实现细节和适用场景上存在显著差异。以下是两者的主要区别:
1.认证机制与会话管理
-
情况一(基于表单登录的有状态认证):
- 依赖于 HTTP Session:认证信息存储在服务器端的
HTTP Session
中。 - 有状态认证:服务器维护用户的认证状态,适用于传统的多页面 Web 应用。
- 自动启用
UsernamePasswordAuthenticationFilter
:通过在SecurityFilterChain
中使用.formLogin()
自动启用默认的登录过滤器。 SecurityContextPersistenceFilter
:自动从HTTP Session
中加载和保存SecurityContext
,确保每次请求都维护用户的认证状态。- 认证对象管理:每次请求都需要单独创建
Authentication
对象,并在请求结束时将SecurityContext
存储回HTTP Session
。
- 依赖于 HTTP Session:认证信息存储在服务器端的
-
情况二(基于 JWT 的无状态认证):
- 使用 JWT:认证状态由客户端携带的 JWT 维护,服务器无需存储会话信息。
- 无状态认证:适用于 RESTful API、分布式系统和微服务架构。
- 自定义认证流程:通过自定义 Controller 和 Service 处理登录请求,生成并返回 JWT。
- 自定义过滤器
JwtAuthenticationFilter
:拦截请求,提取并验证 JWT,设置Authentication
对象到SecurityContextHolder
。、 - 无需 HTTP Session:因为设置了
SessionCreationPolicy.STATELESS
,SecurityContextPersistenceFilter
不再维护SecurityContext
与HTTP Session
的同步。
2.认证处理方式:
-
情况一:
- 登录请求处理:由 Spring Security 的
UsernamePasswordAuthenticationFilter
自动处理,用户通过表单提交凭证。 - 认证成功后:将
Authentication
对象存入SecurityContextHolder
和HTTP Session
,无需手动生成和管理 JWT。
- 登录请求处理:由 Spring Security 的
-
情况二:
- 登录请求处理:由自定义的 Controller 处理,用户通过 API 请求提交凭证。
- 认证成功后:在 Controller 逻辑中生成 JWT,并将其返回给客户端,客户端在后续请求中使用 JWT 进行认证。
3.自定义认证逻辑:
-
情况一:
- 依赖于 Spring Security 的默认配置:利用内置的
DaoAuthenticationProvider
和UserDetailsService
进行用户名和密码的验证。 - 无需自定义认证提供者:除非需要特殊的认证逻辑。
- 依赖于 Spring Security 的默认配置:利用内置的
-
情况二:
- 可扩展性强:如果需要更复杂的认证逻辑,不再依赖于
DaoAuthenticationProvider
处理UsernamePasswordAuthenticationToken
,可以自定义JwtAuthenticationProvider
。 - 专门处理 JWT:
JwtAuthenticationProvider
接收JwtAuthenticationToken
,验证 JWT 并设置认证信息。
- 可扩展性强:如果需要更复杂的认证逻辑,不再依赖于
4.依赖注入与配置:
-
情况一:
- 自动配置:只需配置
UserDetailsService
和PasswordEncoder
,Spring Security 自动配置DaoAuthenticationProvider
和相关过滤器。 - 简化配置:利用 Spring Security 的自动化特性,减少手动配置的工作量。
- 自动配置:只需配置
-
情况二:
- 需要自定义配置:需要手动添加自定义的
JwtAuthenticationFilter
和JwtAuthenticationProvider
到SecurityFilterChain
和AuthenticationManager
。 - 更高的灵活性:通过自定义过滤器和提供者,可以实现更复杂和定制化的认证逻辑。
- 需要自定义配置:需要手动添加自定义的
5.响应与前端交互:
-
情况一:
- 基于表单提交:用户通过浏览器表单提交登录凭证,认证信息通过
HTTP Session
维护。 - 后端重定向:认证成功后,后端将用户重定向到指定页面(例如
/dashboard
)。
- 基于表单提交:用户通过浏览器表单提交登录凭证,认证信息通过
-
情况二:
- 基于 API 请求:用户通过 API 请求提交登录凭证,后端返回 JWT。
- 前端存储 JWT:前端需要将 JWT 存储在安全的地方(如
localStorage
或HttpOnly
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 滥用等。