当前位置: 首页 > news >正文

SpringSecurity的应用

官方文档

一、核心能力

1.1 身份认证 (Authentication) - “你是谁?”

  • 多种认证方式:支持几乎所有主流认证方案,如表单登录(Username/Password)、HTTP Basic、HTTP Digest、OAuth 2.0、OIDC (OpenID Connect)、SAML 2.0、LDAP、JAAS、Pre-Authentication(如CAS)等。

    • 表单登录:最常用的方式,提供默认的登录页。

    • HTTP Basic 认证:常用于 REST API。

    • OAuth 2.0 / OpenID Connect:支持第三方登录(如使用 Google, GitHub, Facebook 登录)。

    • LDAP:支持企业级目录服务认证。

    • JAAS:Java 认证和授权服务。

    • 自定义认证:你可以集成任何你想用的认证方式。

  • 灵活的密码编码:内置支持多种密码加密器(如BCrypt、SCrypt、Pbkdf2、Argon2),并强烈推荐使用BCrypt,防止密码明文存储。

  • “记住我”功能:通过持久化或基于令牌的机制实现长期登录(通过 cookie 实现长期会话)。

  • 多因素认证 (MFA):可以集成TOTP(如Google Authenticator)等二次验证手段。

  • 与现有系统集成:可以轻松地与已有的数据库表结构、用户服务进行对接。

1.2 授权 (Authorization) - “你能做什么?”

  • 请求级别授权:基于URL模式,控制用户对某个API或页面的访问权限(例如 /admin/** 需要 ROLE_ADMIN 角色)。

  • 方法级别授权:通过注解(如 @PreAuthorize@PostAuthorize@Secured)在Service层或Controller层的方法上进行精细化的权限控制。

  • 访问控制列表 (ACL):支持对领域对象(Domain Object) 进行非常细粒度的权限控制(例如,用户A可以“读”文档1,但不能“删除”它)。这是一个相对复杂的功能,适用于特定场景。

  • 动态权限:权限规则可以从数据库或其他动态源加载,实现高度灵活的权限管理。

1.3 防护常见攻击

  • CSRF (跨站请求伪造):默认开启防护,尤其对非幂等的POST、PUT等请求进行令牌验证。

  • Session Fixation (会话固定):默认防护,认证成功后会自动创建新的Session。防止 Session 固定攻击、控制并发会话数(单一用户最多同时在线数)、Session 超时处理等。

  • 点击劫持:可以通过设置HTTP头 X-Frame-Options 来防护。

  • CORS (跨域资源共享):提供便捷的配置方式。

  • 安全头:自动设置一系列安全相关的HTTP头,如 Content-Security-PolicyX-Content-Type-OptionsX-Frame-OptionsStrict-Transport-Security 等来增强浏览器端的安全性。

1.4 与其他技术无缝集成

  • Spring生态系统:与Spring Boot、Spring MVC、Spring WebFlux、Spring Data 深度整合,开箱即用,配置简便。Spring Boot 更是通过自动配置让集成变得极其简单。

  • Servlet API:基于Servlet Filter实现,适用于任何Servlet容器(Tomcat, Jetty等)。

  • 微服务架构:是构建微服务安全(如资源服务器、OAuth2客户端)的事实标准

1.5 能力边界

  • ✅ 在其边界内(做得好)

    • 应用级别的身份认证和授权。

    • 会话管理。

    • 防护基于Web的常见攻击(CSRF, XSS的头防护等)。

  • ❌ 超出其边界(不擅长或不做)

    • 网络层安全:如防火墙规则、VPN、DDoS防护、SSL/TLS终止(通常由网关/负载均衡器负责)。

    • 操作系统/容器安全:如Linux内核安全加固、Docker镜像漏洞扫描。

    • 数据安全:如数据库加密、数据传输过程中的加密(应由TLS负责)。

    • 业务逻辑漏洞:无法自动防止业务层面的漏洞(例如,水平越权:用户A通过修改ID访问了用户B的数据,需要在授权逻辑中手动编写检查代码)。

    • 安全审计与日志:虽然可以与审计集成,但专业的日志分析和审计通常由ELK、Splunk等专用系统完成。

    • WAF (Web应用防火墙) 功能:虽然能防护一些攻击,但无法替代专业的WAF来防护复杂的SQL注入、XSS等攻击(WAF基于规则和模式匹配,在更底层工作)。

二、核心架构与原理

Spring Security 的核心设计理念非常清晰:在 Servlet 过滤器(Filter)层面,为每一个进入应用的 HTTP 请求提供一系列的身份认证(Authentication)和授权(Authorization)检查

它本质上是一个过滤器链,请求必须逐一通过这条链上的每个过滤器,才能最终访问到你的 Controller 中的资源。如果任何一个过滤器检查失败,请求就会被重定向、抛出异常或直接返回错误信息。

2.1 HTTP完整的请求过程

  1. 请求到达: HTTP 请求进入应用。

  2. 遍历过滤器链: 请求依次经过 Spring Security 的各个过滤器。

  3. 建立安全上下文SecurityContextPersistenceFilter 从 Session 中恢复用户的 SecurityContext(如果已登录)或创建一个空的。

  4. 处理登录/认证:

    1. 如果是登录请求(如 /login POST),UsernamePasswordAuthenticationFilter 会拦截它,提取用户名密码,发起认证流程。

    2. 认证成功,一个包含用户信息和权限的、已认证的 Authentication 对象会被放入 SecurityContext,并通常保存到 Session 中。

  5. 处理匿名用户: 如果用户未认证,AnonymousAuthenticationFilter 会放入一个匿名 Token。

  6. 异常转换ExceptionTranslationFilter 准备捕获后续的异常。

  7. 授权决策: 请求到达最终的 FilterSecurityInterceptor

    1. 它提取当前请求对应的权限规则 (ConfigAttribute)。

    2. 它从 SecurityContextHolder 中获取已认证的 Authentication 对象。

    3. 它调用 AccessDecisionManager 进行投票决策。

  8. 决策结果:

    1. 允许访问: 调用 FilterChain.doFilter(),请求最终到达你的 Controller,返回响应。

    2. 拒绝访问: 抛出 AccessDeniedException

  9. 异常处理ExceptionTranslationFilter 捕获到异常:

    1. 如果是 AuthenticationException (认证失败,用户未知),启动认证流程:清除 SecurityContext,调用 AuthenticationEntryPoint(例如:重定向到登录页或返回 WWW-Authenticate 头)。

    2. 如果是 AccessDeniedException (授权失败,权限不足),拒绝访问:调用 AccessDeniedHandler(例如:返回 403 错误页面)。

  10. 清理上下文: 请求处理完毕,SecurityContextPersistenceFilter 将 SecurityContext 保存回 Session(如果需要),并清空 ThreadLocal

2.2 核心组成

2.2.1 过滤器链 (Filter Chain) - 心脏

这是 Spring Security 最核心的概念。整个安全机制都构建在 Servlet 规范定义的 Filter 之上。当一个 HTTP 请求到来时,它会经过一个由多个安全过滤器组成的链条。

核心过滤器(按典型顺序):

  1. ChannelProcessingFilter: 决定是否需要重定向到 HTTPS 或 HTTP。

  2. SecurityContextPersistenceFilter至关重要。在请求开始时,从配置的 SecurityContextRepository(默认是 HttpSessionSecurityContextRepository)中读取 SecurityContext(安全上下文,包含用户认证信息),并将其设置到 SecurityContextHolder 中;在请求结束后,清空 SecurityContextHolder,并可能将 SecurityContext 保存回会话。

  3. CorsFilter: 处理跨域请求 (CORS)。

  4. CsrfFilter: 提供跨站请求伪造 (CSRF) 保护。

  5. LogoutFilter: 匹配退出登录的 URL(如 /logout),处理用户退出逻辑,清除认证信息。

  6. UsernamePasswordAuthenticationFilter核心认证过滤器。尝试处理表单登录请求。它从 POST 请求中提取用户名和密码,创建一个 UsernamePasswordAuthenticationToken(一个 Authentication 接口的实现)并进行认证。

  7. DefaultLoginPageGeneratingFilter: 如果没有配置登录页面,这个过滤器会生成一个默认的登录页。

  8. DefaultLogoutPageGeneratingFilter: 生成默认的退出页面。

  9. BasicAuthenticationFilter: 处理 HTTP Basic 认证头。

  10. RequestCacheAwareFilter: 用于在用户认证成功后,恢复因登录而中断的原始请求。

  11. SecurityContextHolderAwareRequestFilter: 包装原始的 HttpServletRequest,提供一些 Spring Security 特有的方法,如 getRemoteUser()isUserInRole() 等。

  12. AnonymousAuthenticationFilter至关重要。如果此时 SecurityContextHolder 中还没有认证信息(即用户未登录),它会创建一个匿名的 Authentication 对象(AnonymousAuthenticationToken)并放入其中。这确保了安全上下文中永远有一个 Authentication 对象,避免了空指针异常,统一了“已认证”和“未认证”的处理逻辑。

  13. SessionManagementFilter: 处理会话相关的策略,如同一个用户的会话数量控制(防止同一账号多次登录)。

  14. ExceptionTranslationFilter至关重要。它是整个过滤器链的“看门人”,负责捕获后续过滤器(特别是 FilterSecurityInterceptor)抛出的异常,并将其转换为相应的行为(如重定向到登录页、返回 403 错误等)。它本身不进行认证或授权。

  15. FilterSecurityInterceptor最终大门。这是授权发生的地方。它从 SecurityContextHolder 中获取已认证的 Authentication 对象,然后根据配置的权限规则(访问属性配置,如 hasRole(‘ADMIN’)),决定是允许请求继续(调用 FilterChain.doFilter())还是拒绝访问(抛出 AccessDeniedException)。

工作流程简化视图:
HTTP Request -> Filter1 -> Filter2 -> ... -> FilterSecurityInterceptor -> DispatcherServlet -> Your Controller

2.2.2 认证 (Authentication) 核心组件

  • Authentication 接口: 代表一个认证请求或一个已认证的主体(用户)。它包含:

    • principal: 主体标识,通常是用户名、UserDetails 对象或用户ID。

    • credentials: 凭证,通常是密码。认证成功后通常会擦除。

    • authorities: 权限集合,即 GrantedAuthority 对象列表。

  • SecurityContext 接口: 持有 Authentication 对象。SecurityContextHolder.getContext().getAuthentication() 是获取当前用户信息的标准方式。

  • SecurityContextHolder: 存储 SecurityContext 的策略容器。默认使用 ThreadLocal 策略,这意味着每个线程都有自己的 SecurityContext,从而保证了用户请求之间的隔离。

  • AuthenticationManager: 认证的入口/大门。它只有一个方法:authenticate(Authentication authentication)。你通常不会直接使用它。

  • ProviderManagerAuthenticationManager 最常用的实现。它本身不处理认证,而是委托给一个 AuthenticationProvider 列表。它会遍历这个列表,直到有一个 Provider 能够处理当前的 Authentication 类型。

  • AuthenticationProvider: 执行具体认证逻辑的组件。例如:

    • DaoAuthenticationProvider: 最常用的 Provider,从数据库(DAO)中获取用户信息进行认证。它需要依赖一个 UserDetailsService

    • JwtAuthenticationProvider: 用于处理 JWT Token 认证。

    • LdapAuthenticationProvider: 用于 LDAP 认证。

  • UserDetailsService: 核心接口,只有一个方法 loadUserByUsername(String username)。它负责从存储系统(数据库、内存等)中根据用户名加载用户信息,并返回一个 UserDetails 对象。这是你需要自定义实现的最常见接口。

  • UserDetails: 接口,代表从系统存储中加载出来的用户信息,包括用户名、密码、权限、账户是否过期等。框架提供的实现是 User

认证数据流:
UsernamePasswordAuthenticationFilter -> 创建 UsernamePasswordAuthenticationToken (未认证) -> 调用 ProviderManager.authenticate() -> 委托给 DaoAuthenticationProvider -> 调用 UserDetailsService.loadUserByUsername() -> 获取 UserDetails -> 比较密码 -> 认证成功 -> 返回一个已认证的 Authentication 对象 -> 被过滤器设置到 SecurityContextHolder 中。

2.2.3 授权 (Authorization) 核心组件

  • AccessDecisionManager: 授权的决策管理器。它通过轮询一组 AccessDecisionVoter 并进行投票,最终根据投票策略决定是否允许访问。

  • AccessDecisionVoter: 投票器。它检查当前用户的 Authentication 和受保护对象所需的配置属性(ConfigAttribute,如 ROLE_ADMIN),然后投赞成、反对或弃权票。

  • ConfigAttribute: 保存着访问受保护资源(如一个URL)所需的权限信息。通常来自你的配置:.antMatchers("/admin/**").hasRole("ADMIN") 中的 hasRole("ADMIN") 就是一个 ConfigAttribute

  • FilterSecurityInterceptor: 如上所述,它是授权发生的触发器。它调用 AccessDecisionManager 进行决策。

授权数据流:
请求到达 FilterSecurityInterceptor -> 获取受保护资源的 ConfigAttribute -> 调用 AccessDecisionManager.decide() -> 轮询所有 AccessDecisionVoter.vote() -> 根据投票策略(如“一票否决”、“多数同意”)做出最终决定 -> 允许访问或抛出 AccessDeniedException -> 被上层的 ExceptionTranslationFilter 捕获处理。

三、基本使用示例

需求:SpringBoot整合Spring Security页面登陆,要求用户信息存入数据库,且密码加密存储,登录成功后返回JWT令牌用于后续请求认证;要求体现不同用户授予不同权限;要求必要的安全配置。

安全特性:

  • 密码使用BCrypt加密存储

  • 基于角色的访问控制

  • JWT令牌认证,无状态会话。完整的安全JWT流程

    • 登录:用户凭据验证 → 生成签名JWT

    • 传输:通过HTTPS传输 → 防止窃听

    • 存储:客户端安全存储 → 防止XSS

    • 使用:每个请求携带 → 认证用户

    • 验证:服务器验证签名和有效期 → 防止篡改

    • 注销:客户端删除令牌 → 服务器可黑名单

  • CSRF保护禁用(因使用JWT)

  • 会话管理设置为无状态

项目结构:

src/
├── main/
│   ├── java/com/example/demo/
│   │   ├── config/
│   │   │   ├── SecurityConfig.java
│   │   │   ├── JwtAuthenticationFilter.java
│   │   │   └── JwtUtil.java
│   │   ├── controller/
│   │   │   ├── AuthController.java
│   │   │   └── TestController.java
│   │   ├── entity/
│   │   │   ├── User.java
│   │   │   └── Role.java
│   │   ├── mapper/
│   │   │   └── UserMapper.java
│   │   ├── service/
│   │   │   ├── UserService.java
│   │   │   └── CustomUserDetailsService.java
│   │   └── DemoApplication.java
│   └── resources/
│       ├── application.properties
│       ├── schema.sql
│       └── mapper/UserMapper.xml

3.1 依赖配置 (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 http://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>2.7.0</version><relativePath/></parent><groupId>com.example</groupId><artifactId>demo</artifactId><version>1.0.0</version><properties><java.version>11</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.2.2</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.11.5</version></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>0.11.5</version><scope>runtime</scope></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><version>0.11.5</version><scope>runtime</scope></dependency></dependencies>
</project>

3.2 应用配置 (application.properties)

# 服务器端口
server.port=8080# 数据库配置
spring.datasource.url=jdbc:mysql://localhost:3306/security_demo?useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=password
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver# MyBatis配置
mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.type-aliases-package=com.example.demo.entity# JWT密钥
jwt.secret=mySecretKey
jwt.expiration=86400

密钥配置:

  • 密钥长度至少与哈希算法安全性要求一致(HS512建议至少64字节)

  • 生产环境应从安全配置源获取密钥(环境变量、密钥管理服务)

  • 定期轮换密钥

# 使用足够长且复杂的密钥
jwt.secret=mySuperLongAndComplexSecretKeyThatIsHardToGuess123!

虽然代码中不直接体现,但部署时必须使用HTTPS,防止中间人攻击,加密整个通信通道

# 生产环境应强制使用HTTPS
server.ssl.enabled=true
server.ssl.key-store=classpath:keystore.p12
server.ssl.key-store-password=password
server.ssl.key-store-type=PKCS12

3.3 数据库初始化 (schema.sql)

CREATE DATABASE IF NOT EXISTS security_demo;
USE security_demo;CREATE TABLE IF NOT EXISTS users (id INT AUTO_INCREMENT PRIMARY KEY,username VARCHAR(50) NOT NULL UNIQUE,password VARCHAR(100) NOT NULL,enabled BOOLEAN NOT NULL DEFAULT TRUE
);CREATE TABLE IF NOT EXISTS roles (id INT AUTO_INCREMENT PRIMARY KEY,name VARCHAR(50) NOT NULL UNIQUE
);CREATE TABLE IF NOT EXISTS user_roles (user_id INT NOT NULL,role_id INT NOT NULL,PRIMARY KEY (user_id, role_id),FOREIGN KEY (user_id) REFERENCES users(id),FOREIGN KEY (role_id) REFERENCES roles(id)
);-- 插入角色数据
INSERT IGNORE INTO roles (name) VALUES ('ROLE_USER');
INSERT IGNORE INTO roles (name) VALUES ('ROLE_ADMIN');-- 插入用户数据(密码使用BCrypt加密,原始密码均为"password")
INSERT IGNORE INTO users (username, password, enabled) VALUES 
('user', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVKIUi', 1),
('admin', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVKIUi', 1);-- 分配角色
INSERT IGNORE INTO user_roles (user_id, role_id) VALUES 
(1, 1), -- user has ROLE_USER
(2, 2); -- admin has ROLE_ADMIN

3.4 实体类

// User.java
package com.example.demo.entity;import java.util.List;public class User {private Long id;private String username;private String password;private Boolean enabled;private List<Role> roles;// 构造方法、getter和setterpublic User() {}public User(String username, String password) {this.username = username;this.password = password;}// 省略getter和setter
}// Role.java
package com.example.demo.entity;public class Role {private Long id;private String name;// 构造方法、getter和setterpublic Role() {}public Role(String name) {this.name = name;}// 省略getter和setter
}

3.5 MyBatis Mapper接口和XML

// UserMapper.java
package com.example.demo.mapper;import com.example.demo.entity.User;
import org.apache.ibatis.annotations.Mapper;@Mapper
public interface UserMapper {User findByUsername(String username);User findById(Long id);
}
<!-- UserMapper.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.UserMapper"><resultMap id="userResultMap" type="User"><id property="id" column="id" /><result property="username" column="username" /><result property="password" column="password" /><result property="enabled" column="enabled" /><collection property="roles" ofType="Role"><id property="id" column="role_id" /><result property="name" column="role_name" /></collection></resultMap><select id="findByUsername" resultMap="userResultMap">SELECT u.*, r.id as role_id, r.name as role_nameFROM users uLEFT JOIN user_roles ur ON u.id = ur.user_idLEFT JOIN roles r ON ur.role_id = r.idWHERE u.username = #{username}</select><select id="findById" resultMap="userResultMap">SELECT u.*, r.id as role_id, r.name as role_nameFROM users uLEFT JOIN user_roles ur ON u.id = ur.user_idLEFT JOIN roles r ON ur.role_id = r.idWHERE u.id = #{id}</select>
</mapper>

3.6 服务层

// CustomUserDetailsService.java
package com.example.demo.service;import com.example.demo.entity.User;
import com.example.demo.mapper.UserMapper;
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.List;
import java.util.stream.Collectors;@Service
public class CustomUserDetailsService implements UserDetailsService {@Autowiredprivate UserMapper userMapper;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {User user = userMapper.findByUsername(username);if (user == null) {throw new UsernameNotFoundException("用户不存在: " + username);}List<GrantedAuthority> authorities = user.getRoles().stream().map(role -> new SimpleGrantedAuthority(role.getName())).collect(Collectors.toList());return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), authorities);}
}// UserService.java
package com.example.demo.service;import com.example.demo.entity.User;
import com.example.demo.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;@Service
public class UserService {@Autowiredprivate UserMapper userMapper;public User findByUsername(String username) {return userMapper.findByUsername(username);}
}

3.7 JWT工具类

package com.example.demo.config;import io.jsonwebtoken.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;import java.util.Date;/*** JWT工具类 - 负责JWT令牌的生成、解析和验证* * 安全特性说明:* 1. 使用HMAC-SHA512算法进行签名,确保令牌完整性* 2. 设置合理的过期时间,减少令牌泄露风险* 3. 从配置文件中读取密钥,便于管理和轮换* 4. 提供完整的异常处理,防止无效令牌导致系统异常*/
@Component
public class JwtUtil {// 从配置文件中注入JWT密钥,生产环境应使用复杂且足够长的密钥@Value("${jwt.secret}")private String secret;// 从配置文件中注入JWT过期时间(秒)@Value("${jwt.expiration}")private long expiration;/*** 生成JWT令牌* * 安全考虑:* 1. 只包含必要信息(用户名),不包含敏感数据* 2. 设置签发时间和过期时间,控制令牌有效期* 3. 使用强加密算法(HS512)进行签名* * @param authentication Spring Security认证对象* @return JWT令牌字符串*/public String generateToken(Authentication authentication) {// 从认证对象中获取用户信息UserDetails userDetails = (UserDetails) authentication.getPrincipal();Date now = new Date();// 计算过期时间:当前时间 + 配置的过期时间(转换为毫秒)Date expiryDate = new Date(now.getTime() + expiration * 1000);// 构建JWT令牌return Jwts.builder().setSubject(userDetails.getUsername()) // 设置主题(用户名).setIssuedAt(now)                     // 设置签发时间.setExpiration(expiryDate)            // 设置过期时间.signWith(SignatureAlgorithm.HS512, secret) // 使用HS512算法和密钥签名.compact();                           // 生成紧凑的JWT字符串}/*** 从JWT令牌中提取用户名* * 安全考虑:* 1. 验证签名确保令牌未被篡改* 2. 解析前不信任任何令牌内容* * @param token JWT令牌* @return 用户名*/public String getUsernameFromToken(String token) {// 解析JWT令牌,验证签名并获取声明(Claims)Claims claims = Jwts.parser().setSigningKey(secret)                // 设置签名密钥.parseClaimsJws(token)                // 解析JWS(已签名的JWT).getBody();                           // 获取有效负载(Payload)// 返回主题(用户名)return claims.getSubject();}/*** 验证JWT令牌的有效性* * 安全考虑:* 1. 验证签名是否正确,防止伪造令牌* 2. 检查令牌是否过期* 3. 捕获所有可能异常,防止无效令牌导致系统异常* * @param token JWT令牌* @return 令牌是否有效*/public boolean validateToken(String token) {try {// 尝试解析令牌,如果成功则说明令牌有效Jwts.parser().setSigningKey(secret).parseClaimsJws(token);return true;} catch (SignatureException ex) {// 签名不匹配 - 令牌可能被篡改// 记录日志但不抛出异常,避免信息泄露} catch (MalformedJwtException ex) {// 令牌格式错误 - 不是有效的JWT} catch (ExpiredJwtException ex) {// 令牌已过期 - 需要重新登录获取新令牌} catch (UnsupportedJwtException ex) {// 不支持的JWT令牌 - 可能使用了错误的算法} catch (IllegalArgumentException ex) {// JWT claims string is empty - 令牌为空}// 任何异常都意味着令牌无效return false;}
}

3.8 JWT认证过滤

package com.example.demo.config;import org.springframework.beans.factory.annotation.Autowired;
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.util.StringUtils;
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;/*** JWT认证过滤器 - 处理每个请求的JWT认证* * 安全特性说明:* 1. 在每个请求前执行,确保所有请求都经过认证检查* 2. 从Authorization头中提取Bearer令牌* 3. 验证令牌有效性并设置安全上下文* 4. 即使认证失败也继续过滤器链,确保公共接口可访问*/
public class JwtAuthenticationFilter extends OncePerRequestFilter {@Autowiredprivate JwtUtil jwtUtil;@Autowiredprivate UserDetailsService userDetailsService;/*** 过滤器核心方法 - 处理每个HTTP请求* * 安全流程:* 1. 从请求中提取JWT令牌* 2. 验证令牌有效性* 3. 如果有效,从令牌中提取用户名并加载用户详情* 4. 设置安全上下文,供后续授权检查使用* * @param request HTTP请求* @param response HTTP响应* @param filterChain 过滤器链*/@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {try {// 从HTTP请求中获取JWT令牌String jwt = getJwtFromRequest(request);// 验证令牌是否存在且有效if (StringUtils.hasText(jwt) && jwtUtil.validateToken(jwt)) {// 从有效令牌中提取用户名String username = jwtUtil.getUsernameFromToken(jwt);// 从数据库加载用户详细信息(包括权限)UserDetails userDetails = userDetailsService.loadUserByUsername(username);// 创建认证令牌,包含用户详情和权限UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());// 添加请求详情(如IP地址、会话ID等)authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));// 将认证信息设置到安全上下文中,供后续授权检查使用SecurityContextHolder.getContext().setAuthentication(authentication);}} catch (Exception ex) {// 捕获所有异常,避免因认证问题导致请求失败// 记录错误日志但继续处理请求(某些接口可能允许匿名访问)logger.error("Could not set user authentication in security context", ex);}// 继续过滤器链处理(无论认证成功与否)filterChain.doFilter(request, response);}/*** 从HTTP请求中提取JWT令牌* * 安全考虑:* 1. 只接受Bearer类型的认证头* 2. 移除"Bearer "前缀,获取纯令牌* * @param request HTTP请求* @return JWT令牌或null(如果不存在)*/private String getJwtFromRequest(HttpServletRequest request) {// 从Authorization头获取Bearer令牌String bearerToken = request.getHeader("Authorization");// 检查令牌是否存在且格式正确(以"Bearer "开头)if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {// 返回去掉"Bearer "前缀的纯令牌return bearerToken.substring(7);}// 没有找到有效令牌return null;}
}

3.9 Spring Security配置

package com.example.demo.config;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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
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.authentication.UsernamePasswordAuthenticationFilter;/*** Spring Security配置类 - 定义应用程序的安全策略* * 安全特性说明:* 1. 使用无状态会话管理,适合RESTful API* 2. 配置密码编码器,确保密码安全存储* 3. 定义URL访问规则,实现基于角色的访问控制* 4. 集成JWT认证过滤器,替代默认的表单登录*/
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate CustomUserDetailsService userDetailsService;@Autowiredprivate JwtUtil jwtUtil;/*** 密码编码器Bean - 用于密码加密和验证* * 安全考虑:* 1. 使用BCrypt强哈希算法,自动处理盐值* 2. 适合密码存储,抵抗彩虹表攻击* * @return BCrypt密码编码器实例*/@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}/*** 认证管理器Bean - 暴露给其他组件使用* * 用途:* 1. 在AuthController中用于手动认证用户* 2. 可以被其他需要认证服务的组件使用*/@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}/*** 配置认证管理器 - 设置自定义用户详情服务和密码编码器* * 安全流程:* 1. 使用自定义UserDetailsService从数据库加载用户信息* 2. 使用BCrypt密码编码器验证密码*/@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());}/*** 配置HTTP安全策略 - 核心安全配置方法* * 安全策略:* 1. 禁用CORS和CSRF(因使用无状态JWT认证)* 2. 使用无状态会话管理* 3. 配置URL访问规则(基于角色)* 4. 添加JWT认证过滤器*/@Overrideprotected void configure(HttpSecurity http) throws Exception {http// 启用CORS并禁用CSRF(因使用JWT而非Cookie).cors().and().csrf().disable()// 会话管理设置为无状态(不创建和使用HTTP会话).sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()// 配置请求授权规则.authorizeRequests().antMatchers("/api/auth/**").permitAll()      // 认证接口允许匿名访问.antMatchers("/api/user/**").hasRole("USER")  // 用户接口需要USER角色.antMatchers("/api/admin/**").hasRole("ADMIN") // 管理员接口需要ADMIN角色.anyRequest().authenticated()                 // 其他所有请求需要认证.and();// 添加JWT认证过滤器到UsernamePasswordAuthenticationFilter之前http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);}/*** 创建JWT认证过滤器Bean* * 说明:* 1. 过滤器在每个请求前执行* 2. 负责提取和验证JWT令牌* 3. 设置安全上下文中的认证信息* * @return JWT认证过滤器实例*/@Beanpublic JwtAuthenticationFilter jwtAuthenticationFilter() {return new JwtAuthenticationFilter();}
}

3.10 控制器

// AuthController.java
package com.example.demo.controller;import com.example.demo.config.JwtUtil;
import com.example.demo.entity.User;
import com.example.demo.service.UserService;
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.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;import java.util.HashMap;
import java.util.Map;@RestController
@RequestMapping("/api/auth")
public class AuthController {@Autowiredprivate AuthenticationManager authenticationManager;@Autowiredprivate JwtUtil jwtUtil;@Autowiredprivate UserService userService;@PostMapping("/login")public ResponseEntity<?> login(@RequestBody Map<String, String> loginRequest) {String username = loginRequest.get("username");String password = loginRequest.get("password");Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));SecurityContextHolder.getContext().setAuthentication(authentication);String jwt = jwtUtil.generateToken(authentication);User user = userService.findByUsername(username);Map<String, Object> response = new HashMap<>();response.put("token", jwt);response.put("user", user);return ResponseEntity.ok(response);}
}// TestController.java
package com.example.demo.controller;import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("/api")
public class TestController {@GetMapping("/user/test")@PreAuthorize("hasRole('USER')")public String userAccess() {return "用户内容";}@GetMapping("/admin/test")@PreAuthorize("hasRole('ADMIN')")public String adminAccess() {return "管理员内容";}
}

3.11 主应用类

// DemoApplication.java
package com.example.demo;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
public class DemoApplication {public static void main(String[] args) {SpringApplication.run(DemoApplication.class, args);}
}

3.12 测试

登录获取令牌:

POST http://localhost:8080/api/auth/login
Content-Type: application/json{"username": "user","password": "password"
}

访问用户API:

GET http://localhost:8080/api/user/test
Authorization: Bearer <your_token>

访问管理员API:

GET http://localhost:8080/api/admin/test
Authorization: Bearer <your_token>


文章转载自:

http://Ob3Xxd9u.dLjny.cn
http://LpL8ueuw.dLjny.cn
http://l1O0Zmim.dLjny.cn
http://sDfIe7Ou.dLjny.cn
http://bRpgLCGj.dLjny.cn
http://q1Jr1rfo.dLjny.cn
http://X93sYe6p.dLjny.cn
http://UwZm9R5t.dLjny.cn
http://ZKArhEWM.dLjny.cn
http://a28RBgVG.dLjny.cn
http://bVgR0UtP.dLjny.cn
http://yHQAyMo2.dLjny.cn
http://qjeVC45G.dLjny.cn
http://Fsjd7wVO.dLjny.cn
http://kAz7fsK3.dLjny.cn
http://ViSaYJt3.dLjny.cn
http://maOz9liX.dLjny.cn
http://kbkIDqCb.dLjny.cn
http://wEc3kNBt.dLjny.cn
http://VwxCYPs6.dLjny.cn
http://PFAzxARX.dLjny.cn
http://7u4S7Ywg.dLjny.cn
http://lEcv6IKV.dLjny.cn
http://MPFIXAz1.dLjny.cn
http://Aahu2yXy.dLjny.cn
http://YckY3sUk.dLjny.cn
http://rLhqBXKY.dLjny.cn
http://Fr3B4ThR.dLjny.cn
http://syzjrsuB.dLjny.cn
http://eYW79yEX.dLjny.cn
http://www.dtcms.com/a/382606.html

相关文章:

  • 算法题(206):方格取数(动态规划)
  • 第十六周周报
  • [硬件电路-193]:双极型晶体管BJT与场效应晶体管FET异同
  • ID3v2的header中的扩展标头(Extended Header),其Size字段如何计算整个ID3的长度?
  • 【51单片机】【protues仿真】基于51单片机的篮球计时计分器系统
  • Linux -- 权限的理解
  • Java零基础学习Day10——面向对象高级1
  • 解析通过base64 传过来的图片
  • Redis 持久化策略
  • STM32---PWR
  • 0913刷题日记
  • Java基础面试篇(7)
  • 4-机器学习与大模型开发数学教程-第0章 预备知识-0-4 复数与指数形式(欧拉公式)
  • TA-VLA——将关节力矩反馈融入VLA中:无需外部力传感器,即可完成汽车充电器插入(且可多次自主尝试)
  • 从0到1开发一个商用 Agent(智能体),Agent零基础入门到精通!_零基础开发aiagent 用dify从0到1做智能体
  • android 消息队列MessageQueue源码阅读
  • Gtest2025大会学习记录(全球软件测试技术峰会)
  • oneshape acad数据集 sam-dataset
  • 堆(优先队列)
  • 【卷积神经网络详解与实例】7——经典CNN之AlexNet
  • Digital Clock 4,一款免费的个性化桌面数字时钟
  • mysql 必须在逗号分隔字符串和JSON字段之间二选一,怎么选
  • 分布式锁介绍与实现
  • 海盗王客户端dx9的64位release版测试
  • MX模拟赛总结
  • FLEXSPI_Init 硬件故障问题
  • Linux进程概念(下):进程地址空间
  • C++11_3(智能指针篇)
  • 从理论到实践:构建高效AI智能体系统的架构演进
  • 如何运用好DeepSeek为自己服务:智能增强的范式革命 | 1.3 人机认知耦合协议