在互联网时代,应用安全已经不再是可选项,而是必须项。一个没有适当安全防护的应用就像没有锁的房子,任何人都可以随意进入并造成破坏。Web应用面临着各种安全威胁,包括未授权访问、数据泄露、SQL注入、跨站脚本攻击(XSS)、跨站请求伪造(CSRF)等。这些威胁不仅可能导致数据丢失,还可能损害用户信任,甚至带来法律风险。
Spring Security是Spring生态系统中的安全框架,它提供了全面的安全解决方案,包括身份认证、授权、密码加密、会话管理、CSRF防护等功能。Spring Security的设计哲学是“安全默认”,它提供了合理的默认配置,同时允许你根据具体需求进行定制。通过Spring Boot的自动配置,Spring Security能够开箱即用,你只需要添加依赖和少量配置,就能为应用添加强大的安全防护。
这节课我们将深入学习如何使用Spring Security保护Spring Boot应用,包括身份认证的实现、基于角色的访问控制、密码加密、JWT令牌认证、OAuth2集成、CSRF防护、CORS配置等内容。通过这些知识的学习,你将能够构建出安全可靠的企业级应用。
在开始实现安全功能之前,我们需要理解Web安全的基本概念。身份认证(Authentication)是验证用户身份的过程,确认用户是否真的是他所声称的那个人。授权(Authorization)是确定用户是否有权限执行某个操作的过程,即使身份已经确认,也需要检查权限。这两个概念经常被混淆,但它们解决的是不同的问题:身份认证解决“你是谁”的问题,授权解决“你能做什么”的问题。
密码加密是保护用户密码的重要手段。明文存储密码是极其危险的,一旦数据库泄露,所有用户的密码都会暴露。应该使用单向哈希函数(如BCrypt)对密码进行加密,即使攻击者获得了加密后的密码,也无法还原出原始密码。盐值(Salt)的引入进一步增强了密码的安全性,即使两个用户使用相同的密码,加密后的结果也会不同。
会话管理是Web安全的重要组成部分。HTTP协议本身是无状态的,服务器需要通过某种机制来识别用户。传统的做法是使用Session,服务器在用户登录后创建一个Session,并将Session ID返回给客户端,客户端在后续请求中携带这个Session ID。这种方式虽然简单,但在分布式环境中会遇到问题,因为Session通常存储在单台服务器的内存中。
令牌认证(Token-based Authentication)是另一种常见的认证方式,服务器在用户登录后生成一个令牌(Token),客户端在后续请求中携带这个令牌。JWT(JSON Web Token)是一种流行的令牌格式,它包含用户信息、过期时间等数据,并且可以被签名和加密。令牌认证的优势在于它是无状态的,服务器不需要存储会话信息,非常适合分布式和微服务架构。

在开始实现安全功能之前,我们需要添加Spring Security的依赖。Spring Security提供了两个起步依赖:spring-boot-starter-security用于传统的Servlet应用,spring-boot-starter-webflux已经包含了响应式安全支持。对于大多数应用,我们使用spring-boot-starter-security。
在pom.xml文件中添加Spring Security依赖:
|<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
添加依赖后,刷新Maven项目。Spring Security会自动配置基本的安全设置,默认情况下,所有端点都需要认证,并且会生成一个默认的用户名和密码(用户名是user,密码在启动日志中显示)。虽然这对于快速开始很有用,但在实际应用中,你需要配置自己的用户管理和认证逻辑。
在实现身份认证之前,我们需要创建用户实体来存储用户信息。在src/main/java/com/example/myapp/my_spring_boot_app/model包下创建User.java文件:
|package com.example.myapp.my_spring_boot_app.model; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; @Entity @Table(name = "users") public class User { @Id
这个用户实体包含了基本的用户信息,包括用户名、邮箱、密码、角色和启用状态。role字段用于存储用户的角色,Spring Security使用角色来实现基于角色的访问控制。enabled字段用于启用或禁用用户账户,这对于临时禁用用户而不删除账户很有用。
创建用户Repository接口。在src/main/java/com/example/myapp/my_spring_boot_app/repository包下创建UserRepository.java文件:
|package com.example.myapp.my_spring_boot_app.repository; import com.example.myapp.my_spring_boot_app.model.User; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import java.util.Optional; @Repository public interface UserRepository extends JpaRepository<User, Long> { Optional<User> findByUsername(String username); Optional<User> findByEmail(String email
这个Repository提供了基本的用户查询方法,包括根据用户名和邮箱查找用户,以及检查用户名和邮箱是否已存在。这些方法将在用户认证和注册功能中使用。
Spring Security使用UserDetailsService接口来加载用户信息。这个接口只有一个方法loadUserByUsername,它根据用户名加载用户信息并返回UserDetails对象。我们需要实现这个接口,让它从数据库中加载用户信息。
在src/main/java/com/example/myapp/my_spring_boot_app/config包下创建CustomUserDetailsService.java文件:
|package com.example.myapp.my_spring_boot_app.config; import com.example.myapp.my_spring_boot_app.model.User; import com.example.myapp.my_spring_boot_app.repository.UserRepository; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import java.util.Collection; import java.util.Collections; @Service public class CustomUserDetailsService implements UserDetailsService
loadUserByUsername方法从数据库中查找用户,如果用户不存在或已被禁用,抛出UsernameNotFoundException异常。User.builder()创建Spring Security的User对象,这个对象实现了UserDetails接口。getAuthorities方法将用户的角色转换为GrantedAuthority对象,Spring Security使用这些权限来决定用户能够访问哪些资源。
注意角色名称需要以ROLE_为前缀,这是Spring Security的约定。例如,如果用户的角色是ADMIN,权限应该是ROLE_ADMIN。这种约定让Spring Security能够区分角色和其他类型的权限。
现在我们需要配置Spring Security,定义哪些端点需要认证,哪些端点可以公开访问,以及如何处理认证和授权。在src/main/java/com/example/myapp/my_spring_boot_app/config包下创建SecurityConfig.java文件:
|package com.example.myapp.my_spring_boot_app.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public
@EnableWebSecurity启用Spring Security的Web安全支持。SecurityFilterChain是Spring Security 5.7+引入的新配置方式,它使用函数式配置,比之前的WebSecurityConfigurerAdapter更加灵活。
authorizeHttpRequests配置请求的授权规则。requestMatchers("/api/public/**").permitAll()允许所有用户访问/api/public/**路径,不需要认证。requestMatchers("/api/admin/**").hasRole("ADMIN")要求访问/api/admin/**路径的用户必须具有ADMIN角色。requestMatchers("/api/user/**").hasAnyRole("USER", "ADMIN")允许具有USER或ADMIN角色的用户访问。anyRequest().authenticated()要求所有其他请求都需要认证。
sessionCreationPolicy(SessionCreationPolicy.STATELESS)配置为无状态会话,这意味着不会创建HTTP Session,适合使用JWT令牌的REST API。PasswordEncoder Bean用于加密和验证密码,BCrypt是推荐的密码加密算法,它专门为密码加密设计,计算速度慢,能够有效抵抗暴力破解攻击。
在实现认证之前,让我们先实现用户注册功能,让用户能够创建账户。创建用户服务类来处理用户注册逻辑。在src/main/java/com/example/myapp/my_spring_boot_app/service包下创建UserService.java文件:
|package com.example.myapp.my_spring_boot_app.service; import com.example.myapp.my_spring_boot_app.model.User; import com.example.myapp.my_spring_boot_app.repository.UserRepository; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service @Transactional public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; public UserService(UserRepository userRepository, PasswordEncoder
registerUser方法处理用户注册逻辑。它首先检查用户名和邮箱是否已存在,如果存在则抛出异常。然后使用PasswordEncoder对密码进行加密,创建用户对象并保存到数据库。注意密码在存储之前必须加密,这是安全的基本要求。
创建注册控制器。在src/main/java/com/example/myapp/my_spring_boot_app/controller包下创建AuthController.java文件:
|package com.example.myapp.my_spring_boot_app.controller; import com.example.myapp.my_spring_boot_app.model.User; import com.example.myapp.my_spring_boot_app.service.UserService; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.HashMap; import java.util.Map; @RestController @RequestMapping("/api/public") public
这个控制器处理用户注册请求。它接收用户名、邮箱、密码和角色(可选,默认为USER),调用服务层创建用户,并返回注册结果。由于这个端点位于/api/public/**路径下,根据之前的安全配置,它不需要认证就可以访问。
Spring Security支持多种认证方式,包括基于表单的认证、HTTP Basic认证、JWT令牌认证等。让我们先实现基于表单的认证,这是最传统的Web应用认证方式。
更新SecurityConfig以支持表单登录:
|@Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/public/**", "/h2-console/**").permitAll() .requestMatchers("/api/admin/**").hasRole("ADMIN")
formLogin配置表单登录。loginPage指定登录页面的URL,loginProcessingUrl指定处理登录请求的URL,defaultSuccessUrl指定登录成功后的重定向URL,failureUrl指定登录失败后的重定向URL。Spring Security会自动处理登录表单的提交,验证用户名和密码,创建安全上下文。
logout配置登出功能。logoutUrl指定登出请求的URL,logoutSuccessUrl指定登出成功后的重定向URL,invalidateHttpSession使HTTP Session失效,deleteCookies删除指定的Cookie。
csrf配置CSRF防护。CookieCsrfTokenRepository.withHttpOnlyFalse()将CSRF令牌存储在Cookie中,并允许JavaScript访问,这对于前后端分离的应用很有用。headers().frameOptions().sameOrigin()允许同源的iframe嵌入,这对于H2控制台是必需的。
对于REST API,基于Session的认证不太适合,因为REST API应该是无状态的。JWT令牌认证是更好的选择,它让API能够完全无状态,非常适合微服务架构和前后端分离的应用。
在pom.xml中添加JWT依赖:
|<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.12.3</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.12.3</version>
创建JWT工具类。在src/main/java/com/example/myapp/my_spring_boot_app/config包下创建JwtTokenUtil.java文件:
|package com.example.myapp.my_spring_boot_app.config; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import javax.crypto.SecretKey; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.function.Function; @Component public class JwtTokenUtil { @Value("${jwt.secret:mySecretKeyForJWTTokenGenerationThatShouldBeAtLeast256BitsLong}"
这个工具类提供了JWT令牌的生成和验证功能。generateToken方法根据用户名和角色生成JWT令牌,令牌包含用户名、角色、签发时间和过期时间。validateToken方法验证令牌是否有效,包括检查用户名是否匹配和令牌是否过期。
创建JWT认证过滤器。在src/main/java/com/example/myapp/my_spring_boot_app/config包下创建JwtAuthenticationFilter.java文件:
|package com.example.myapp.my_spring_boot_app.config; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; 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 java.io.IOException; @Component
这个过滤器在每个请求中检查Authorization头,如果存在JWT令牌,就验证令牌并设置安全上下文。OncePerRequestFilter确保过滤器只对每个请求执行一次。过滤器从请求头中提取JWT令牌(格式为Bearer <token>),验证令牌的有效性,如果有效就加载用户信息并设置到安全上下文中。
更新SecurityConfig以使用JWT过滤器:
|@Bean public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtAuthenticationFilter jwtAuthenticationFilter) throws Exception { http .csrf(csrf -> csrf.disable()) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/public/**", "/h2-console/**").permitAll() .requestMatchers("/api/admin/**").hasRole("ADMIN") .
addFilterBefore将JWT过滤器添加到Spring Security的过滤器链中,在UsernamePasswordAuthenticationFilter之前执行。这样JWT过滤器会先检查JWT令牌,如果令牌有效就设置安全上下文,后续的过滤器就可以使用这个安全上下文。
在AuthController中添加登录端点来生成JWT令牌:
|@PostMapping("/login") public ResponseEntity<Map<String, Object>> login(@RequestBody Map<String, String> request) { String username = request.get("username"); String password = request.get("password"); try { authenticationManager.authenticate( new
这个登录端点验证用户名和密码,如果验证成功就生成JWT令牌并返回给客户端。客户端在后续请求中需要在Authorization头中携带这个令牌(格式为Bearer <token>)。

跨域资源共享(CORS)是浏览器实施的安全策略,它限制来自不同源的请求。当前后端分离时,前端应用通常运行在不同的端口或域名上,这就需要配置CORS来允许跨域请求。
在SecurityConfig中配置CORS:
|@Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000", "http://localhost:8081")); configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); configuration.setAllowedHeaders(Arrays.asList
CorsConfiguration定义了CORS规则。setAllowedOrigins指定允许的源,只有来自这些源的请求才会被允许。setAllowedMethods指定允许的HTTP方法。setAllowedHeaders指定允许的请求头,"*"表示允许所有请求头。setAllowCredentials(true)允许携带凭证(如Cookie),这对于需要认证的请求很重要。setMaxAge指定预检请求的缓存时间。
UrlBasedCorsConfigurationSource将CORS配置应用到所有路径。cors()配置让Spring Security使用这个CORS配置源。
除了在URL层面控制访问权限,Spring Security还支持在方法层面控制访问权限。这种方式更加灵活,让你能够在业务逻辑中精确控制权限。
在SecurityConfig中启用方法级安全:
|@Configuration @EnableWebSecurity @EnableMethodSecurity public class SecurityConfig { // 配置内容 }
@EnableMethodSecurity启用方法级安全支持。现在你可以在服务方法上使用安全注解:
|@Service public class CourseService { @PreAuthorize("hasRole('USER')") public List<Course> listCourses() { return repository.findAll(); } @PreAuthorize("hasRole('ADMIN')") public Course createCourse(String title, String description, String category, Integer difficultyLevel
@PreAuthorize在方法执行前检查权限,使用SpEL(Spring Expression Language)表达式来定义权限规则。hasRole('USER')检查用户是否具有USER角色,hasRole('ADMIN')检查用户是否具有ADMIN角色。@courseService.isOwner(#id, authentication.name)调用服务方法检查用户是否是资源的所有者,这种方式实现了基于所有权的访问控制。
@PostAuthorize在方法执行后检查权限,可以基于方法的返回值来决定是否允许访问。@Secured是另一种方法级安全注解,它使用简单的角色名称,不支持SpEL表达式。
某些端点可能包含敏感信息或执行危险操作,需要额外的保护。Spring Security提供了多种方式来保护这些端点,包括IP白名单、请求频率限制、额外的认证要求等。
让我们创建一个自定义的安全配置来保护管理端点:
|@Component public class AdminEndpointProtection { @EventListener public void onApplicationEvent(ApplicationReadyEvent event) { // 应用启动后的初始化逻辑 } @Bean public FilterRegistrationBean<Filter> adminIpFilter() { FilterRegistrationBean<Filter> registration = new FilterRegistrationBean<>(); registration.setFilter(new AdminIpFilter());
这个过滤器检查请求的来源IP,只允许白名单中的IP访问管理端点。FilterRegistrationBean用于注册自定义过滤器,setOrder(1)设置过滤器的执行顺序,数字越小越先执行。

强密码策略是保护用户账户的重要手段。虽然密码加密能够防止密码泄露,但如果用户使用弱密码,仍然容易被暴力破解。实现密码策略能够强制用户使用强密码,提高账户安全性。
创建密码验证服务。在src/main/java/com/example/myapp/my_spring_boot_app/service包下创建PasswordValidationService.java文件:
|package com.example.myapp.my_spring_boot_app.service; import org.springframework.stereotype.Service; import java.util.regex.Pattern; @Service public class PasswordValidationService { private static final int MIN_LENGTH = 8; private static final int MAX_LENGTH = 128; private static final Pattern UPPER_CASE = Pattern.compile("[A-Z]"
这个服务验证密码是否符合策略要求,包括最小长度、最大长度、必须包含大写字母、小写字母、数字和特殊字符。在用户注册和修改密码时调用这个服务来验证密码强度。
更新UserService以使用密码验证:
|private final PasswordValidationService passwordValidationService; public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder, PasswordValidationService passwordValidationService) { this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; this.passwordValidationService = passwordValidationService; } public User registerUser(String username, String email, String password, String role) { passwordValidationService.validatePassword(password); // 其他注册逻辑 }
这样在用户注册时,如果密码不符合策略要求,会抛出异常并返回错误信息,强制用户使用强密码。
账户锁定是防止暴力破解攻击的重要手段。当用户连续多次输入错误密码时,应该临时锁定账户,防止攻击者继续尝试。Spring Security提供了账户锁定功能,但我们需要自己实现锁定逻辑。
更新User实体添加锁定相关字段:
|private boolean accountNonLocked = true; private int failedLoginAttempts = 0; private LocalDateTime lockTime; public boolean isAccountNonLocked() { return accountNonLocked; } public void setAccountNonLocked(boolean accountNonLocked) { this.accountNonLocked = accountNonLocked; } public int getFailedLoginAttempts() {
更新CustomUserDetailsService以检查账户锁定:
|@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username) .orElseThrow(() -> new UsernameNotFoundException("用户不存在: " + username)); if (!user.isEnabled()) { throw new UsernameNotFoundException("用户已被禁用: " + username); } if (user.getLockTime
这个实现检查账户是否被锁定,如果锁定时间已过就自动解锁。在登录失败时增加失败次数,达到阈值时锁定账户。这种方式能够有效防止暴力破解攻击。
安全是一个持续的过程,不是一次性的配置。你需要定期更新依赖、监控安全日志、进行安全审计、及时修复安全漏洞。Spring Security提供了强大的安全功能,但正确的配置和使用同样重要。在生产环境中,应该使用HTTPS、配置强密码策略、实现账户锁定、监控异常登录行为等,全面保护应用的安全。
这部分我们系统梳理了Spring Security在Spring Boot项目中的核心安全能力:不仅掌握了身份认证、细粒度授权、密码加密等基础操作,还实战体验了JWT无状态令牌、CORS跨域安全配置、方法级安全管控,以及强密码和账户锁定等高阶安全策略。 这些实用的安全机制是企业级应用抵御常见威胁、保护用户数据的防线。
安全是一个细致入微、持续进化的过程。合理设计安全架构、及时响应风险、优化用户体验,都离不开对这些知识的灵活运用。 接下来,我们将步入应用部署与运维环节,探索如何落地生产环境、保障应用稳定在线,为用户提供持久、可靠的服务。