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-Policy
,X-Content-Type-Options
,X-Frame-Options
,Strict-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完整的请求过程
-
请求到达: HTTP 请求进入应用。
-
遍历过滤器链: 请求依次经过 Spring Security 的各个过滤器。
-
建立安全上下文:
SecurityContextPersistenceFilter
从 Session 中恢复用户的SecurityContext
(如果已登录)或创建一个空的。 -
处理登录/认证:
-
如果是登录请求(如
/login
POST),UsernamePasswordAuthenticationFilter
会拦截它,提取用户名密码,发起认证流程。 -
认证成功,一个包含用户信息和权限的、已认证的
Authentication
对象会被放入SecurityContext
,并通常保存到 Session 中。
-
-
处理匿名用户: 如果用户未认证,
AnonymousAuthenticationFilter
会放入一个匿名 Token。 -
异常转换:
ExceptionTranslationFilter
准备捕获后续的异常。 -
授权决策: 请求到达最终的
FilterSecurityInterceptor
。-
它提取当前请求对应的权限规则 (
ConfigAttribute
)。 -
它从
SecurityContextHolder
中获取已认证的Authentication
对象。 -
它调用
AccessDecisionManager
进行投票决策。
-
-
决策结果:
-
允许访问: 调用
FilterChain.doFilter()
,请求最终到达你的 Controller,返回响应。 -
拒绝访问: 抛出
AccessDeniedException
。
-
-
异常处理:
ExceptionTranslationFilter
捕获到异常:-
如果是
AuthenticationException
(认证失败,用户未知),启动认证流程:清除SecurityContext
,调用AuthenticationEntryPoint
(例如:重定向到登录页或返回 WWW-Authenticate 头)。 -
如果是
AccessDeniedException
(授权失败,权限不足),拒绝访问:调用AccessDeniedHandler
(例如:返回 403 错误页面)。
-
-
清理上下文: 请求处理完毕,
SecurityContextPersistenceFilter
将SecurityContext
保存回 Session(如果需要),并清空ThreadLocal
。
2.2 核心组成
2.2.1 过滤器链 (Filter Chain) - 心脏
这是 Spring Security 最核心的概念。整个安全机制都构建在 Servlet 规范定义的 Filter
之上。当一个 HTTP 请求到来时,它会经过一个由多个安全过滤器组成的链条。
核心过滤器(按典型顺序):
-
ChannelProcessingFilter
: 决定是否需要重定向到 HTTPS 或 HTTP。 -
SecurityContextPersistenceFilter
: 至关重要。在请求开始时,从配置的SecurityContextRepository
(默认是HttpSessionSecurityContextRepository
)中读取SecurityContext
(安全上下文,包含用户认证信息),并将其设置到SecurityContextHolder
中;在请求结束后,清空SecurityContextHolder
,并可能将SecurityContext
保存回会话。 -
CorsFilter
: 处理跨域请求 (CORS)。 -
CsrfFilter
: 提供跨站请求伪造 (CSRF) 保护。 -
LogoutFilter
: 匹配退出登录的 URL(如/logout
),处理用户退出逻辑,清除认证信息。 -
UsernamePasswordAuthenticationFilter
: 核心认证过滤器。尝试处理表单登录请求。它从 POST 请求中提取用户名和密码,创建一个UsernamePasswordAuthenticationToken
(一个Authentication
接口的实现)并进行认证。 -
DefaultLoginPageGeneratingFilter
: 如果没有配置登录页面,这个过滤器会生成一个默认的登录页。 -
DefaultLogoutPageGeneratingFilter
: 生成默认的退出页面。 -
BasicAuthenticationFilter
: 处理 HTTP Basic 认证头。 -
RequestCacheAwareFilter
: 用于在用户认证成功后,恢复因登录而中断的原始请求。 -
SecurityContextHolderAwareRequestFilter
: 包装原始的HttpServletRequest
,提供一些 Spring Security 特有的方法,如getRemoteUser()
,isUserInRole()
等。 -
AnonymousAuthenticationFilter
: 至关重要。如果此时SecurityContextHolder
中还没有认证信息(即用户未登录),它会创建一个匿名的Authentication
对象(AnonymousAuthenticationToken
)并放入其中。这确保了安全上下文中永远有一个Authentication
对象,避免了空指针异常,统一了“已认证”和“未认证”的处理逻辑。 -
SessionManagementFilter
: 处理会话相关的策略,如同一个用户的会话数量控制(防止同一账号多次登录)。 -
ExceptionTranslationFilter
: 至关重要。它是整个过滤器链的“看门人”,负责捕获后续过滤器(特别是FilterSecurityInterceptor
)抛出的异常,并将其转换为相应的行为(如重定向到登录页、返回 403 错误等)。它本身不进行认证或授权。 -
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)
。你通常不会直接使用它。 -
ProviderManager
:AuthenticationManager
最常用的实现。它本身不处理认证,而是委托给一个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>