跳到主要内容

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;
}

小结

本章我们学习了:

  1. 安全框架概述:认证和授权的基本概念
  2. 认证机制:UserDetailsService、密码编码
  3. 授权机制:方法级安全、SpEL 表达式
  4. JWT 认证:Token 生成和验证
  5. CSRF 防护:理解和配置
  6. CORS 配置:跨域请求处理
  7. 最佳实践:密码安全、HTTPS、安全头

练习

  1. 实现一个基于数据库的用户认证系统
  2. 集成 JWT 实现无状态认证
  3. 使用方法级权限控制 API 访问
  4. 配置 CORS 允许前端跨域访问

参考资源