SpringSecurity入门
相关基础概念
Filter 回顾
在传统的 Servlet 程序中,从客户端发往 Servlet 层的请求会经过多层过滤器(Filter)。这些过滤器在请求到达最终处理逻辑前对其进行预处理,主要作用包括规范请求的编码格式、处理请求中的细节内容、为请求添加必要的附加信息,以及在安全验证方面对请求进行筛查。通过过滤器,可以拦截错误或未经授权的请求,从而有效保护程序的安全性,同时为后续的请求处理做好准备。这种机制不仅提升了程序的安全性和灵活性,也实现了逻辑的分层和关注点分离,使代码更易于维护和扩展。
在请求处理中,当有多个过滤器需要按顺序执行时,我们将这些过滤器的集合抽象为一个对象,称为 FilterChain
(筛选链)。FilterChain
的本质是由多个有序的过滤器(Filter
)组成的链式结构,它按照预定义的顺序依次调用每个过滤器,并最终将请求传递给目标 Servlet
。
当客户端向应用程序发送请求时,容器会基于请求 URI 的路径创建一个 FilterChain
,其中包含需要处理该请求的 Filter
实例和目标 Servlet
。在 Spring MVC 应用程序中,目标 Servlet
通常是 DispatcherServlet
。对于每个 HttpServletRequest
和 HttpServletResponse
,最多只能有一个 Servlet
进行处理,但可以有多个 Filter
参与其中。
Filter
的主要功能包括:
-
拦截请求并阻止后续的处理:通过控制逻辑,
Filter
可以终止请求链,避免下游的其他Filter
或Servlet
被调用。这种情况下,Filter
通常会直接写入HttpServletResponse
作为响应。 -
修改请求或响应内容:
Filter
可以对HttpServletRequest
或HttpServletResponse
进行修改,为后续的Filter
或目标Servlet
提供自定义处理。
Filter
的强大之处在于它与 FilterChain
的结合。FilterChain
负责维护过滤器的执行顺序并传递请求到下一个 Filter
或目标 Servlet
,从而形成一套灵活的请求处理机制。
FilterChain示例:
@Override
protected void doFilter(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain
) throws ServletException, IOException {
// do something before the rest of the application
chain.doFilter(request, response); // invoke the rest of the application
// do something after the rest of the application
}
Java Servlet 过滤器(Filter
)中,调用 chain.doFilter(request, response)
的作用是将当前请求和响应对象传递给过滤链中的下一个过滤器或最终的目标 Servlet
。
这样能优化编码,我们只需要考虑FilterChain中Filter的摆放顺序而不用考虑,各层之间的Filter然后调用, chain.doFilter(request, response) 会直接将当前Http对象逐层传递
DelegatingFilterProxy
DelegatingFilterProxy 是 Spring 提供的一个特殊的 Filter
,用于在 Servlet 容器的过滤器生命周期 和 Spring 管理的 Bean 生命周期 之间架起桥梁。它的核心思想是将 Servlet 容器中的 Filter
请求代理给 Spring 容器中的一个实际 Filter
Bean。
DelegatingFilterProxy
从 ApplicationContext 中查找名为 Filter_0 的 Bean,然后调用该 Bean Filter_0
它主要是通过代理模式将 Spring 管理的
Filter
集成到 Servlet 容器的过滤链中,并在初始化时使用同步块(lock)确保线程安全
FilterChainProxy
Spring Security 的 Servlet 支持通过一个特殊的 Filter —— FilterChainProxy 实现,该 Filter 委托给多个 SecurityFilterChain
实例,并作为一个 Spring Bean 通常被 DelegatingFilterProxy 包装。
SecurityFilterChain 安全过滤器链
SecurityFilterChain
被 FilterChainProxy 用于确定哪个 Spring Security Filter
应为当前请求调用实例。
Security Filters 中的安全过滤器通常是 Bean,但它们是注册到 FilterChainProxy 中,而不是 DelegatingFilterProxy。
在 Multiple SecurityFilterChain 图中,FilterChainProxy
负责决定使用哪个 SecurityFilterChain
,并且只调用第一个匹配的 SecurityFilterChain
。例如,当请求的 URL 是 /api/messages/
时,它首先匹配 SecurityFilterChain 0
的模式 /api/**
,因此仅调用 SecurityFilterChain 0
,即使该请求也匹配 SecurityFilterChain n
。如果请求的 URL 是 /messages/
,由于不匹配 /api/**
的 SecurityFilterChain 0
,FilterChainProxy
会继续检查其他 SecurityFilterChain
。如果没有其他实例匹配,则最终调用 SecurityFilterChain n
。
Security Filters 安全过滤器
安全过滤器通过 SecurityFilterChain API 插入到 FilterChainProxy 中,这些过滤器可以用于多种目的,例如身份验证、授权、漏洞防护等。这些过滤器按特定顺序执行,以确保它们在正确的时间被调用。例如,用于执行身份验证的过滤器应在执行授权的过滤器之前被调用。
通常不需要了解 Spring Security 的过滤器执行顺序,但在某些情况下,了解其顺序是有益的。如果您想了解具体的执行顺序,可以查看 FilterOrderRegistration
代码
为了举例说明上述段落,让我们考虑以下安全配置:
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfiguration {
private final JwTAuthenticationFilter jwtAuthFilter;
private final AuthenticationProvider authenticationProvider;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http
) throws Exception {
http
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.csrf(csrf -> csrf.disable())
.securityMatcher("/api/**", "/app/**")
.authorizeHttpRequests((authorize) -> authorize
.dispatcherTypeMatchers(DispatcherType.FORWARD, DispatcherType.ERROR).permitAll()
.requestMatchers("/api/v1/auth/**").permitAll()
.anyRequest().authenticated()
)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
}
Security过程
提供的测试代码的验证链
- DelegatingFilterProxy中的Filter会按照下面的顺序一次执行
DisableEncodeUrlFilter -> WebAsyncManagerIntegrationFilter -> SecurityContextHolderFilter ->
HeaderWriterFilter -> LogoutFilter -> JwTAuthenticationFilter -> RequestCacheAwareFilter ->
SecurityContextHolderAwareRequestFilter ->AnonymousAuthenticationFilter ->
SessionManagementFilter -> ExceptionTranslationFilter -> AuthorizationFilter
图像化执行流程
解释:
HTTP 请求进入Tomcat -> HTTP对象进入 JwTAuthenticationFilter -> 通过JwTService工具类处理Jwt -> 我们通过 UserDetailsService查询数据库中是否存在 -> 处理玩后将您想要交给 Security 代理的信息放入 SecurityContextHolder 中 -> 进入 Servlet 相关的 Filter -> 达到 Controller 处理业务
构建一个完整的Security
- 项目所包含的依赖
- JPA
- Spring Security
- MySQL Driver
- Lombok
- JWT
- opensaml
初始化项目
初始化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.3.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.springread</groupId>
<artifactId>security</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>security</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- Spring Security Core -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Spring Security SAML 2 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-saml2-service-provider</artifactId>
</dependency>
<!-- Web Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- JPA Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- MySQL Driver -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.2</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.2</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.2</version>
</dependency>
<!-- Test Dependencies -->
<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>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-saml2-service-provider</artifactId>
<version>6.1.0</version> <!-- 确保版本与 Spring Security 版本匹配 -->
</dependency>
<dependency>
<groupId>org.opensaml</groupId>
<artifactId>opensaml-core</artifactId>
<version>4.1.1</version>
</dependency>
<dependency>
<groupId>org.opensaml</groupId>
<artifactId>opensaml-saml-api</artifactId>
<version>4.1.1</version>
</dependency>
<dependency>
<groupId>org.opensaml</groupId>
<artifactId>opensaml-saml-impl</artifactId>
<version>4.1.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>central</id>
<url>https://repo.maven.apache.org/maven2</url>
</repository>
<repository>
<id>shibboleth-repo</id>
<url>https://build.shibboleth.net/maven/releases/</url>
</repository>
</repositories>
</project>
初始化application.yml
spring:
security:
filter:
dispatcher-types: ASYNC, ERROR, FORWARD, INCLUDE, REQUEST
application:
name: security
datasource:
url: jdbc:mysql://127.0.0.1:3306/jwt_security
username: root
password: Alone117
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: create-drop
show-sql: true
properties:
hibernate:
format_sql: true
database: mysql
# springboot3会自动推断mysqldatabase-platform的类型
# database-platform: org.hibernate.dialect.MySQLDialect
logging:
level:
org.springframework.security: DEBUG
构建Security所认证的安全对象
Role
public enum Role {
USER, ADMIN
}
User
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
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.List;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "_user")
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String firstname;
private String lastname;
private String email;
private String password;
@Enumerated(EnumType.STRING)
private Role role;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority(role.name()));
}
@Override
public String getUsername() {
return email;
}
@Override
public String getPassword() {
return password;
}
@Override
public boolean isAccountNonExpired() {
// 默认为true
return UserDetails.super.isAccountNonExpired();
}
@Override
public boolean isAccountNonLocked() {
// 默认为true
return UserDetails.super.isAccountNonLocked();
}
@Override
public boolean isCredentialsNonExpired() {
// 默认为true
return UserDetails.super.isCredentialsNonExpired();
}
@Override
public boolean isEnabled() {
// 默认为true
return UserDetails.super.isEnabled();
}
}
准备Jwt相关类
JwTService - Jwt工具类
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import io.jsonwebtoken.*;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Service
public class JwTService {
// 密钥,用于签名和验证JWT
private static final String SECRET_KEY = "350b0614bab4c5af7db53202295827463a5a643e9ae95c8bbdcf7f2a4bb107dc";
/**
* 从JWT令牌中提取用户名
* @param token JWT令牌
* @return 提取的用户名
*/
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
/**
* 生成JWT令牌(无额外声明)
* @param userDetails 用户详细信息
* @return 生成的JWT令牌
*/
public String generateToken(UserDetails userDetails) {
return generateToken(userDetails, new HashMap<>());
}
/**
* 生成JWT令牌(带额外声明)
* @param userDetails 用户详细信息
* @param extraClaims 额外的声明信息
* @return 生成的JWT令牌
*/
public String generateToken(
UserDetails userDetails,
Map<String, Object> extraClaims
) {
return Jwts
.builder()
.setClaims(extraClaims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 2)) // 令牌有效期2分钟
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
/**
* 验证JWT令牌是否有效
* @param token JWT令牌
* @param userDetails 用户详细信息
* @return 如果令牌有效则返回true,否则返回false
*/
public boolean isTokenValid(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return username.equals(userDetails.getUsername());
}
/**
* 检查JWT令牌是否已过期
* @param token JWT令牌
* @return 如果令牌已过期则返回true,否则返回false
*/
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 claimsResolver 用于解析声明的函数
* @param <T> 声明值的类型
* @return 提取的声明值
*/
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
/**
* 提取JWT令牌中的所有声明
* @param token JWT令牌
* @return 提取的所有声明
*/
private Claims extractAllClaims(String token) {
return Jwts
.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
}
/**
* 获取用于签名和验证的密钥
* @return 密钥对象
*/
private Key getSigningKey() {
byte[] keyBytes = Decoders.BASE64.decode(SECRET_KEY);
return Keys.hmacShaKeyFor(keyBytes);
}
}
JwTAuthenticationFilter
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
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.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
/**
* 自定义 JWT 认证过滤器,扩展 Spring Security 的 OncePerRequestFilter,
* 确保每次请求仅执行一次过滤逻辑。
*/
@Component
@RequiredArgsConstructor
public class JwTAuthenticationFilter extends OncePerRequestFilter {
// JWT 服务类,用于验证和解析 JWT
private final JwTService jwTService;
// UserDetailsService,用于加载用户信息
private final UserDetailsService userDetailsService;
/**
* 过滤器的核心方法,执行认证逻辑。
*
* @param request HTTP 请求
* @param response HTTP 响应
* @param filterChain 过滤器链
*/
@Override
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain
) throws ServletException, IOException {
// 从请求头中获取 Authorization 字段的值
final String authHeader = request.getHeader("Authorization");
final String jwt; // 保存提取的 JWT
final String userEmail; // 保存从 JWT 中提取的用户邮箱
// 检查 Authorization 头是否缺失或格式不正确
// 合法的 Authorization 头格式为:Bearer <token>
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
// 如果请求头不包含 JWT,则直接跳过当前过滤器,继续执行过滤器链
filterChain.doFilter(request, response);
return; // 提前返回,避免后续逻辑执行
}
// 从 Authorization 头中提取 JWT,去掉 "Bearer " 前缀
jwt = authHeader.substring(7);
// 从 JWT 中提取用户名(通常是邮箱)
userEmail = jwTService.extractUsername(jwt);
// 如果用户未认证且 JWT 中包含有效的用户名,则执行认证逻辑
if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
// 根据用户名加载用户详细信息
UserDetails userDetails = userDetailsService.loadUserByUsername(userEmail);
// 验证 JWT 是否有效
if (jwTService.isTokenValid(jwt, userDetails)) {
// 如果有效,则创建 UsernamePasswordAuthenticationToken 对象
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, // 认证的用户详情
null, // 凭证(此处为 null,因为 JWT 本身已被验证)
userDetails.getAuthorities() // 用户权限
);
// 设置额外的认证详情(如 IP 地址、会话 ID 等)
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 将认证对象设置到安全上下文中,表示当前用户已认证
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
// 继续执行过滤器链的下一个过滤器
filterChain.doFilter(request, response);
}
}
ApplicationConfig
import com.springread.security.user.UserRepository;
import lombok.RequiredArgsConstructor;
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.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* ApplicationConfig 类用于配置 Spring Security 的认证组件,
* 包括 UserDetailsService、AuthenticationProvider、PasswordEncoder 等。
*/
@Configuration // 表示这是一个配置类,Spring 容器会在启动时加载其中的 Bean。
@RequiredArgsConstructor // 自动生成构造函数,为 final 字段注入依赖。
public class ApplicationConfig {
private final UserRepository userRepository; // 用户数据访问层,用于查询用户信息。
/**
* 配置 UserDetailsService,用于根据用户名加载用户的详细信息。
*
* @return UserDetailsService 实例
*/
@Bean
public UserDetailsService userDetailsService() {
return username -> userRepository.findByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found")); // 如果找不到用户,抛出异常。
}
/**
* 配置 AuthenticationProvider,指定使用 DaoAuthenticationProvider。
* DaoAuthenticationProvider 是 Spring Security 的默认实现,负责从 UserDetailsService 获取用户信息。
*
* @return 配置好的 AuthenticationProvider
*/
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setUserDetailsService(userDetailsService()); // 指定用户信息服务。
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder()); // 指定密码编码器。
return daoAuthenticationProvider;
}
/**
* 配置 AuthenticationManager,用于处理认证请求。
*
* @param conf AuthenticationConfiguration 对象,由 Spring Security 自动注入。
* @return 配置好的 AuthenticationManager
* @throws Exception 如果配置出错
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration conf) throws Exception {
return conf.getAuthenticationManager(); // 从 AuthenticationConfiguration 获取 AuthenticationManager。
}
/**
* 配置密码编码器,指定使用 BCryptPasswordEncoder。
* BCryptPasswordEncoder 是一种强哈希算法,适合存储密码。
*
* @return PasswordEncoder 实例
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); // 创建一个 BCrypt 密码编码器实例。
}
}
SecurityConfiguration
import jakarta.servlet.DispatcherType;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
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.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* 配置类,用于定义 Spring Security 的安全策略。
*/
@Configuration
@EnableWebSecurity // 启用 Spring Security 的 Web 安全支持。
@RequiredArgsConstructor // 自动生成构造函数,为 final 字段注入依赖。
public class SecurityConfiguration {
// 自定义 JWT 认证过滤器,用于处理 JWT 认证逻辑。
private final JwTAuthenticationFilter jwtAuthFilter;
// 认证提供者,用于自定义用户认证逻辑。
private final AuthenticationProvider authenticationProvider;
/**
* 配置 Spring Security 的过滤器链。
*
* @param http HttpSecurity 对象,用于配置 Spring Security 的安全策略。
* @return 配置好的 SecurityFilterChain 实例。
* @throws Exception 如果配置出错,抛出异常。
*/
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 添加自定义 JWT 认证过滤器。
// 放置在 UsernamePasswordAuthenticationFilter 之前,确保 JWT 认证先执行。
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
// 禁用 CSRF 保护。
// CSRF(跨站请求伪造)保护通常在无状态 REST API 中被禁用,因为客户端通常使用 JWT 进行身份认证。
.csrf(csrf -> csrf.disable())
// 配置授权规则。
.authorizeHttpRequests((authorize) -> authorize
/*允许所有转发(FORWARD)和错误(ERROR)类型的请求。
*允许未经认证访问 "/api/v1/auth/**" 路径的请求
*对其他所有请求,要求用户经过认证。
*/
.dispatcherTypeMatchers(DispatcherType.FORWARD, DispatcherType.ERROR).permitAll()
.requestMatchers("/api/v1/auth/**").permitAll()
.anyRequest().authenticated()
)
/* 配置会话管理策略。
*设置为无状态(STATELESS),因为 REST API 通常不使用基于会话的认证。
*/
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
// 构建并返回配置好的 SecurityFilterChain。
return http.build();
}
}
构建测试类
RegisterRequest
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class RegisterRequest {
private String firstname;
private String lastname;
private String email;
private String password;
}
AuthenticationRequest
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class AuthenticationRequest {
String email;
String password;
}
AuthenticationResponse
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class AuthenticationResponse {
private String token;
}
AuthenticationService
import com.springread.security.config.JwTService;
import com.springread.security.user.Role;
import com.springread.security.user.User;
import com.springread.security.user.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class AuthenticationService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final JwTService jwTService;
private final AuthenticationManager authenticationManager;
public AuthenticationResponse register(RegisterRequest request) {
var user = User.builder()
.firstname(request.getFirstname())
.lastname(request.getLastname())
.email(request.getEmail())
.password(passwordEncoder.encode(request.getPassword()))
.role(Role.USER)
.build();
userRepository.save(user);
var jwtToken = jwTService.generateToken(user);
return AuthenticationResponse.builder()
.token(jwtToken)
.build();
}
public AuthenticationResponse authenticate(AuthenticationRequest request) {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getEmail(),
request.getPassword()
)
);
var user = userRepository.findByEmail(request.getEmail())
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
var jwtToken = jwTService.generateToken(user);
return AuthenticationResponse.builder()
.token(jwtToken)
.build();
}
}
AuthenticationController
import com.springread.security.config.BaseController;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/auth")
@RequiredArgsConstructor
public class AuthenticationController {
private final AuthenticationService service;
@PostMapping("/register")
public ResponseEntity<AuthenticationResponse> register(
@RequestBody RegisterRequest request
){
return ResponseEntity.ok(
service.register(request)
);
}
@PostMapping("/authenticate")
public ResponseEntity<AuthenticationResponse> authenticate(
@RequestBody AuthenticationRequest request
){
return ResponseEntity.ok(
service.authenticate(request)
);
}
}
测试
- register 接口测试
- authenticate 接口测试
增加业务代码的编码性能
UserService
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
@Service
public class UserService {
/**
* 获取当前认证用户的信息
* @return UserDetails 对象或 null(如果未认证)
*/
public UserDetails getCurrentUser() {
// 从安全上下文中获取当前认证信息
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// 检查认证对象的 Principal 是否为 UserDetails 实例
if (authentication != null && authentication.getPrincipal() instanceof UserDetails) {
return (UserDetails) authentication.getPrincipal();
}
// 未认证或认证信息无效
return null;
}
}
BaseController
import com.springread.security.user.User;
import jakarta.annotation.Resource;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor // 自动生成构造函数注入
public class BaseController {
@Resource
public UserService userService; // 注入 UserService
public User getUser(){
UserDetails userDetails = userService.getCurrentUser();
return (User)userDetails;
}
public String getCurrentUserName(){
return getUser().getUsername();
}
}
让Controller层的类继承BaseController让Controller能简单的获取到当前Http所访问的用户信息