Spring Security 概述
什么是 Spring Security
Spring Security 是一个专注于为 Java 应用程序提供身份验证和授权的框架。它与 Spring 生态系统紧密集成,是保护 Spring 应用的事实标准。
- 核心功能
- 身份验证(Authentication):验证用户身份
- 授权(Authorization):控制用户对资源的访问权限
- 攻击防护:防止 CSRF、XSS、会话固定等攻击
- 密码加密:支持多种密码编码方式
- 主要优势
- 与 Spring 无缝集成:充分利用 Spring IoC 和 AOP
- 高度可定制:可以自定义认证和授权逻辑
- 社区活跃:文档丰富,生态完善
- 企业级安全:经过大量生产环境验证
Spring Security 的核心概念
身份验证(Authentication)
验证用户是谁的过程,通常通过用户名和密码进行。
1 2 3 4 5
| 1. 用户提供凭证(用户名/密码) 2. AuthenticationManager 验证凭证 3. 如果成功,返回 Authentication 对象 4. 将 Authentication 存入 SecurityContext
|
授权(Authorization)
确定用户能做什么的过程,基于角色或权限。
核心组件
| 组件 |
职责 |
| SecurityContextHolder |
存储当前用户的安全上下文 |
| Authentication |
表示当前认证的用户信息 |
| AuthenticationManager |
处理身份验证请求 |
| UserDetailsService |
加载用户特定数据的核心接口 |
| PasswordEncoder |
密码加密和解密 |
| GrantedAuthority |
授予用户的权限 |
Spring Security 的工作原理
过滤器链
Spring Security 通过一系列过滤器来处理安全相关的请求。
1 2 3 4 5 6 7 8 9 10 11 12
| 典型过滤器链顺序: 1. ChannelProcessingFilter - 确保请求使用正确的协议(HTTP/HTTPS) 2. CsrfFilter - CSRF 保护 3. LogoutFilter - 处理登出请求 4. UsernamePasswordAuthenticationFilter - 表单登录认证 5. BasicAuthenticationFilter - HTTP Basic 认证 6. RequestCacheAwareFilter - 缓存请求 7. SecurityContextHolderAwareRequestFilter - 包装请求 8. AnonymousAuthenticationFilter - 匿名用户 9. SessionManagementFilter - 会话管理 10. ExceptionTranslationFilter - 异常转换 11. FilterSecurityInterceptor - 访问控制决策
|
认证流程
1 2 3 4 5 6 7 8 9
| 1. 用户提交登录表单 2. UsernamePasswordAuthenticationFilter 捕获请求 3. 创建 UsernamePasswordAuthenticationToken 4. AuthenticationManager 验证 5. UserDetailsService 加载用户信息 6. PasswordEncoder 验证密码 7. 认证成功,创建 Authentication 对象 8. 存入 SecurityContext 9. 重定向到目标页面
|
环境搭建
添加依赖
Maven 依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </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>
|
Gradle 依赖
1 2 3 4
| implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'io.jsonwebtoken:jjwt-api:0.11.5' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
|
基本配置
内存用户配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/public/**").permitAll() .requestMatchers("/admin/**").hasRole("ADMIN") .anyRequest().authenticated() ) .formLogin(form -> form .loginPage("/login") .defaultSuccessUrl("/home", true) .failureUrl("/login?error") ) .logout(logout -> logout .logoutUrl("/logout") .logoutSuccessUrl("/login?logout") ); return http.build(); } @Bean public UserDetailsService userDetailsService() { UserDetails user = User.builder() .username("user") .password(passwordEncoder().encode("123456")) .roles("USER") .build(); UserDetails admin = User.builder() .username("admin") .password(passwordEncoder().encode("admin123")) .roles("ADMIN") .build(); return new InMemoryUserDetailsManager(user, admin); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
|
数据库用户配置
实体类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| @Entity @Table(name = "sys_user") @Data public class SysUser implements UserDetails { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; @Column(unique = true, nullable = false) private String username; @Column(nullable = false) private String password; private Boolean enabled = true; @ManyToMany(fetch = FetchType.EAGER) @JoinTable( name = "sys_user_role", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "role_id") ) private Set<SysRole> roles; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return roles.stream() .map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName())) .collect(Collectors.toList()); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return this.enabled; } }
|
Role 实体
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Entity @Table(name = "sys_role") @Data public class SysRole { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; @Column(unique = true, nullable = false) private String name; private String description; }
|
UserDetailsService 实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @Service public class CustomUserDetailsService implements UserDetailsService { @Autowired private UserRepository userRepository; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { SysUser user = userRepository.findByUsername(username) .orElseThrow(() -> new UsernameNotFoundException("用户不存在: " + username)); if (!user.isEnabled()) { throw new DisabledException("用户已被禁用"); } return user; } }
|
Repository
1 2 3
| public interface UserRepository extends JpaRepository<SysUser, Integer> { Optional<SysUser> findByUsername(String username); }
|
安全配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| @Configuration @EnableWebSecurity public class SecurityConfig { @Autowired private CustomUserDetailsService userDetailsService; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/public/**", "/login").permitAll() .requestMatchers("/admin/**").hasRole("ADMIN") .anyRequest().authenticated() ) .formLogin(form -> form .loginPage("/login") .defaultSuccessUrl("/home", true) ) .userDetailsService(userDetailsService); return http.build(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
|
认证方式
表单认证
登录页面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| <!DOCTYPE html> <html> <head> <title>登录</title> </head> <body> <h2>登录</h2> <form action="/login" method="post"> <div> <label>用户名:</label> <input type="text" name="username"/> </div> <div> <label>密码:</label> <input type="password" name="password"/> </div> <div> <button type="submit">登录</button> </div> </form> <div th:if="${param.error}"> 用户名或密码错误 </div> <div th:if="${param.logout}"> 已退出登录 </div> </body> </html>
|
配置表单登录
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .formLogin(form -> form .loginPage("/login") .loginProcessingUrl("/login") .defaultSuccessUrl("/home", true) .failureUrl("/login?error") .usernameParameter("username") .passwordParameter("password") ) .logout(logout -> logout .logoutUrl("/logout") .logoutSuccessUrl("/login?logout") .invalidateHttpSession(true) .deleteCookies("JSESSIONID") ); return http.build(); }
|
HTTP Basic 认证
1 2 3 4 5 6 7 8 9 10 11 12
| @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .httpBasic(basic -> basic .realmName("My Realm") ) .authorizeHttpRequests(auth -> auth .anyRequest().authenticated() ); return http.build(); }
|
JWT 认证
JWT 工具类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| @Component public class JwtUtil { private static final String SECRET_KEY = "your-secret-key-at-least-256-bits-long"; private static final long EXPIRATION_TIME = 7 * 24 * 60 * 60 * 1000; public String generateToken(UserDetails userDetails) { Map<String, Object> claims = new HashMap<>(); claims.put("roles", userDetails.getAuthorities().stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toList())); return Jwts.builder() .setClaims(claims) .setSubject(userDetails.getUsername()) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); } public String extractUsername(String token) { return Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody() .getSubject(); } public List<String> extractRoles(String token) { Claims claims = Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody(); return (List<String>) claims.get("roles"); } public boolean validateToken(String token) { try { Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token); return true; } catch (JwtException | IllegalArgumentException e) { return false; } } }
|
JWT 过滤器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| @Component public class JwtAuthenticationFilter extends OncePerRequestFilter { @Autowired private JwtUtil jwtUtil; @Autowired private UserDetailsService userDetailsService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String header = request.getHeader("Authorization"); if (header != null && header.startsWith("Bearer ")) { String token = header.substring(7); if (jwtUtil.validateToken(token)) { String username = jwtUtil.extractUsername(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); } }
|
配置 JWT 认证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Configuration @EnableWebSecurity public class SecurityConfig { @Autowired private JwtAuthenticationFilter jwtAuthenticationFilter; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers("/auth/login", "/auth/register").permitAll() .anyRequest().authenticated() ) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } }
|
认证控制器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| @RestController @RequestMapping("/auth") public class AuthController { @Autowired private AuthenticationManager authenticationManager; @Autowired private JwtUtil jwtUtil; @Autowired private UserDetailsService userDetailsService; @PostMapping("/login") public ResponseEntity<Map<String, String>> login(@RequestBody LoginRequest request) { try { Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken( request.getUsername(), request.getPassword() ) ); SecurityContextHolder.getContext().setAuthentication(authentication); UserDetails userDetails = userDetailsService.loadUserByUsername(request.getUsername()); String token = jwtUtil.generateToken(userDetails); Map<String, String> response = new HashMap<>(); response.put("token", token); return ResponseEntity.ok(response); } catch (BadCredentialsException e) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED) .body(Collections.singletonMap("error", "用户名或密码错误")); } } }
|
授权控制
URL 级别授权
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/", "/home", "/public/**").permitAll() .requestMatchers("/user/**").authenticated() .requestMatchers("/admin/**").hasRole("ADMIN") .requestMatchers("/manager/**").hasAnyRole("ADMIN", "MANAGER") .requestMatchers("/api/data/**").hasAuthority("DATA_READ") .requestMatchers("/internal/**").hasIpAddress("192.168.1.0/24") .anyRequest().authenticated() ); return http.build(); }
|
方法级别授权
启用方法安全
1 2 3 4 5 6 7
| @Configuration @EnableMethodSecurity public class MethodSecurityConfig { }
|
@PreAuthorize
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| @Service public class UserService { @PreAuthorize("hasRole('ADMIN')") public void deleteUser(Integer id) { } @PreAuthorize("hasAuthority('USER_WRITE')") public void updateUser(User user) { } @PreAuthorize("#userId == authentication.principal.id or hasRole('ADMIN')") public User getUserById(Integer userId) { } @PreAuthorize("hasRole('ADMIN') and #user.enabled") public void activateUser(User user) { } }
|
@PostAuthorize
1 2 3 4 5
| @PostAuthorize("returnObject.owner == authentication.principal.username") public Document getDocument(Integer id) { return documentRepository.findById(id); }
|
@Secured
1 2 3 4 5 6 7 8 9
| @Secured("ROLE_ADMIN") public void deleteProduct(Integer id) { }
@Secured({"ROLE_ADMIN", "ROLE_MANAGER"}) public void updateProduct(Product product) { }
|
@RolesAllowed(JSR-250)
1 2 3 4
| @RolesAllowed("ADMIN") public void deleteOrder(Integer id) { }
|
自定义权限表达式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| @Component("customPermissionEvaluator") public class CustomPermissionEvaluator implements PermissionEvaluator { @Override public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) { if (authentication == null || targetDomainObject == null) { return false; } String userType = authentication.getAuthorities().stream() .findFirst() .map(GrantedAuthority::getAuthority) .orElse(""); return "ROLE_ADMIN".equals(userType) || ("ROLE_USER".equals(userType) && "READ".equals(permission)); } @Override public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) { return hasPermission(authentication, null, permission); } }
|
1 2 3 4 5
| @PreAuthorize("hasPermission(#document, 'READ')") public Document getDocument(Integer id) { }
|
安全防护
CSRF 保护
默认启用
Spring Security 默认启用 CSRF 保护,适用于表单提交。
禁用 CSRF(API 项目)
1 2 3 4 5 6 7
| @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()); return http.build(); }
|
前端携带 CSRF Token
1 2 3 4
| <form action="/update" method="post"> <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/> </form>
|
CORS 配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @Configuration public class CorsConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOrigin("*"); config.addAllowedMethod("*"); config.addAllowedHeader("*"); config.setAllowCredentials(false); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }
|
或在 Security 配置中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .cors(cors -> cors.configurationSource(request -> { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOrigin("http://localhost:3000"); config.addAllowedMethod("*"); config.addAllowedHeader("*"); config.setAllowCredentials(true); return config; })); return http.build(); }
|
密码加密
BCrypt(推荐)
1 2 3 4 5 6 7 8 9 10
| @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
String encodedPassword = passwordEncoder.encode("rawPassword");
boolean matches = passwordEncoder.matches("rawPassword", encodedPassword);
|
其他编码器
1 2 3 4 5 6 7 8
| new SCryptPasswordEncoder();
new Argon2PasswordEncoder();
new Pbkdf2PasswordEncoder();
|
会话管理
1 2 3 4 5 6 7 8 9 10 11 12
| @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .sessionManagement(session -> session .maximumSessions(1) .maxSessionsPreventsLogin(false) .expiredUrl("/login?expired") .sessionFixation().migrateSession() ); return http.build(); }
|
最佳实践
最小权限原则
1 2 3 4 5 6 7
| .authorizeHttpRequests(auth -> auth .requestMatchers("/public/**").permitAll() .requestMatchers("/user/**").hasRole("USER") .requestMatchers("/admin/**").hasRole("ADMIN") .anyRequest().denyAll() )
|
使用 HTTPS
1 2 3 4 5 6
| server: ssl: key-store: classpath:keystore.p12 key-store-password: changeit key-store-type: PKCS12 key-alias: tomcat
|
定期更新密钥
日志记录
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @Slf4j @Component public class AuthenticationEventListener { @EventListener public void handleAuthenticationSuccess(AuthenticationSuccessEvent event) { log.info("用户登录成功: {}", event.getAuthentication().getName()); } @EventListener public void handleAuthenticationFailure(AuthenticationFailureBadCredentialsEvent event) { log.warn("登录失败: {}, IP: {}", event.getAuthentication().getName(), ((WebAuthenticationDetails) event.getAuthentication().getDetails()).getRemoteAddress() ); } }
|
常见问题
无限重定向
1 2 3 4 5 6 7 8 9
| 问题:登录后无限重定向到登录页
错误原因: 1. 登录成功 URL 也需要认证 2. CSRF Token 缺失
解决方案: 1. 确保 defaultSuccessUrl 不需要认证或正确配置 2. 表单中添加 CSRF Token
|
权限不生效
1 2 3 4 5 6 7 8 9
| 问题:@PreAuthorize 注解不生效
错误原因: 1. 未启用方法安全 2. 代理问题
解决方案: 1. 添加 @EnableMethodSecurity 或 @EnableGlobalMethodSecurity 2. 确保调用的是代理对象,不是直接 new 的对象
|
报错处理
💗💗 Spring Security 报错:AccessDeniedException
1 2 3 4 5 6 7 8 9 10 11
| 错误信息: Access Denied
错误原因: 1. 用户没有所需权限 2. 未登录访问受保护资源
解决方案: 1. 检查用户角色和权限是否正确 2. 确认用户已登录 3. 检查 URL 匹配规则
|
💗💗 Spring Security 报错:BadCredentialsException
1 2 3 4 5 6 7 8 9 10 11
| 错误信息: Bad credentials
错误原因: 1. 用户名或密码错误 2. 密码未加密或加密方式不匹配
解决方案: 1. 检查用户名和密码是否正确 2. 确认数据库中密码已正确加密 3. 检查 PasswordEncoder 配置
|
学习资源
- 视频
- 尚硅谷 SpringSecurity 教程:
https://www.bilibili.com/video/BV15a411A7kP
- SpringSecurity + OAuth2 教程:
https://www.bilibili.com/video/BV14b4y1A7Wz
- 官方文档
- Spring Security 官方文档:
https://spring.io/projects/spring-security
- Spring Security GitHub:
https://github.com/spring-projects/spring-security
- 书籍
- 《Spring Security 实战》:陈韶健著
- 《Spring Security 权威指南》:Rob Winch 著
- 教程
- Spring Security 入门教程:
https://www.baeldung.com/category/spring-security/
- Baeldung Spring Security 教程:
https://www.baeldung.com/category/spring-security/
- 社区
- Stack Overflow Spring Security 标签:
https://stackoverflow.com/questions/tagged/spring-security
- Spring 中文社区:
https://spring.io/projects