Skip to content

Spring Security 完全指南:让你的 Spring Boot 应用安全无忧 🔐

前言:为什么需要 Spring Security?

想象一下,你精心开发了一个 Web 应用,但任何人都可以随意访问所有功能,包括敏感的用户数据和管理操作。这就像把家门敞开,任由陌生人进出一样危险!

Spring Security 就是你应用的"安保系统",它解决了以下核心问题:

  • 身份认证:确认用户是谁(Who are you?)
  • 授权控制:决定用户能做什么(What can you do?)
  • 攻击防护:抵御常见的网络攻击(CSRF、XSS等)

IMPORTANT

Spring Security 不仅仅是一个安全框架,更是现代 Web 应用不可或缺的基础设施。它让开发者能够专注于业务逻辑,而不用担心安全漏洞。

1. Spring Security 核心概念与架构

1.1 设计哲学:安全优先的防御体系

Spring Security 采用"默认安全"的设计理念:

1.2 自动配置的魔法

当你在项目中添加 spring-boot-starter-security 依赖时,Spring Boot 会自动为你配置:

kotlin
@Configuration
@EnableWebSecurity
class DefaultSecurityConfig {
    
    @Bean
    fun defaultSecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            // 所有请求都需要认证
            authorizeHttpRequests {
                authorize(anyRequest, authenticated)
            }
            // 启用表单登录
            formLogin { }
            // 启用 HTTP Basic 认证
            httpBasic { }
        }
        return http.build()
    }
}
kotlin
// 没有 Spring Security 时,你需要手动处理:
@RestController
class UserController {
    
    @GetMapping("/users")
    fun getUsers(request: HttpServletRequest): List<User> {
        // 手动检查 session
        val session = request.getSession(false)
        if (session?.getAttribute("user") == null) {
            throw UnauthorizedException("请先登录")
        }
        
        // 手动检查权限
        val user = session.getAttribute("user") as User
        if (!user.hasRole("ADMIN")) {
            throw ForbiddenException("权限不足")
        }
        
        return userService.findAll()
    }
}

TIP

Spring Security 的自动配置让你的应用在添加依赖的那一刻就拥有了基础的安全防护,这是"安全左移"理念的完美体现。

2. 默认安全配置详解

2.1 默认用户与密码

Spring Boot 会自动创建一个默认用户:

  • 用户名user
  • 密码:随机生成,在启动日志中显示
bash
Using generated security password: 78fa095d-3f4c-48b1-ad50-e24c31d5cf35

This generated password is for development use only. Your security configuration must be updated before running your application in production.

WARNING

默认密码仅用于开发环境!生产环境必须配置自定义的用户认证方式。

2.2 自定义用户配置

properties
# 自定义默认用户
spring.security.user.name=admin
spring.security.user.password=mypassword
spring.security.user.roles=ADMIN
yaml
spring:
  security:
    user:
      name: admin
      password: mypassword
      roles: ADMIN

2.3 默认安全特性

Spring Security 开箱即用提供:

默认安全特性

  • 内存用户存储UserDetailsService 实现
  • 多种认证方式:表单登录 + HTTP Basic
  • 全应用保护:包括 Actuator 端点
  • 事件发布DefaultAuthenticationEventPublisher

3. MVC Security:传统 Web 应用的安全配置

3.1 自定义安全配置

kotlin
@Configuration
@EnableWebSecurity
class WebSecurityConfig {

    @Bean
    fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            authorizeHttpRequests {
                // 静态资源无需认证
                authorize("/css/**", "/js/**", "/images/**", permitAll)
                // 公开 API 无需认证
                authorize("/api/public/**", permitAll)
                // 管理员页面需要 ADMIN 角色
                authorize("/admin/**", hasRole("ADMIN"))
                // 其他请求需要认证
                authorize(anyRequest, authenticated)
            }
            
            formLogin {
                loginPage = "/login" // 自定义登录页面
                defaultSuccessUrl = "/dashboard" // 登录成功后跳转
                failureUrl = "/login?error" // 登录失败跳转
            }
            
            logout {
                logoutUrl = "/logout"
                logoutSuccessUrl = "/login?logout"
                invalidateHttpSession = true
            }
            
            // 记住我功能
            rememberMe {
                key = "mySecretKey"
                tokenValiditySeconds = 86400 // 24小时
            }
        }
        return http.build()
    }

    @Bean
    fun userDetailsService(): UserDetailsService {
        val users = User.withDefaultPasswordEncoder()
        
        val admin = users
            .username("admin")
            .password("admin123")
            .roles("ADMIN", "USER")
            .build()
            
        val user = users
            .username("user")
            .password("user123")
            .roles("USER")
            .build()

        return InMemoryUserDetailsManager(admin, user)
    }
}

3.2 实际业务场景示例

电商系统权限控制示例
kotlin
@Configuration
@EnableWebSecurity
class ECommerceSecurityConfig {

    @Bean
    fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            authorizeHttpRequests {
                // 商品浏览无需登录
                authorize("/products/**", permitAll)
                authorize("/categories/**", permitAll)
                
                // 用户相关操作需要登录
                authorize("/cart/**", hasRole("USER"))
                authorize("/orders/**", hasRole("USER"))
                authorize("/profile/**", hasRole("USER"))
                
                // 管理员功能
                authorize("/admin/products/**", hasRole("ADMIN"))
                authorize("/admin/orders/**", hasRole("ADMIN"))
                authorize("/admin/users/**", hasRole("SUPER_ADMIN"))
                
                authorize(anyRequest, authenticated)
            }
            
            formLogin {
                loginPage = "/login"
                defaultSuccessUrl = "/dashboard"
            }
            
            // 启用 CSRF 保护(默认启用)
            csrf { }
        }
        return http.build()
    }
}

4. WebFlux Security:响应式应用的安全配置

对于使用 WebFlux 的响应式应用,Spring Security 提供了对应的响应式安全配置:

kotlin
@Configuration
@EnableWebFluxSecurity
class ReactiveSecurityConfig {

    @Bean
    fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
        return http {
            authorizeExchange {
                // 静态资源放行
                authorize(PathRequest.toStaticResources().atCommonLocations(), permitAll)
                // API 路径需要认证
                authorize("/api/**", authenticated)
                authorize(anyExchange, permitAll)
            }
            
            formLogin {
                loginPage = "/login"
            }
            
            // 响应式 CSRF 保护
            csrf { }
        }.build()
    }

    @Bean
    fun reactiveUserDetailsService(): ReactiveUserDetailsService {
        val user = User.withDefaultPasswordEncoder()
            .username("user")
            .password("password")
            .roles("USER")
            .build()

        return MapReactiveUserDetailsService(user)
    }
}

NOTE

WebFlux Security 使用 SecurityWebFilterChain 而不是 SecurityFilterChain,这是因为响应式编程模型的不同。

5. OAuth2 集成:现代化的认证授权

5.1 OAuth2 Client 配置

OAuth2 让你的应用可以集成第三方登录(如 Google、GitHub):

yaml
spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: your-google-client-id
            client-secret: your-google-client-secret
            scope: openid,profile,email
          github:
            client-id: your-github-client-id
            client-secret: your-github-client-secret
            scope: read:user,user:email
kotlin
@Configuration
@EnableWebSecurity
class OAuth2SecurityConfig {

    @Bean
    fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            authorizeHttpRequests {
                authorize("/", permitAll)
                authorize("/login", permitAll)
                authorize(anyRequest, authenticated)
            }
            
            oauth2Login {
                loginPage = "/login"
                defaultSuccessUrl = "/dashboard"
                // 自定义用户信息处理
                userInfoEndpoint {
                    userService = customOAuth2UserService()
                }
            }
        }
        return http.build()
    }

    @Bean
    fun customOAuth2UserService(): OAuth2UserService<OAuth2UserRequest, OAuth2User> {
        return CustomOAuth2UserService()
    }
}

5.2 OAuth2 Resource Server

如果你的应用需要验证 JWT 令牌:

yaml
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          # 方式1:使用 JWK Set URI
          jwk-set-uri: https://your-auth-server.com/.well-known/jwks.json
          # 方式2:使用 Issuer URI(推荐)
          issuer-uri: https://your-auth-server.com
          # 验证 audience 声明
          audiences:
            - my-api
kotlin
@RestController
@RequestMapping("/api")
class SecureApiController {

    @GetMapping("/user-info")
    @PreAuthorize("hasAuthority('SCOPE_read')")
    fun getUserInfo(authentication: JwtAuthenticationToken): Map<String, Any> {
        val jwt = authentication.token
        return mapOf(
            "username" to jwt.getClaimAsString("sub"),
            "email" to jwt.getClaimAsString("email"),
            "authorities" to authentication.authorities.map { it.authority }
        )
    }
}

5.3 OAuth2 Authorization Server

Spring Boot 还支持搭建自己的授权服务器:

OAuth2 授权服务器配置示例
yaml
spring:
  security:
    oauth2:
      authorizationserver:
        client:
          my-client:
            registration:
              client-id: my-client-id
              client-secret: "{noop}my-client-secret"
              client-authentication-methods:
                - client_secret_basic
              authorization-grant-types:
                - authorization_code
                - refresh_token
              redirect-uris:
                - http://localhost:8080/login/oauth2/code/my-client
              scopes:
                - openid
                - profile
                - email
            require-authorization-consent: true

6. 实战案例:构建完整的认证授权系统

让我们通过一个实际的博客系统来演示 Spring Security 的完整使用:

kotlin
// 用户实体
@Entity
@Table(name = "users")
data class User(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0,
    
    @Column(unique = true)
    val username: String,
    
    val password: String,
    
    val email: String,
    
    @Enumerated(EnumType.STRING)
    val role: Role,
    
    val enabled: Boolean = true
)

enum class Role {
    USER, ADMIN, MODERATOR
}

// 自定义 UserDetailsService
@Service
class CustomUserDetailsService(
    private val userRepository: UserRepository
) : UserDetailsService {

    override fun loadUserByUsername(username: String): UserDetails {
        val user = userRepository.findByUsername(username)
            ?: throw UsernameNotFoundException("用户不存在: $username")
        
        return User.builder()
            .username(user.username)
            .password(user.password)
            .authorities("ROLE_${user.role.name}")
            .disabled(!user.enabled)
            .build()
    }
}

// 安全配置
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
class BlogSecurityConfig(
    private val userDetailsService: CustomUserDetailsService
) {

    @Bean
    fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder()

    @Bean
    fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            authorizeHttpRequests {
                // 公开内容
                authorize("/", permitAll)
                authorize("/posts/**", permitAll)
                authorize("/register", permitAll)
                authorize("/css/**", "/js/**", "/images/**", permitAll)
                
                // 用户功能
                authorize("/profile/**", hasRole("USER"))
                authorize("/posts/create", hasRole("USER"))
                authorize("/posts/*/edit", hasRole("USER"))
                
                // 管理员功能
                authorize("/admin/**", hasRole("ADMIN"))
                authorize("/users/**", hasRole("ADMIN"))
                
                authorize(anyRequest, authenticated)
            }
            
            formLogin {
                loginPage = "/login"
                defaultSuccessUrl = "/dashboard"
                failureUrl = "/login?error"
            }
            
            logout {
                logoutUrl = "/logout"
                logoutSuccessUrl = "/"
                invalidateHttpSession = true
                deleteCookies("JSESSIONID")
            }
            
            rememberMe {
                key = "blog-remember-me-key"
                tokenValiditySeconds = 86400 * 7 // 7天
                userDetailsService = this@BlogSecurityConfig.userDetailsService
            }
            
            sessionManagement {
                sessionCreationPolicy = SessionCreationPolicy.IF_REQUIRED
                maximumSessions = 1
                maxSessionsPreventsLogin = false
            }
        }
        return http.build()
    }
}

// 控制器中使用方法级安全
@RestController
@RequestMapping("/api/posts")
class PostController(private val postService: PostService) {

    @GetMapping
    fun getAllPosts(): List<Post> = postService.findAll()

    @PostMapping
    @PreAuthorize("hasRole('USER')")
    fun createPost(@RequestBody post: Post, authentication: Authentication): Post {
        return postService.create(post.copy(author = authentication.name))
    }

    @PutMapping("/{id}")
    @PreAuthorize("hasRole('ADMIN') or @postService.isOwner(#id, authentication.name)")
    fun updatePost(@PathVariable id: Long, @RequestBody post: Post): Post {
        return postService.update(id, post)
    }

    @DeleteMapping("/{id}")
    @PreAuthorize("hasRole('ADMIN')")
    fun deletePost(@PathVariable id: Long) {
        postService.delete(id)
    }
}

7. 最佳实践与安全建议

7.1 密码安全

kotlin
@Configuration
class PasswordConfig {

    @Bean
    fun passwordEncoder(): PasswordEncoder {
        // 使用 BCrypt 加密,强度为 12
        return BCryptPasswordEncoder(12)
    }

    @Bean
    fun passwordPolicy(): PasswordPolicy {
        return PasswordPolicy.builder()
            .minLength(8)
            .maxLength(128)
            .requireUppercase()
            .requireLowercase()
            .requireDigits()
            .requireSpecialCharacters()
            .build()
    }
}

7.2 会话管理

kotlin
http {
    sessionManagement {
        sessionCreationPolicy = SessionCreationPolicy.IF_REQUIRED
        maximumSessions = 1 // 限制同时登录会话数
        maxSessionsPreventsLogin = false // 新登录踢掉旧会话
        sessionRegistry = sessionRegistry()
    }
}

7.3 CSRF 保护

CSRF 攻击防护

CSRF(跨站请求伪造)是常见的 Web 攻击方式。Spring Security 默认启用 CSRF 保护,但在某些场景下需要特殊处理:

kotlin
http {
    csrf {
        // API 接口可以禁用 CSRF(如果使用 JWT)
        ignoringRequestMatchers("/api/**")
        // 或者自定义 CSRF 令牌仓库
        csrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse()
    }
}

8. 常见问题与解决方案

8.1 循环依赖问题

kotlin
// ❌ 错误的做法
@Configuration
@EnableWebSecurity
class SecurityConfig(
    private val userDetailsService: UserDetailsService // 可能导致循环依赖
) {
    // ...
}

// ✅ 正确的做法
@Configuration
@EnableWebSecurity
class SecurityConfig {
    
    @Bean
    fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        // 在这里注入依赖
        http.userDetailsService(userDetailsService())
        // ...
    }
    
    @Bean
    fun userDetailsService(): UserDetailsService {
        // 创建 UserDetailsService
    }
}

8.2 静态资源访问问题

kotlin
http {
    authorizeHttpRequests {
        // 方式1:使用 PathRequest(推荐)
        authorize(PathRequest.toStaticResources().atCommonLocations(), permitAll)
        
        // 方式2:手动指定路径
        authorize("/css/**", "/js/**", "/images/**", permitAll)
    }
}

9. 总结

Spring Security 是一个功能强大且灵活的安全框架,它为 Spring Boot 应用提供了:

核心价值

  • 🛡️ 开箱即用的安全保护:零配置即可获得基础安全防护
  • 🔧 高度可定制:可以根据业务需求灵活配置
  • 🌐 现代化支持:完整的 OAuth2、JWT、SAML 支持
  • 🚀 响应式支持:WebFlux 应用的完整安全解决方案

通过本文的学习,你应该能够:

  1. ✅ 理解 Spring Security 的核心概念和工作原理
  2. ✅ 配置适合你应用的安全策略
  3. ✅ 集成 OAuth2 实现现代化认证
  4. ✅ 处理常见的安全问题和最佳实践

IMPORTANT

安全不是一次性的工作,而是需要持续关注和改进的过程。定期更新依赖、审查安全配置、监控安全事件,这些都是保障应用安全的重要环节。

记住:安全是应用的基石,而 Spring Security 让这个基石变得既坚固又易于构建! 🎯