Spring--Security
一、Security 简介
Spring Security 是一个功能强大、高度可定制的身份认证和访问控制框架,它是保护基于 Spring 的应用程序的事实标准。它的核心目标是为一套 Java 应用程序提供全面的安全服务。
- 核心思想:通过一系列的 过滤器(Filter) 来拦截进入应用程序的请求,并在这些过滤器中执行身份验证和授权检查。
- 核心特性:
- 全面的安全服务:身份验证、授权、防护攻击等。
- 高度可配置:基于注解、Java Config 或 XML。
- 与 Spring 生态无缝集成:Spring MVC、Spring Boot 等。
- 丰富的认证方式:表单登录、OAuth2、JWT、LDAP 等。
- 防护常见攻击:CSRF、Session Fixation、点击劫持等。
二、Security 核心功能
- 身份认证 - Authentication:确认用户的身份。
- 支持的认证方式:
- 表单登录:最常用的方式,提供标准的登录页面。
- HTTP Basic 认证:浏览器弹窗式的简单认证。
- OAuth 2.0:现代的单点登录和第三方授权标准。
- JWT:用于无状态 REST API 的令牌认证。
- LDAP:与企业目录服务集成。
- 支持的认证方式:
- 授权 - Authorization:确认已认证的用户是否有权限访问某个资源或执行某个操作。
- 基于URL的访问控制:例如,限制 /admin/** 路径下的所有链接只有 ROLE_ADMIN 角色的用户才能访问。
- 方法级安全:使用注解(如 @PreAuthorize)在 Service 层或 Controller 层的方法上进行精细化的权限控制。
- 动态权限:从数据库加载权限规则,实现高度灵活的权限管理。
三、Security 核心架构与组件
3.1 过滤器链(Filter Chain)
Spring Security 基于 Servlet 过滤器实现,核心是一个过滤器链:
客户端请求 → SecurityFilterChain → 应用
主要过滤器(按顺序):
- SecurityContextPersistenceFilter:在请求开始时建立 SecurityContext。
- UsernamePasswordAuthenticationFilter:处理表单登录。
- BasicAuthenticationFilter:处理 HTTP Basic 认证。
- RememberMeAuthenticationFilter:处理"记住我"功能。
- AnonymousAuthenticationFilter:为未认证用户设置匿名身份。
- ExceptionTranslationFilter:处理认证异常。
- FilterSecurityInterceptor:进行授权决策。
3.2 核心组件
-
SecurityContextHolder:
- 这是安全上下文的存储容器。它存储了当前与应用程序交互的用户的详细信息(即 Authentication 对象)。
- 默认使用 ThreadLocal 策略,意味着每个请求线程都有自己的 SecurityContext。
-
Authentication:
- 这是一个接口,代表一个认证请求或一个已认证的用户主体。它通常包含:
- principal:用户的身份标识(通常是 UserDetails 对象)。
- credentials:通常是密码,认证成功后会被擦除。
- authorities:用户被授予的权限集合(例如角色列表)。
- 这是一个接口,代表一个认证请求或一个已认证的用户主体。它通常包含:
-
UserDetails 与 UserDetailsService:
- UserDetails:一个接口,它提供了构建Authentication对象所必需的核心用户信息(用户名、密码、权限、账户是否过期等)。你需要实现这个接口来适配你自己的用户模型。
- UserDetailsService:一个核心接口,只有一个方法 loadUserByUsername(String username)。Spring Security 会调用它来根据用户名加载用户信息。这是你连接自己用户数据源(如数据库)的关键入口。
-
PasswordEncoder:
- 负责密码的编码和匹配。绝对不允许明文存储密码!
- 常用实现是 BCryptPasswordEncoder,它会自动加盐,提供强大的哈希加密。
-
FilterChainProxy (Security Filter Chain):
- Spring Security 的功能实际上是由一个过滤器链实现的。这个链中包含了一系列具有特定职责的过滤器(如 UsernamePasswordAuthenticationFilter, BasicAuthenticationFilter, FilterSecurityInterceptor 等)。
四、Security 工作流程
- 用户访问:用户访问受保护的 URL /admin。
- 拦截重定向:Spring Security 的过滤器发现用户未认证,将其重定向到 /login。
- 提交表单:用户在登录页面输入用户名和密码并提交。
- 认证处理:
- UsernamePasswordAuthenticationFilter 拦截登录请求。
- 调用我们自定义的 MyUserDetailsService.loadUserByUsername(username) 从数据库加载用户信息。
- PasswordEncoder 校验提交的密码和数据库中存储的加密密码是否匹配。
- 创建安全上下文:认证成功后,Spring Security 会创建一个已认证的 Authentication 对象,并将其放入 SecurityContextHolder。
- 授权检查:用户再次访问 /admin,此时过滤器 FilterSecurityInterceptor 会检查该用户的权限(GrantedAuthority)是否包含 ROLE_ADMIN。
- 访问结果:
- 有权限:请求继续,正常访问 /admin 页面。
- 无权限:返回 403 Forbidden 错误。
五、高级特性与集成
5.1 方法级安全控制
使用 @PreAuthorize, @PostAuthorize, @Secured 注解进行更精细的控制。
@Configuration
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class MethodSecurityConfig {// 启用方法级安全注解
}// 在服务层使用
@Service
public class ProductService {@PreAuthorize("hasRole('ADMIN') or #product.owner == authentication.name")public void updateProduct(Product product) {// 只有管理员或产品所有者可以更新}@PostAuthorize("returnObject.owner == authentication.name")public Product getProduct(Long id) {// 只能访问自己的产品}@PreFilter("filterObject.owner == authentication.name")public void updateProducts(List<Product> products) {// 过滤输入列表}@Secured({"ROLE_ADMIN", "ROLE_MANAGER"})public void deleteProduct(Long id) {// 需要管理员或经理角色}
}
5.2 JWT 集成
对于 REST API,可以创建一个自定义的过滤器来验证 JWT Token,并据此构建 Authentication 对象。
-
JWT 工具类
@Component public class JwtTokenProvider {@Value("${jwt.secret}")private String jwtSecret;@Value("${jwt.expiration}")private long jwtExpiration;public String generateToken(Authentication authentication) {UserDetails userDetails = (UserDetails) authentication.getPrincipal();Date now = new Date();Date expiryDate = new Date(now.getTime() + jwtExpiration);return Jwts.builder().setSubject(userDetails.getUsername()).setIssuedAt(now).setExpiration(expiryDate).signWith(SignatureAlgorithm.HS512, jwtSecret).compact();}public String getUsernameFromToken(String token) {Claims claims = Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody();return claims.getSubject();}public boolean validateToken(String token) {try {Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token);return true;} catch (Exception ex) {return false;}} } -
JWT 认证过滤器
public class JwtAuthenticationFilter extends OncePerRequestFilter {@Autowiredprivate JwtTokenProvider tokenProvider;@Autowiredprivate CustomUserDetailsService userDetailsService;@Overrideprotected void doFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain filterChain) throws ServletException, IOException {try {String jwt = getJwtFromRequest(request);if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {String username = tokenProvider.getUsernameFromToken(jwt);UserDetails userDetails = userDetailsService.loadUserByUsername(username);UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));SecurityContextHolder.getContext().setAuthentication(authentication);}} catch (Exception ex) {logger.error("无法设置用户认证", ex);}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;} }
5.3 OAuth2 集成
@Configuration
@EnableWebSecurity
public class OAuth2SecurityConfig {@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http.oauth2Login(oauth2 -> oauth2.loginPage("/login").defaultSuccessUrl("/dashboard").userInfoEndpoint(userInfo -> userInfo.userService(customOAuth2UserService))).authorizeHttpRequests(authz -> authz.requestMatchers("/", "/login**").permitAll().anyRequest().authenticated());return http.build();}
}
六、完整案例:基于 JWT 的 REST API 安全
在Spring Boot 2.x中整合Spring Security和JWT。主要步骤包括:
- 添加依赖
- 创建JWT工具类(生成、验证、解析Token)
- 实现UserDetailsService接口来加载用户信息
- 配置Spring Security,自定义认证和授权流程
- 创建认证接口(登录)和测试接口
步骤 1:添加依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"><dependencies><!-- Spring Boot Starter Web --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- Spring Security --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><!-- JWT 库 --><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><!-- Lombok --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency></dependencies>
</project>
配置文件 (application.yml)
server:port: 8080jwt:secret: "mySecretKey123456789012345678901234567890"expiration: 86400000 # 24小时logging:level:com.example.demo: DEBUG
步骤 2:创建JWT工具类
package com.example.demo.util;import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;@Component
public class JwtTokenUtil {@Value("${jwt.secret:mySecretKey}")private String secret;@Value("${jwt.expiration:86400000}") // 默认24小时private Long expiration;// 生成密钥private SecretKey getSigningKey() {return Keys.hmacShaKeyFor(secret.getBytes());}// 生成tokenpublic String generateToken(UserDetails userDetails) {Map<String, Object> claims = new HashMap<>();return doGenerateToken(claims, userDetails.getUsername());}private String doGenerateToken(Map<String, Object> claims, String subject) {return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis())).setExpiration(new Date(System.currentTimeMillis() + expiration)).signWith(getSigningKey(), SignatureAlgorithm.HS256).compact();}// 从token中获取用户名public String getUsernameFromToken(String token) {return getAllClaimsFromToken(token).getSubject();}// 获取过期时间public Date getExpirationDateFromToken(String token) {return getAllClaimsFromToken(token).getExpiration();}// 验证tokenpublic boolean validateToken(String token, UserDetails userDetails) {final String username = getUsernameFromToken(token);return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));}// 检查token是否过期private boolean isTokenExpired(String token) {final Date expiration = getExpirationDateFromToken(token);return expiration.before(new Date());}// 从token中获取所有信息private Claims getAllClaimsFromToken(String token) {return Jwts.parserBuilder().setSigningKey(getSigningKey()).build().parseClaimsJws(token).getBody();}
}
步骤 3:实现UserDetailsService接口
package com.example.demo.service;import com.example.demo.entity.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;import java.util.ArrayList;
import java.util.List;@Service
public class UserService implements UserDetailsService {// 模拟数据库中的用户数据private List<User> users = new ArrayList<>();public UserService() {// 初始化测试用户BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();User admin = new User();admin.setId(1L);admin.setUsername("admin");admin.setPassword(encoder.encode("admin123"));admin.setRole("ROLE_ADMIN");User user = new User();user.setId(2L);user.setUsername("user");user.setPassword(encoder.encode("user123"));user.setRole("ROLE_USER");users.add(admin);users.add(user);}@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {return users.stream().filter(u -> u.getUsername().equals(username)).findFirst().orElseThrow(() -> new UsernameNotFoundException("用户不存在: " + username));}
}
步骤 4:JWT 认证过滤器
package com.example.demo.filter;import com.example.demo.util.JwtTokenUtil;
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.stereotype.Component;
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;@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {@Autowiredprivate UserDetailsService userDetailsService;@Autowiredprivate JwtTokenUtil jwtTokenUtil;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {String authHeader = request.getHeader("Authorization");if (authHeader != null && authHeader.startsWith("Bearer ")) {String authToken = authHeader.substring(7);try {String username = jwtTokenUtil.getUsernameFromToken(authToken);if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {UserDetails userDetails = userDetailsService.loadUserByUsername(username);if (jwtTokenUtil.validateToken(authToken, userDetails)) {UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));SecurityContextHolder.getContext().setAuthentication(authentication);}}} catch (Exception e) {logger.error("JWT Token 处理失败", e);}}chain.doFilter(request, response);}
}
步骤 5:Spring Security 配置
这是最核心的配置类。
package com.example.demo.config;import com.example.demo.filter.JwtAuthenticationTokenFilter;
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.method.configuration.EnableGlobalMethodSecurity;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate UserDetailsService userDetailsService;@Autowiredprivate JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;/*** 密码编码器配置* 定义密码加密方式:BCryptPasswordEncoder 是 Spring Security 推荐的加密算法 * 特点:自动加盐、不可逆加密、每次加密结果不同但验证有效**/@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}/*** 认证管理器* 将 AuthenticationManager 暴露为 Spring Bean* 在登录认证时使用:authenticationManager.authenticate()* 负责处理用户名密码认证逻辑**/@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}/*** 用户详情服务配置* 配置认证源:使用自定义的 UserDetailsService* 关联密码编码器:告诉 Spring Security 如何验证密码* AuthenticationManagerBuilder 用于构建认证配置**/@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());}/*** HTTP 安全配置(核心部分)**/@Overrideprotected void configure(HttpSecurity http) throws Exception {http.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests().antMatchers("/auth/login", "/auth/register").permitAll().antMatchers("/admin/**").hasRole("ADMIN").anyRequest().authenticated();// 添加 JWT 过滤器配置http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);// 禁用缓存,添加缓存控制头,防止敏感信息被缓存http.headers().cacheControl();}
}
HTTP 安全配置(核心部分)
-
CSRF 防护配置:
http.csrf().disable()- CSRF(跨站请求伪造):Spring Security 默认启用的防护机制。
- 禁用原因:在 REST API + JWT 的无状态架构中,CSRF 防护不是必需的。
- 如果是有状态的 Web 应用(使用 Session Cookie),应该保持启用
-
Session 管理策略:
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)- STATELESS:完全不会创建或使用 HttpSession(JWT 方案使用)
- NEVER:不会创建 Session,但如果其他组件创建了则会使用
- IF_REQUIRED:只在需要时创建 Session(默认)
- ALWAYS:总是创建 Session
-
请求授权规则:
.authorizeRequests() .antMatchers("/auth/login", "/auth/register").permitAll() .antMatchers("/admin/**").hasRole("ADMIN") .anyRequest().authenticated();- permitAll():完全开放,不需要认证。
- hasRole(“ADMIN”):需要 ADMIN 角色(会自动添加 “ROLE_” 前缀)。
- hasAuthority(“ROLE_ADMIN”):明确指定权限名称。
- authenticated():需要认证,但不检查具体角色。
- denyAll():拒绝所有访问。
- 匹配顺序:规则按配置顺序匹配,更具体的规则应该放在前面
步骤 6:认证控制器
package com.example.demo.controller;import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import com.example.demo.util.JwtTokenUtil;
import lombok.Data;
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.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;import javax.validation.Valid;
import javax.validation.constraints.NotBlank;@RestController
@RequestMapping("/auth")
public class AuthController {@Autowiredprivate AuthenticationManager authenticationManager;@Autowiredprivate UserService userService;@Autowiredprivate JwtTokenUtil jwtTokenUtil;@PostMapping("/login")public ResponseEntity<?> login(@Valid @RequestBody LoginRequest loginRequest) {// 认证Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));SecurityContextHolder.getContext().setAuthentication(authentication);// 生成tokenUserDetails userDetails = (UserDetails) authentication.getPrincipal();String token = jwtTokenUtil.generateToken(userDetails);return ResponseEntity.ok(new JwtResponse(token));}@Datastatic class LoginRequest {@NotBlankprivate String username;@NotBlankprivate String password;}@Datastatic class JwtResponse {private String token;private String type = "Bearer";public JwtResponse(String token) {this.token = token;}}
}
步骤 7:测试控制器
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.RestController;@RestController
public class TestController {@GetMapping("/hello")public String hello() {return "Hello, World!";}@GetMapping("/user")@PreAuthorize("hasRole('USER') or hasRole('ADMIN')")public String userAccess() {return "User Content.";}@GetMapping("/admin")@PreAuthorize("hasRole('ADMIN')")public String adminAccess() {return "Admin Board.";}
}
