纲要
- 认证与授权的区别
AccessDecisionManager核心接口及其decide方法AccessDecisionVoter投票器接口及三种决策策略RoleVoter角色投票器与ROLE_前缀机制- 常用安全表达式速览(
hasRole、hasAuthority、permitAll、denyAll、access等) - 基于
access表达式的细粒度权限控制 - 实战:限制“用户只能访问自身资源,管理员可访问全部”
- 自定义权限逻辑封装为 Spring Bean 并在表达式内调用
- 项目结构、安全配置、控制器及单元测试完整代码
认证与授权
认证解决“你是谁”,授权解决“你能做什么”。例如小区门禁认证通过后,你仍不能进入别人家,只能进入自己家,这便是授权。Spring Security 的授权模型基于投票决策,由AccessDecisionManager统一协调。
授权核心组件
AccessDecisionManager
该接口负责根据安全对象和配置属性做出授权决定,核心方法如下:
voiddecide(Authenticationauthentication,Objectobject,Collection<ConfigAttribute>configAttributes)throwsAccessDeniedException,InsufficientAuthenticationException;参数中的object是一个安全对象,Spring Security 并没有限制具体类型,可以是FilterInvocation(Web 请求)或MethodInvocation(方法调用),这使得框架可以在不同层面复用相同的投票机制。
投票器与策略
AccessDecisionManager会轮询一组AccessDecisionVoter,根据各投票器返回的结果,结合自身策略决定放行或拒绝。Spring Security 提供了三种内置策略:
| 策略实现 | 说明 |
|---|---|
AffirmativeBased | 只要有一票赞成即通过(默认) |
ConsensusBased | 多数票决定,平局时可配置通过或拒绝 |
UnanimousBased | 必须全部赞成票才通过 |
组件协作流程如下:
类结构关系如下:
RoleVoter 的角色前缀
RoleVoter会检查ConfigAttribute是否以ROLE_开头。在安全配置中编写hasRole('ADMIN')时,框架自动补充前缀,最终匹配的权限字符串为ROLE_ADMIN。RoleVoter会判断用户持有的GrantedAuthority列表中是否存在该字符串,存在则投赞成,不存在则拒绝;若资源未配置任何角色属性,则投弃权。
安全表达式一览
从 Spring Security 3 开始,可以使用 Spring EL 表达式进行细粒度授权,常用表达式如下:
| 表达式 | 说明 |
|---|---|
denyAll | 拒绝所有访问 |
permitAll | 允许所有访问 |
isAnonymous() | 是否为匿名用户 |
isRememberMe() | 是否通过 Remember-Me 认证 |
isAuthenticated() | 是否已认证(匿名返回 false) |
isFullyAuthenticated() | 是否通过完整登录(非 Remember-Me) |
hasRole('ADMIN') | 拥有角色ROLE_ADMIN |
hasAuthority('ROLE_ADMIN') | 拥有指定的权限(前缀需明确写出) |
access('表达式') | 支持 Spring EL 编写复杂逻辑,如hasRole('ADMIN') or authentication.name == #username |
配置顺序:匹配范围越广的规则应放在越后面,例如denyAll若放在最前,则所有请求都会立即被拒绝,后续规则将不生效。
实战:细粒度资源授权
场景描述
定义接口/api/users/{username},返回欢迎信息。要求:
- 管理员可访问任意用户信息;
- 普通用户仅能访问自己的信息;
- 未认证或权限不符则拒绝访问。
项目结构
src/main/java/com/example/security/ ├── config │ └── SecurityConfig.java ├── controller │ └── UserController.java ├── service │ └── UserAuthorizationService.java └── SecurityApplication.java src/test/java/com/example/security/ └── UserControllerTest.java安全配置
packagecom.example.security.config;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;importorg.springframework.security.config.annotation.web.builders.HttpSecurity;importorg.springframework.security.config.annotation.web.configuration.EnableWebSecurity;importorg.springframework.security.core.userdetails.User;importorg.springframework.security.core.userdetails.UserDetailsService;importorg.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;importorg.springframework.security.crypto.password.PasswordEncoder;importorg.springframework.security.provisioning.InMemoryUserDetailsManager;importorg.springframework.security.web.SecurityFilterChain;@Configuration@EnableWebSecurity@EnableGlobalMethodSecurity(prePostEnabled=true)publicclassSecurityConfig{@BeanpublicSecurityFilterChainfilterChain(HttpSecurityhttp)throwsException{http.authorizeRequests().antMatchers("/api/public").permitAll()// 使用 access 表达式进行复杂授权.antMatchers("/api/users/{username}").access("hasRole('ADMIN') or authentication.name == #username").anyRequest().authenticated().and().httpBasic();// 为方便测试使用 HTTP Basicreturnhttp.build();}@BeanpublicUserDetailsServiceuserDetailsService(){varuser=User.withUsername("zhangsan").password(passwordEncoder().encode("123456")).roles("USER").build();varadmin=User.withUsername("admin").password(passwordEncoder().encode("123456")).roles("ADMIN").build();returnnewInMemoryUserDetailsManager(user,admin);}@BeanpublicPasswordEncoderpasswordEncoder(){returnnewBCryptPasswordEncoder();}}控制器
packagecom.example.security.controller;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.PathVariable;importorg.springframework.web.bind.annotation.RestController;@RestControllerpublicclassUserController{@GetMapping("/api/users/{username}")publicStringgetUserInfo(@PathVariableStringusername){return"Hello, "+username;}}自定义权限服务(Bean 表达式调用)
当权限逻辑复杂时,可将其抽取为 Spring Bean,在表达式中通过@beanName.method(params)调用。
packagecom.example.security.service;importorg.springframework.security.core.Authentication;importorg.springframework.stereotype.Service;@Service("userAuthz")publicclassUserAuthorizationService{publicbooleanisOwnerOrAdmin(Authenticationauthentication,Stringusername){if(authentication==null||!authentication.isAuthenticated()){returnfalse;}// 管理员角色通过 GrantedAuthority 判断booleanisAdmin=authentication.getAuthorities().stream().anyMatch(a->a.getAuthority().equals("ROLE_ADMIN"));// 或者是资源拥有者booleanisOwner=authentication.getName().equals(username);returnisAdmin||isOwner;}}此时安全配置可简化为:
.antMatchers("/api/users/{username}").access("@userAuthz.isOwnerOrAdmin(authentication, #username)")单元测试
packagecom.example.security;importcom.example.security.SecurityApplication;importorg.junit.jupiter.api.Test;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;importorg.springframework.boot.test.context.SpringBootTest;importorg.springframework.security.test.context.support.WithMockUser;importorg.springframework.test.web.servlet.MockMvc;importstaticorg.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;importstaticorg.springframework.test.web.servlet.result.MockMvcResultMatchers.status;@SpringBootTest(classes=SecurityApplication.class)@AutoConfigureMockMvcpublicclassUserControllerTest{@AutowiredprivateMockMvcmockMvc;@TestpublicvoidwhenAdminAccessAnyUser_thenOk()throwsException{mockMvc.perform(get("/api/users/zhangsan").with(org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic("admin","123456"))).andExpect(status().isOk());}@Test@WithMockUser(username="zhangsan",roles="USER")publicvoidwhenOwnerAccessOwnInfo_thenOk()throwsException{mockMvc.perform(get("/api/users/zhangsan")).andExpect(status().isOk());}@Test@WithMockUser(username="zhangsan",roles="USER")publicvoidwhenUserAccessOtherInfo_thenForbidden()throwsException{mockMvc.perform(get("/api/users/lisi")).andExpect(status().isForbidden());}}总结
Spring Security 的授权机制基于投票器模型,AccessDecisionManager调度多个Voter进行决策。通过安全表达式,特别是access,可以编写灵活且与业务逻辑解耦的权限规则。
本文从核心组件到实际编码,完整演示了如何实现“用户只能访问自身资源、管理员可访问全部”的需求,并展示了如何将自定义权限逻辑封装为 Bean 在表达式中调用。