
在 Web 应用开发中,安全是重中之重。之前我们已经探讨了 Spring Security 认证部分的核心组件,如 AuthenticationManager 和 UserDetailsService。今天,我们将深入其另一半核心功能:授权(Authorization)。你将通过实践掌握如何在端点级别精细地控制用户的访问权限。
认证与授权的本质区别
首先,我们必须厘清两个核心概念:
- 认证(Authentication):解决“你是谁?”的问题。系统通过验证用户提供的凭据(如用户名/密码、令牌)来确认其身份。这个过程主要由
SecurityFilterChain 中的认证过滤器完成,最终会将一个包含用户信息的 Authentication 对象存入 SecurityContextHolder。
- 授权(Authorization):解决“你能做什么?”的问题。在用户身份确认之后,系统根据其拥有的权限或角色,判断是否允许其访问某个资源(如 API 端点)。
简而言之:先认证,后授权。下面的流程图清晰地展示了这一协作过程:

如何在 Spring Security 中实施授权
Spring Security 提供了多种方式来定义授权规则,主要分为两类:
- 端点级授权:在安全配置中,通过
authorizeHttpRequests 方法为 URL 路径匹配规则。
- 方法级授权:在 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)
}
现在,wayne 和 james 都能成功访问 /orders,因为他们都拥有 READ 权限。
权限 vs 角色:理解约定
在 Spring Security 中,权限(Authority) 和 角色(Role) 在技术上均由 GrantedAuthority 接口表示,没有本质区别。它们的区别主要在于语义约定:
- 权限:通常代表具体的操作,例如
READ、WRITE、DELETE。
- 角色:通常代表一组权限的集合或一种身份标识,例如
USER、ADMIN、MANAGER。
关键点在于,当你使用 .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”)。建议在项目中保持一种统一的约定,以避免混淆。

综合实战:多用户、多端点的精细授权
让我们构建一个更真实的场景,包含多个端点和更复杂的权限规则。
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()
规则解析
/health: permitAll() 意味着无需任何认证,所有人都可访问。
/orders: hasAnyAuthority(“READ“, “WRITE”) 表示用户只要拥有 READ 或 WRITE 其中一项权限即可访问。
GET /items: 仅允许拥有 READ 权限的用户查看物品列表。
POST /items: 仅允许拥有 WRITE 权限的用户创建新物品。
DELETE /items/**: 通配符 /** 匹配该路径下的所有子路径(如 /items/5),仅允许拥有 DELETE 权限的用户执行删除。
/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 的授权功能。如果你在实践过程中有任何心得或疑问,欢迎在技术社区如 云栈社区 与更多的开发者交流探讨。