找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3595

积分

0

好友

493

主题
发表于 前天 03:32 | 查看: 13| 回复: 0

Spring Security 授权机制:认证、授权、角色与权限

在 Web 应用开发中,安全是重中之重。之前我们已经探讨了 Spring Security 认证部分的核心组件,如 AuthenticationManagerUserDetailsService。今天,我们将深入其另一半核心功能:授权(Authorization)。你将通过实践掌握如何在端点级别精细地控制用户的访问权限。

认证与授权的本质区别

首先,我们必须厘清两个核心概念:

  • 认证(Authentication):解决“你是谁?”的问题。系统通过验证用户提供的凭据(如用户名/密码、令牌)来确认其身份。这个过程主要由 SecurityFilterChain 中的认证过滤器完成,最终会将一个包含用户信息的 Authentication 对象存入 SecurityContextHolder
  • 授权(Authorization):解决“你能做什么?”的问题。在用户身份确认之后,系统根据其拥有的权限或角色,判断是否允许其访问某个资源(如 API 端点)。

简而言之:先认证,后授权。下面的流程图清晰地展示了这一协作过程:

Spring Security 认证与授权流程图

如何在 Spring Security 中实施授权

Spring Security 提供了多种方式来定义授权规则,主要分为两类:

  1. 端点级授权:在安全配置中,通过 authorizeHttpRequests 方法为 URL 路径匹配规则。
  2. 方法级授权:在 Service 层或 Controller 方法上使用 @PreAuthorize@PostAuthorize 等注解。

本教程将聚焦于端点级授权,这是构建安全 REST API 的基础。

项目搭建与基础配置

我们将使用 Kotlin 和 Spring Boot 3.x 进行演示。首先,确保你的 build.gradle.kts 包含必要依赖。

plugins {
    kotlin("jvm") version "1.9.25"
    kotlin("plugin.spring") version "1.9.25"
    id("org.springframework.boot") version "3.5.4"
    id("io.spring.dependency-management") version "1.1.7"
}
group = "com.atomic.coding"
version = "0.0.1-SNAPSHOT"
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
    }
}
repositories {
    mavenCentral()
}
dependencies {
    implementation("org.springframework.boot:spring-boot-starter-security")
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
    testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
    testImplementation("org.springframework.security:spring-security-test")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
kotlin {
    compilerOptions {
        freeCompilerArgs.addAll("-Xjsr305=strict")
    }
}
tasks.withType<Test> {
    useJUnitPlatform()
}

为了测试,我们创建一个简单的订单查询接口。

@RestController
@RequestMapping("/orders")
class OrderController(
    private val orderService: OrderService,
) {
    @GetMapping
    fun getOrders(): List<Order> = orderService.orders()
}

从基础认证到第一个授权规则

步骤1:配置内存用户与基础安全

我们使用 InMemoryUserDetailsManager 快速创建测试用户,并配置一个要求所有请求都必须认证的基础安全规则。

@Configuration
class SecurityConfig {
    @Bean
    fun securityFilterChain(http: HttpSecurity): SecurityFilterChain =
        http
            .httpBasic { }
            .authorizeHttpRequests { request ->
                request.anyRequest().authenticated() // 所有请求需认证
            }
            .build()
    @Bean
    fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder()
    @Bean
    fun userDetailsService(): UserDetailsService {
        val wayne = User
            .withUsername("wayne")
            .password(passwordEncoder().encode("wayne123"))
            .build()
        return InMemoryUserDetailsManager(wayne)
    }
}

使用 curl 测试,提供正确或错误的 Basic Auth 凭据,会分别得到 200 OK 和 401 Unauthorized 响应。

步骤2:声明第一个授权规则

现在,我们要求访问 /orders 端点必须拥有 READ 权限(Authority)

@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain =
    http
        .httpBasic { }
        .authorizeHttpRequests { request ->
            request.requestMatchers("/orders").hasAuthority("READ") // 新增授权规则
            request.anyRequest().authenticated()
        }
        .build()

此时,用户 wayne 没有任何权限,访问 /orders 将得到 403 Forbidden。这正是我们期望的——认证通过,但授权失败。

步骤3:为用户分配权限

让我们创建两个用户,并赋予他们不同的权限集。

@Bean
fun userDetailsService(): UserDetailsService {
    val wayne = User
        .withUsername("wayne")
        .password(passwordEncoder().encode("wayne123"))
        .authorities("READ", "WRITE", "DELETE") // wayne 拥有多项权限
        .build()
    val james = User
            .withUsername("james")
            .password(passwordEncoder().encode("james123"))
            .authorities("READ") // james 仅拥有 READ 权限
            .build()
    return InMemoryUserDetailsManager(wayne, james)
}

现在,waynejames 都能成功访问 /orders,因为他们都拥有 READ 权限。

权限 vs 角色:理解约定

在 Spring Security 中,权限(Authority)角色(Role) 在技术上均由 GrantedAuthority 接口表示,没有本质区别。它们的区别主要在于语义约定:

  • 权限:通常代表具体的操作,例如 READWRITEDELETE
  • 角色:通常代表一组权限的集合或一种身份标识,例如 USERADMINMANAGER

关键点在于,当你使用 .roles(“USER”) 方法时,Spring Security 会自动为其添加 ROLE_ 前缀,最终生成的权限字符串是 ROLE_USER。而使用 .authorities(“READ”) 生成的就是 READ

// 使用 .authorities(), 权限就是 “READ”
val user1 = User
    .withUsername(“wayne”)
    .password(…)
    .authorities(“READ”)
    .build()
// 使用 .roles(), 权限是 “ROLE_USER”
val user2 = User
    .withUsername(“wayne”)
    .password(…)
    .roles(“USER”) // 实际权限:ROLE_USER
    .build()

在配置规则时,也需要对应使用 hasRole(“USER”)hasAuthority(“ROLE_USER”)建议在项目中保持一种统一的约定,以避免混淆。

Spring Security 认证与授权过滤器执行顺序

综合实战:多用户、多端点的精细授权

让我们构建一个更真实的场景,包含多个端点和更复杂的权限规则。

1. 定义多个控制器

我们创建健康检查、订单、统计和物品管理等端点。

@RestController
@RequestMapping(“/health“)
class HealthController {
    @GetMapping
    fun healthCheck(): String = “OK, and Running!”
}
@RestController
@RequestMapping(“/orders“)
class OrderController(private val orderService: OrderService) {
    @GetMapping
    fun getOrders(): List<Order> = orderService.orders()
}
@RestController
@RequestMapping(“/statistics“)
class StatisticsController(private val statisticsService: StatisticsService) {
    @GetMapping
    fun getStatistics(): Statistics = statisticsService.statistics()
}
@RestController
@RequestMapping(“/items“)
class ItemController(private val itemService: ItemService) {
    @GetMapping
    fun getItems(): List<Item> = itemService.items()
    @PostMapping
    fun addItem(@RequestParam(“name“) name: String) {
        itemService.addItem(name)
    }
    @DeleteMapping(“/{id}“)
    fun deleteItem(@PathVariable id: Int) = itemService.deleteItem(id)
}

2. 创建拥有不同权限集的用户

@Bean
fun userDetailsService(): UserDetailsService {
    val wayne = User
        .withUsername(“wayne“)
        .password(passwordEncoder().encode(“wayne123“))
        .authorities(“READ“, “STATISTICS“) // 可读,可看统计
        .build()
    val james = User
        .withUsername(“james“)
        .password(passwordEncoder().encode(“james123“))
        .authorities(“WRITE“) // 可写
        .build()
    val bill = User
        .withUsername(“bill“)
        .password(passwordEncoder().encode(“bill123“))
        .authorities(“DELETE“) // 可删除
        .build()
    return InMemoryUserDetailsManager(wayne, james, bill)
}

3. 配置精细化的安全规则

这是核心部分,我们为不同的路径和 HTTP 方法绑定不同的权限要求。

@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain =
    http
        .httpBasic { }
        .csrf { it.disable() } // 为方便API测试,暂时禁用CSRF
        .authorizeHttpRequests { request ->
            request.requestMatchers(“/health“).permitAll() // 1. 完全公开
            request.requestMatchers(“/orders“).hasAnyAuthority(“READ“, “WRITE“) // 2. 读或写权限可访问订单
            request.requestMatchers(HttpMethod.GET, “/items“).hasAuthority(“READ“) // 3. 获取物品需读权限
            request.requestMatchers(HttpMethod.POST, “/items“).hasAuthority(“WRITE“) // 4. 创建物品需写权限
            request.requestMatchers(HttpMethod.DELETE, “/items/**“).hasAuthority(“DELETE“) // 5. 删除物品需删除权限
            request.requestMatchers(“/statistics“).hasAuthority(“STATISTICS“) // 6. 查看统计需特定权限
            request.anyRequest().authenticated() // 其他所有请求需认证
        }
        .build()

规则解析

  1. /health: permitAll() 意味着无需任何认证,所有人都可访问。
  2. /orders: hasAnyAuthority(“READ“, “WRITE”) 表示用户只要拥有 READWRITE 其中一项权限即可访问。
  3. GET /items: 仅允许拥有 READ 权限的用户查看物品列表。
  4. POST /items: 仅允许拥有 WRITE 权限的用户创建新物品。
  5. DELETE /items/**: 通配符 /** 匹配该路径下的所有子路径(如 /items/5),仅允许拥有 DELETE 权限的用户执行删除。
  6. /statistics: 仅允许拥有 STATISTICS 权限的用户访问。

你可以使用通配符进行更简洁的配置,例如 request.requestMatchers(“/statistics/**”).hasAuthority(“STATISTICS”) 可以保护 /statistics 下的所有子路径。

测试与调试

你可以使用下表提供的用户凭据,通过 curl、Postman 等工具进行测试。

用户名 密码 Base64 编码 权限
wayne wayne123 d2F5bmU6d2F5bmUxMjM= READ, STATISTICS
james james123 amFtZXM6amFtZXMxMjM= WRITE
bill bill123 YmlsbDpiaWxsMTIz DELETE

测试建议:

  • 无需凭证访问 /health,应始终成功。
  • 分别用三个用户尝试访问 /orders, /items (GET/POST), /statistics,观察是否符合权限规则。

示例请求:

curl http://localhost:8080/orders \
--header “Authorization: Basic d2F5bmU6d2F5bmUxMjM=”

理解 401 与 403 状态码

在测试中,明确区分以下两种状态码对调试至关重要:

状态码 含义 原因
401 Unauthorized 认证失败 用户未提供凭据、凭据错误或已过期。系统无法识别你是谁。
403 Forbidden 授权失败 用户身份已确认,但其账户不具备访问当前请求资源所需的权限或角色。

例如:

  • 用错误密码访问受保护端点 -> 401
  • james 用户(只有WRITE权限)尝试访问 /statistics -> 403

总结

通过本文的实践,我们深入掌握了 Spring Security 端点级授权的核心机制:

  • 认证与授权的清晰界限与协作流程。
  • 权限(Authority)角色(Role) 的异同与使用约定。
  • 如何使用 authorizeHttpRequests 方法,通过匹配 URL 路径和 HTTP 方法来声明精细化的访问控制规则。
  • 如何通过不同的 HTTP 状态码(401 vs 403)快速定位安全问题。

这为构建安全的应用程序后端奠定了坚实基础。后续我们将探讨方法级安全注解、CSRF防护等更高级的主题。希望这篇实战指南能帮助你更好地驾驭 Spring Security 的授权功能。如果你在实践过程中有任何心得或疑问,欢迎在技术社区如 云栈社区 与更多的开发者交流探讨。




上一篇:Windows 11 2026年2月更新:历时两年打磨的彩色电池图标与电量百分比详解
下一篇:RTX 4060部署本地LLM实测:8年老电脑也能实现Token自由
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-10 08:53 , Processed in 0.428059 second(s), 43 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表