Java框架快速入门X44: Spring Security+OAuth2之授权机制与安全表达式实战 📅 2026/8/5 18:36:39 纲要认证与授权的区别AccessDecisionManager核心接口及其decide方法AccessDecisionVoter投票器接口及三种决策策略RoleVoter角色投票器与ROLE_前缀机制常用安全表达式速览hasRole、hasAuthority、permitAll、denyAll、access等基于access表达式的细粒度权限控制实战限制“用户只能访问自身资源管理员可访问全部”自定义权限逻辑封装为 Spring Bean 并在表达式内调用项目结构、安全配置、控制器及单元测试完整代码认证与授权认证解决“你是谁”授权解决“你能做什么”。例如小区门禁认证通过后你仍不能进入别人家只能进入自己家这便是授权。Spring Security 的授权模型基于投票决策由AccessDecisionManager统一协调。授权核心组件AccessDecisionManager该接口负责根据安全对象和配置属性做出授权决定核心方法如下voiddecide(Authenticationauthentication,Objectobject,CollectionConfigAttributeconfigAttributes)throwsAccessDeniedException,InsufficientAuthenticationException;参数中的object是一个安全对象Spring Security 并没有限制具体类型可以是FilterInvocationWeb 请求或MethodInvocation方法调用这使得框架可以在不同层面复用相同的投票机制。投票器与策略AccessDecisionManager会轮询一组AccessDecisionVoter根据各投票器返回的结果结合自身策略决定放行或拒绝。Spring Security 提供了三种内置策略策略实现说明AffirmativeBased只要有一票赞成即通过默认ConsensusBased多数票决定平局时可配置通过或拒绝UnanimousBased必须全部赞成票才通过组件协作流程如下Voter (AuthenticatedVoter)Voter (RoleVoter)AccessDecisionManagerFilterSecurityInterceptorVoter (AuthenticatedVoter)Voter (RoleVoter)AccessDecisionManagerFilterSecurityInterceptorloop[遍历所有 Voter]alt[策略判定通过][拒绝]decide(auth, object, config)vote(auth, object, config)ACCESS_GRANTED / DENIED / ABSTAINvote(...)...放行AccessDeniedException类结构关系如下AccessDecisionManagerdecide(Authentication, Object, CollectionConfigAttribute) : voidAffirmativeBaseddecide(...) : voidConsensusBaseddecide(...) : voidUnanimousBaseddecide(...) : voidAccessDecisionVotervote(Authentication, Object, CollectionConfigAttribute) : intRoleVotervote(...) : intAuthenticatedVotervote(...) : intRoleVoter 的角色前缀RoleVoter会检查ConfigAttribute是否以ROLE_开头。在安全配置中编写hasRole(ADMIN)时框架自动补充前缀最终匹配的权限字符串为ROLE_ADMIN。RoleVoter会判断用户持有的GrantedAuthority列表中是否存在该字符串存在则投赞成不存在则拒绝若资源未配置任何角色属性则投弃权。安全表达式一览从 Spring Security 3 开始可以使用 Spring EL 表达式进行细粒度授权常用表达式如下表达式说明denyAll拒绝所有访问permitAll允许所有访问isAnonymous()是否为匿名用户isRememberMe()是否通过 Remember-Me 认证isAuthenticated()是否已认证匿名返回 falseisFullyAuthenticated()是否通过完整登录非 Remember-MehasRole(ADMIN)拥有角色ROLE_ADMINhasAuthority(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;ConfigurationEnableWebSecurityEnableGlobalMethodSecurity(prePostEnabledtrue)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(){varuserUser.withUsername(zhangsan).password(passwordEncoder().encode(123456)).roles(USER).build();varadminUser.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){returnHello, 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(authenticationnull||!authentication.isAuthenticated()){returnfalse;}// 管理员角色通过 GrantedAuthority 判断booleanisAdminauthentication.getAuthorities().stream().anyMatch(a-a.getAuthority().equals(ROLE_ADMIN));// 或者是资源拥有者booleanisOwnerauthentication.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(classesSecurityApplication.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());}TestWithMockUser(usernamezhangsan,rolesUSER)publicvoidwhenOwnerAccessOwnInfo_thenOk()throwsException{mockMvc.perform(get(/api/users/zhangsan)).andExpect(status().isOk());}TestWithMockUser(usernamezhangsan,rolesUSER)publicvoidwhenUserAccessOtherInfo_thenForbidden()throwsException{mockMvc.perform(get(/api/users/lisi)).andExpect(status().isForbidden());}}总结Spring Security 的授权机制基于投票器模型AccessDecisionManager调度多个Voter进行决策。通过安全表达式特别是access可以编写灵活且与业务逻辑解耦的权限规则。本文从核心组件到实际编码完整演示了如何实现“用户只能访问自身资源、管理员可访问全部”的需求并展示了如何将自定义权限逻辑封装为 Bean 在表达式中调用。