Spring Security 安全框架
Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架,是保护 Spring 应用的标准解决方案。
安全框架概述
什么是 Spring Security?
Spring Security 提供了完整的安全性解决方案,包括:
- 认证(Authentication):验证用户身份
- 授权(Authorization):验证用户权限
- 防止攻击:CSRF、Session Fixation、Clickjacking 等
- 集成能力:OAuth2、SAML、LDAP 等
核心架构
┌─────────────────────────────────────────────────────────────┐
│ Spring Security 架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ HTTP 请求 ──> Security Filter Chain ──> 应用程序 │
│ │ │
│ ┌───────────────┴───────────────┐ │
│ ▼ ▼ │
│ 认证过滤器 授权过滤器 │
│ (Authentication) (Authorization) │
│ │ │ │
│ ▼ ▼ │
│ AuthenticationManager AccessDecisionManager │
│ │ │ │
│ ▼ ▼ │
│ UserDetailsService 权限配置 │
│ │
└─────────────────────────────────────────────────────────────┘
核心组件
| 组件 | 说明 |
|---|---|
SecurityContextHolder | 存储当前安全上下文 |
SecurityContext | 持有 Authentication 和安全信息 |
Authentication | 表示已认证用户的身份信息 |
UserDetails | 用户详细信息接口 |
AuthenticationManager | 认证管理器 |
AccessDecisionManager | 访问决策管理器 |
快速开始
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
基本配置
添加依赖后,Spring Security 默认启用基本安全配置:
- 所有请求需要认证
- 默认用户名:
user - 默认密码:启动时生成并打印在控制台
自定义用户和密码
# application.yml
spring:
security:
user:
name: admin
password: admin123
roles: ADMIN
基本安全配置
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 配置请求授权
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/home", "/register", "/login").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
)
// 配置表单登录
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/dashboard")
.permitAll()
)
// 配置登出
.logout(logout -> logout
.logoutSuccessUrl("/login?logout")
.permitAll()
)
// 配置记住我
.rememberMe(remember -> remember
.key("uniqueAndSecret")
.tokenValiditySeconds(86400) // 1 天
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
认证机制
用户详情服务
实现 UserDetailsService 接口加载用户信息:
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("用户不存在: " + username));
return org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
.password(user.getPassword())
.roles(user.getRoles().toArray(new String[0]))
.accountExpired(!user.isAccountNonExpired())
.accountLocked(!user.isAccountNonLocked())
.credentialsExpired(!user.isCredentialsNonExpired())
.disabled(!user.isEnabled())
.build();
}
}
用户实体
@Entity
@Table(name = "t_user")
@Data
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String username;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String email;
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "t_user_roles", joinColumns = @JoinColumn(name = "user_id"))
@Column(name = "role")
private Set<String> roles = new HashSet<>();
private boolean enabled = true;
private boolean accountNonExpired = true;
private boolean accountNonLocked = true;
private boolean credentialsNonExpired = true;
}
密码编码
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
public User register(UserDTO userDTO) {
// 检查用户名是否已存在
if (userRepository.existsByUsername(userDTO.getUsername())) {
throw new BusinessException("用户名已存在");
}
User user = new User();
user.setUsername(userDTO.getUsername());
user.setPassword(passwordEncoder.encode(userDTO.getPassword())); // 加密密码
user.setEmail(userDTO.getEmail());
user.setRoles(Collections.singleton("USER"));
return userRepository.save(user);
}
}
认证流程
┌─────────────────────────────────────────────────────────────┐
│ 认证流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 用户提交用户名和密码 │
│ │ │
│ 2. UsernamePasswordAuthenticationFilter 捕获请求 │
│ │ │
│ 3. AuthenticationManager 调用认证 │
│ │ │
│ 4. DaoAuthenticationProvider 调用 UserDetailsService │
│ │ │
│ 5. UserDetailsService 从数据库加载用户信息 │
│ │ │
│ 6. PasswordEncoder 验证密码 │
│ │ │
│ 7. 认证成功:创建 Authentication 并存入 SecurityContext │
│ 认证失败:抛出 AuthenticationException │
│ │
└─────────────────────────────────────────────────────────────┘
授权机制
方法级安全
@Service
public class OrderService {
@PreAuthorize("hasRole('ADMIN')")
public List<Order> findAllOrders() {
// 只有 ADMIN 角色可以访问
return orderRepository.findAll();
}
@PreAuthorize("hasRole('USER') or hasRole('ADMIN')")
public Order findMyOrder(Long orderId, String username) {
return orderRepository.findByIdAndUsername(orderId, username);
}
@PreAuthorize("#username == authentication.principal.username")
public Order getMyOrder(Long orderId, String username) {
return orderRepository.findById(orderId);
}
@PostAuthorize("returnObject.username == authentication.principal.username")
public Order getOrderById(Long id) {
return orderRepository.findById(id);
}
@PreAuthorize("@orderService.isOwner(#id, authentication.principal.username)")
public void updateOrder(Long id, OrderDTO orderDTO) {
// 自定义权限检查
}
@PostFilter("filterObject.username == authentication.principal.username")
public List<Order> getMyOrders() {
return orderRepository.findAll();
}
}
启用方法级安全
@Configuration
@EnableMethodSecurity
public class SecurityConfig {
// ...
}
SpEL 表达式
| 表达式 | 说明 |
|---|---|
hasRole('ROLE') | 是否有指定角色 |
hasAnyRole('ROLE1', 'ROLE2') | 是否有任意一个角色 |
hasAuthority('AUTH') | 是否有指定权限 |
hasAnyAuthority('AUTH1', 'AUTH2') | 是否有任意一个权限 |
permitAll | 允许所有人 |
denyAll | 拒绝所有人 |
isAnonymous() | 是否匿名用户 |
isAuthenticated() | 是否已认证 |
isRememberMe() | 是否记住我登录 |
isFullyAuthenticated() | 是否完全认证(非记住我) |
principal | 当前用户主体 |
authentication | 当前认证对象 |
JWT 认证
添加依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.5</version>
<scope>runtime</scope>
</dependency>
JWT 工具类
@Component
public class JwtTokenProvider {
@Value("${jwt.secret:your-secret-key-must-be-at-least-256-bits-long}")
private String jwtSecret;
@Value("${jwt.expiration:86400000}")
private long jwtExpiration; // 默认 24 小时
/**
* 生成 JWT Token
*/
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return Jwts.builder()
.claims(claims)
.subject(userDetails.getUsername())
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + jwtExpiration))
.signWith(getSignKey())
.compact();
}
/**
* 从 Token 中获取用户名
*/
public String getUsernameFromToken(String token) {
return Jwts.parser()
.verifyWith(getSignKey())
.build()
.parseSignedClaims(token)
.getPayload()
.getSubject();
}
/**
* 验证 Token
*/
public boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
}
private boolean isTokenExpired(String token) {
Date expiration = Jwts.parser()
.verifyWith(getSignKey())
.build()
.parseSignedClaims(token)
.getPayload()
.getExpiration();
return expiration.before(new Date());
}
private SecretKey getSignKey() {
byte[] keyBytes = Decoders.BASE64.decode(jwtSecret);
return Keys.hmacShaKeyFor(keyBytes);
}
}
JWT 认证过滤器
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenProvider jwtTokenProvider;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String token = getJwtFromRequest(request);
if (StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)) {
String username = jwtTokenProvider.getUsernameFromToken(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
JWT 安全配置
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class JwtSecurityConfig {
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
认证控制器
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtTokenProvider jwtTokenProvider;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private UserService userService;
/**
* 用户登录
*/
@PostMapping("/login")
public Result<AuthResponse> login(@RequestBody LoginRequest loginRequest) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(),
loginRequest.getPassword()
)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
String token = jwtTokenProvider.generateToken(userDetails);
return Result.success(new AuthResponse(token, userDetails.getUsername()));
}
/**
* 用户注册
*/
@PostMapping("/register")
public Result<User> register(@RequestBody @Valid RegisterRequest registerRequest) {
User user = userService.register(registerRequest);
return Result.success(user);
}
/**
* 获取当前用户信息
*/
@GetMapping("/me")
public Result<UserInfo> getCurrentUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
// 返回用户信息
return Result.success(userService.getUserInfo(userDetails.getUsername()));
}
}
CSRF 防护
理解 CSRF
CSRF(Cross-Site Request Forgery,跨站请求伪造)是一种攻击方式:
┌─────────────────────────────────────────────────────────────┐
│ CSRF 攻击流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 用户登录银行网站,获取 Cookie │
│ │ │
│ 2. 用户访问恶意网站 │
│ │ │
│ 3. 恶意网站发起请求到银行网站(携带用户的 Cookie) │
│ │ │
│ 4. 银行网站验证 Cookie 成功,执行转账操作 │
│ │
│ 防护方式: │
│ - CSRF Token │
│ - SameSite Cookie │
│ - 验证 Referer/Origin 头 │
│ │
└─────────────────────────────────────────────────────────────┘
配置 CSRF
@Configuration
@EnableWebSecurity
public class CsrfSecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 启用 CSRF(默认启用)
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
)
// 或者对特定端点禁用
.csrf(csrf -> csrf
.ignoringRequestMatchers("/api/**")
);
return http.build();
}
}
前端处理 CSRF Token
// 从 Cookie 获取 CSRF Token
function getCsrfToken() {
const cookies = document.cookie.split(';');
for (let cookie of cookies) {
const [name, value] = cookie.trim().split('=');
if (name === 'XSRF-TOKEN') {
return decodeURIComponent(value);
}
}
return null;
}
// 请求时携带 CSRF Token
fetch('/api/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-XSRF-TOKEN': getCsrfToken()
},
body: JSON.stringify(data)
});
CORS 配置
全局 CORS 配置
@Configuration
public class CorsConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000", "https://example.com"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
在 Security 中配置 CORS
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
// ...其他配置
;
return http.build();
}
方法级权限
自定义权限评估器
@Component("orderService")
public class OrderService {
/**
* 检查是否是订单所有者
*/
public boolean isOwner(Long orderId, String username) {
Order order = orderRepository.findById(orderId).orElse(null);
return order != null && order.getUsername().equals(username);
}
}
使用自定义权限
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@PreAuthorize("@orderService.isOwner(#id, authentication.principal.username)")
@PutMapping("/{id}")
public Result<Order> updateOrder(@PathVariable Long id, @RequestBody OrderDTO orderDTO) {
// 只有订单所有者可以更新
return Result.success(orderService.update(id, orderDTO));
}
}
会话管理
配置会话
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) // 按需创建
.maximumSessions(1) // 每个用户最多 1 个会话
.maxSessionsPreventsLogin(true) // 阻止新登录
.expiredUrl("/login?expired") // 会话过期跳转
);
return http.build();
}
会话创建策略
| 策略 | 说明 |
|---|---|
IF_REQUIRED | 按需创建(默认) |
ALWAYS | 总是创建会话 |
NEVER | 从不创建会话 |
STATELESS | 无状态(JWT 场景) |
安全最佳实践
1. 密码安全
// 使用 BCrypt 加密
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); // 默认强度 10
}
// 或使用更强的加密
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // 强度 12
}
2. HTTPS 强制
@Configuration
public class SslConfig {
@Bean
public ServletWebServerFactory servletContainer() {
TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory() {
@Override
protected void postProcessContext(Context context) {
SecurityConstraint securityConstraint = new SecurityConstraint();
securityConstraint.setUserConstraint("CONFIDENTIAL");
SecurityCollection collection = new SecurityCollection();
collection.addPattern("/*");
securityConstraint.addCollection(collection);
context.addConstraint(securityConstraint);
}
};
tomcat.addAdditionalTomcatConnectors(redirectConnector());
return tomcat;
}
private Connector redirectConnector() {
Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
connector.setScheme("http");
connector.setPort(8080);
connector.setSecure(false);
connector.setRedirectPort(8443);
return connector;
}
}
3. 安全响应头
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.headers(headers -> headers
.contentTypeOptions(contentTypeOptions -> contentTypeOptions.disable())
.xssProtection(xss -> xss.disable())
.frameOptions(frame -> frame.sameOrigin())
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'; script-src 'self' 'unsafe-inline'")
)
);
return http.build();
}
4. 输入验证
@Data
public class UserDTO {
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 20, message = "用户名长度必须在3-20个字符之间")
@Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "用户名只能包含字母、数字和下划线")
private String username;
@NotBlank(message = "密码不能为空")
@Size(min = 8, max = 100, message = "密码长度必须在8-100个字符之间")
private String password;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
}
小结
本章我们学习了:
- 安全框架概述:认证和授权的基本概念
- 认证机制:UserDetailsService、密码编码
- 授权机制:方法级安全、SpEL 表达式
- JWT 认证:Token 生成和验证
- CSRF 防护:理解和配置
- CORS 配置:跨域请求处理
- 最佳实践:密码安全、HTTPS、安全头
练习
- 实现一个基于数据库的用户认证系统
- 集成 JWT 实现无状态认证
- 使用方法级权限控制 API 访问
- 配置 CORS 允许前端跨域访问