外观
SpringSecurity 笔记
约 2335 字大约 8 分钟
2025-08-16
一、Spring Security 简介
1.1 什么是 Spring Security?
Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架。它是用于保护基于 Spring 的应用程序的事实标准,提供了全面的安全服务,特别针对企业级应用程序。
1.2 Spring Security 的核心功能
- 身份认证(Authentication):验证用户身份,确认"你是谁"
- 授权(Authorization):确定用户可以访问哪些资源,确认"你能做什么"
- 防护常见攻击:提供对 CSRF、Session Fixation、点击劫持等常见安全威胁的防护
- 与 Servlet API 集成:无缝集成到 Web 应用中
- 与 Spring 生态系统集成:与 Spring Boot、Spring MVC 等完美集成
1.3 为什么选择 Spring Security?
- 强大而灵活:提供全面的安全功能,同时允许高度定制
- 社区支持:作为 Spring 生态系统的一部分,拥有庞大的社区和丰富的文档
- 持续更新:定期更新以应对新的安全威胁
- 企业级支持:被众多企业级应用采用和验证
二、基础配置
2.1 依赖配置
Maven 配置:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>Gradle 配置:
implementation 'org.springframework.boot:spring-boot-starter-security'2.2 基本安全配置
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/home").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("user").password("{noop}password").roles("USER")
.and()
.withUser("admin").password("{noop}admin").roles("ADMIN");
}
}2.3 默认安全设置
当引入 Spring Security 后,应用会自动获得以下安全保护:
- 所有 HTTP 请求都需要身份验证
- 具有生成的随机密码的默认登录表单
- 基于表单的身份验证
- HTTP Basic 身份验证
- 防御 CSRF 攻击
- 会话固定保护
- 安全头集成(如 Content Security Policy)
三、身份认证
3.1 内存认证
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("user").password("{noop}password").roles("USER")
.and()
.withUser("admin").password("{noop}admin").roles("ADMIN", "USER");
}注意:{noop} 表示不使用密码编码器,生产环境应使用密码编码。
3.2 密码编码
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.passwordEncoder(passwordEncoder())
.withUser("user").password(passwordEncoder().encode("password")).roles("USER");
}3.3 JDBC 认证
@Autowired
DataSource dataSource;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery("select username, password, enabled from users where username = ?")
.authoritiesByUsernameQuery("select username, authority from authorities where username = ?");
}3.4 自定义 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("User not found"));
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
getAuthorities(user)
);
}
private Collection<? extends GrantedAuthority> getAuthorities(User user) {
return user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
.collect(Collectors.toList());
}
}3.5 登录与登出配置
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin()
.loginPage("/login") // 自定义登录页面
.loginProcessingUrl("/login-process") // 处理登录请求的URL
.usernameParameter("username") // 自定义用户名参数
.passwordParameter("password") // 自定义密码参数
.defaultSuccessUrl("/home") // 登录成功后跳转
.failureUrl("/login?error") // 登录失败后跳转
.permitAll()
.and()
.logout()
.logoutUrl("/logout") // 自定义登出URL
.logoutSuccessUrl("/login") // 登出成功后跳转
.invalidateHttpSession(true) // 使会话失效
.deleteCookies("JSESSIONID") // 删除cookies
.permitAll();
}四、访问控制
4.1 基于 URL 的访问控制
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/public/**").permitAll() // 公共资源
.antMatchers("/admin/**").hasRole("ADMIN") // 需要ADMIN角色
.antMatchers("/user/**").hasAnyRole("USER", "ADMIN") // 需要USER或ADMIN角色
.antMatchers(HttpMethod.POST, "/api/**").hasAuthority("WRITE_PRIVILEGE") // 需要特定权限
.anyRequest().authenticated() // 其他请求需要认证
.and()
.httpBasic(); // 启用HTTP Basic认证
}4.2 基于方法的访问控制
启用方法级安全:
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
}使用注解控制方法访问:
@Service
public class UserService {
@Secured("ROLE_ADMIN")
public void adminOnlyMethod() {
// 只有ADMIN角色可以访问
}
@PreAuthorize("hasRole('USER') or hasRole('ADMIN')")
public void accessibleByUserOrAdmin() {
// USER或ADMIN角色可以访问
}
@PreAuthorize("#userId == authentication.principal.id")
public void accessOnlyByOwner(Long userId) {
// 只能访问自己的数据
}
@PostAuthorize("returnObject.owner == authentication.principal")
public Document getDocument() {
// 返回结果后进行权限检查
return document;
}
}4.3 角色与权限
角色与权限的区别:
- 角色(Role):代表用户的身份,如
ROLE_ADMIN - 权限(Authority):代表用户的权限,如
WRITE_PRIVILEGE
角色前缀:
- 默认情况下,Spring Security 会自动为角色添加
ROLE_前缀 - 例如:
hasRole('ADMIN')实际检查的是ROLE_ADMIN
自定义角色前缀:
http
.authorizeRequests()
.antMatchers("/admin/**").hasAuthority("ADMIN")
.and()
.httpBasic();五、Remember Me 功能
5.1 基本 Remember Me 配置
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.rememberMe()
.tokenValiditySeconds(86400) // 24小时
.key("mySecretKey") // Remember Me token密钥
.rememberMeParameter("remember-me") // 表单中remember-me参数名
.userDetailsService(userDetailsService);
}5.2 持久化 Remember Me
@Autowired
private DataSource dataSource;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.rememberMe()
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(86400);
}
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
return tokenRepository;
}创建持久化表:
CREATE TABLE persistent_logins (
username VARCHAR(64) NOT NULL,
series VARCHAR(64) PRIMARY KEY,
token VARCHAR(64) NOT NULL,
last_used TIMESTAMP NOT NULL
);六、CSRF 保护
6.1 CSRF 原理
CSRF(Cross-Site Request Forgery,跨站请求伪造)是一种攻击方式,攻击者通过伪装成用户向应用发送恶意请求。
6.2 Spring Security 的 CSRF 保护
默认启用:Spring Security 默认启用 CSRF 保护,对于非 GET、HEAD、TRACE 和 OPTIONS 请求需要 CSRF token。
表单中添加 CSRF token:
<form action="/transfer" method="post">
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
<!-- 其他表单内容 -->
</form>6.3 配置 CSRF 保护
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) // 将token存储在cookie中
.ignoringAntMatchers("/api/**") // 忽略API端点
.and()
// 其他配置...
}针对 REST API 的 CSRF 配置:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.ignoringAntMatchers("/api/**", "/oauth/token") // 忽略API端点
.and()
.authorizeRequests()
.antMatchers("/api/**").authenticated()
.and()
.httpBasic();
}七、Spring Security 与 REST API
7.1 基于 Token 的认证
@Configuration
@EnableWebSecurity
public class RestSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}
}7.2 JWT 集成
JWT 工具类:
@Component
public class JwtTokenProvider {
@Value("${app.jwtSecret}")
private String jwtSecret;
@Value("${app.jwtExpirationInMs}")
private int jwtExpirationInMs;
public String generateToken(UserPrincipal userPrincipal) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + jwtExpirationInMs);
return Jwts.builder()
.setSubject(Long.toString(userPrincipal.getId()))
.setIssuedAt(new Date())
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
public Long getUserIdFromJWT(String token) {
Claims claims = Jwts.parser()
.setSigningKey(jwtSecret)
.parseClaimsJws(token)
.getBody();
return Long.parseLong(claims.getSubject());
}
public boolean validateToken(String authToken) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
return true;
} catch (SignatureException ex) {
// invalid JWT signature
} catch (MalformedJwtException ex) {
// invalid JWT
} catch (ExpiredJwtException ex) {
// token is expired
} catch (UnsupportedJwtException ex) {
// JWT is unsupported
} catch (IllegalArgumentException ex) {
// JWT claims string is empty
}
return false;
}
}JWT 认证过滤器:
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenProvider tokenProvider;
@Autowired
private CustomUserDetailsService customUserDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
Long userId = tokenProvider.getUserIdFromJWT(jwt);
UserDetails userDetails = customUserDetailsService.loadUserById(userId);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
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);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}7.3 CORS 配置
@Configuration
public class CorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:3000", "https://example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
};
}
}八、实用技巧与扩展
8.1 自定义认证成功/失败处理器
@Component
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication)
throws IOException, ServletException {
String targetUrl = determineTargetUrl(authentication);
if (response.isCommitted()) {
logger.debug("Response has already been committed. Unable to redirect to " + targetUrl);
return;
}
response.sendRedirect(targetUrl);
}
protected String determineTargetUrl(Authentication authentication) {
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
if (authorities.contains(new SimpleGrantedAuthority("ROLE_ADMIN"))) {
return "/admin/dashboard";
} else if (authorities.contains(new SimpleGrantedAuthority("ROLE_USER"))) {
return "/user/home";
} else {
return "/access-denied";
}
}
}
@Component
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception)
throws IOException, ServletException {
String errorMessage;
if (exception instanceof BadCredentialsException) {
errorMessage = "Invalid username or password!";
} else if (exception instanceof LockedException) {
errorMessage = exception.getMessage() + " Please contact administrator.";
} else {
errorMessage = "Unknown error - " + exception.getMessage();
}
response.sendRedirect("/login?error=" + errorMessage);
}
}配置使用自定义处理器:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin()
.successHandler(customAuthenticationSuccessHandler)
.failureHandler(customAuthenticationFailureHandler)
.and()
// 其他配置...
}8.2 自定义访问拒绝处理器
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException exc)
throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType("application/json");
Map<String, Object> data = new HashMap<>();
data.put("timestamp", System.currentTimeMillis());
data.put("status", HttpServletResponse.SC_FORBIDDEN);
data.put("error", "Forbidden");
data.put("message", "You don't have permission to access this resource");
data.put("path", request.getRequestURI());
ObjectMapper mapper = new ObjectMapper();
response.getOutputStream().println(mapper.writeValueAsString(data));
}
}配置使用自定义访问拒绝处理器:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.exceptionHandling()
.accessDeniedHandler(customAccessDeniedHandler)
.and()
// 其他配置...
}8.3 验证码集成
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CaptchaAuthenticationFilter captchaAuthenticationFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(captchaAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
// 其他配置...
}
}
@Component
public class CaptchaAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain)
throws ServletException, IOException {
if ("POST".equals(request.getMethod()) && "/login".equals(request.getServletPath())) {
String captcha = request.getParameter("captcha");
String sessionCaptcha = (String) request.getSession().getAttribute("captcha");
if (captcha == null || !captcha.equalsIgnoreCase(sessionCaptcha)) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid captcha");
return;
}
}
chain.doFilter(request, response);
}
}九、最佳实践
9.1 安全配置最佳实践
- 最小权限原则:只授予用户完成任务所需的最低权限
- 密码安全:
- 使用强密码编码器(如 BCrypt)
- 设置密码复杂度要求
- 实现密码过期策略
- 会话管理:
- 设置会话超时时间
- 启用会话固定保护
- 限制并发会话数
- 安全头配置:
http .headers() .contentSecurityPolicy("default-src 'self'") .and() .httpStrictTransportSecurity().maxAgeInSeconds(31536000).includeSubDomains() .and() .xssProtection() .and() .frameOptions().sameOrigin();
9.2 常见问题解决
问题:登录后无法跳转到目标页面
解决方案:配置 defaultSuccessUrl 并设置 true 以始终使用目标 URL
.formLogin()
.defaultSuccessUrl("/", true)问题:REST API 需要 CSRF 但移动端无法提供
解决方案:对 API 端点禁用 CSRF
.csrf()
.ignoringAntMatchers("/api/**", "/oauth/token")问题:需要同时支持表单登录和 JWT 认证
解决方案:配置多 HTTP 安全配置
@Configuration
@Order(1)
public class ApiWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/api/**")
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
@Configuration
public class FormLoginWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin();
}
}