Spring Security的应用案例讲解
作者:熙客
Spring Security原理
Spring Security是一个强大的身份验证和访问控制框架,它提供了一套全面的安全解决方案,包括身份验证、授权、防止攻击等功能,用于保护Spring应用程序。它的设计理念是基于过滤器链(Filter Chain)和委托模式,通过一系列的过滤器来处理不同的安全功能。
1. 过滤器链(Filter Chain): Spring Security通过过滤器链的方式来处理安全性。每个过滤器负责一个特定的安全功能,例如身份验证、授权、会话管理等。过滤器链是有序的,请求会依次通过这些过滤器,每个过滤器都有机会对请求进行处理。
2. 委托模式: Spring Security使用委托模式将不同的安全功能委托给不同的组件。例如,身份验证(Authentication)的实现被委托给AuthenticationManager,而授权(Authorization)的实现则被委托给AccessDecisionManager。
3. 安全上下文(SecurityContext): 安全上下文是一个存储当前用户的地方,可以通过SecurityContextHolder来访问。它包含了当前用户的身份验证信息(Authentication)以及其他与安全相关的信息。
一、核心能力
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(如果已登录)或创建一个空的。 - 处理登录/认证:
- 如果是登录请求(如
/loginPOST),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_ADMIN3.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和setter
public 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和setter
public 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_name
FROM users u
LEFT JOIN user_roles ur ON u.id = ur.user_id
LEFT JOIN roles r ON ur.role_id = r.id
WHERE u.username = #{username}
</select>
<select id="findById" resultMap="userResultMap">
SELECT u.*, r.id as role_id, r.name as role_name
FROM users u
LEFT JOIN user_roles ur ON u.id = ur.user_id
LEFT JOIN roles r ON ur.role_id = r.id
WHERE 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 {
@Autowired
private UserMapper userMapper;
@Override
public 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 {
@Autowired
private 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 {
@Autowired
private JwtUtil jwtUtil;
@Autowired
private UserDetailsService userDetailsService;
/**
* 过滤器核心方法 - 处理每个HTTP请求
*
* 安全流程:
* 1. 从请求中提取JWT令牌
* 2. 验证令牌有效性
* 3. 如果有效,从令牌中提取用户名并加载用户详情
* 4. 设置安全上下文,供后续授权检查使用
*
* @param request HTTP请求
* @param response HTTP响应
* @param filterChain 过滤器链
*/
@Override
protected 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 {
@Autowired
private CustomUserDetailsService userDetailsService;
@Autowired
private JwtUtil jwtUtil;
/**
* 密码编码器Bean - 用于密码加密和验证
*
* 安全考虑:
* 1. 使用BCrypt强哈希算法,自动处理盐值
* 2. 适合密码存储,抵抗彩虹表攻击
*
* @return BCrypt密码编码器实例
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 认证管理器Bean - 暴露给其他组件使用
*
* 用途:
* 1. 在AuthController中用于手动认证用户
* 2. 可以被其他需要认证服务的组件使用
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 配置认证管理器 - 设置自定义用户详情服务和密码编码器
*
* 安全流程:
* 1. 使用自定义UserDetailsService从数据库加载用户信息
* 2. 使用BCrypt密码编码器验证密码
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
/**
* 配置HTTP安全策略 - 核心安全配置方法
*
* 安全策略:
* 1. 禁用CORS和CSRF(因使用无状态JWT认证)
* 2. 使用无状态会话管理
* 3. 配置URL访问规则(基于角色)
* 4. 添加JWT认证过滤器
*/
@Override
protected 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认证过滤器实例
*/
@Bean
public 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 {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtUtil jwtUtil;
@Autowired
private 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>
到此这篇关于Spring Security的基本使用示例的文章就介绍到这了,更多相关Spring Security使用内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
