如何实现结合自定义投票器的Spring Security AuthorizationManager?
核心需求实现思路
要实现「注解标记接口,未登录访问触发登录流程,无权限时返回404」的效果,需通过以下核心步骤:
- 自定义注解标记需要登录的接口
- 实现自定义
AuthorizationVoter,识别注解对应的配置属性并判断用户认证状态 - 组合自定义投票器与Spring Security默认投票器,构建
AuthorizationManager,通过决策策略区分未认证/权限不足场景,抛出对应异常 - 配置异常处理器,分别处理两类异常:未认证触发登录跳转,权限不足返回404
具体实现步骤
1. 自定义登录要求注解
定义注解标记需要登录验证的接口/方法:
@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface RequireLogin { }
2. 实现自定义投票器
实现AuthorizationVoter接口,处理@RequireLogin标记的接口,判断用户是否已认证:
public class LoginRequiredVoter implements AuthorizationVoter<MethodInvocation> { @Override public boolean supports(ConfigAttribute attribute) { return "REQUIRE_LOGIN".equals(attribute.getAttribute()); } @Override public boolean supports(Class<?> clazz) { return MethodInvocation.class.isAssignableFrom(clazz); } @Override public int vote(Authentication authentication, MethodInvocation object, Collection<ConfigAttribute> attributes) { // 检查当前请求是否需要登录验证 boolean requireLogin = attributes.stream().anyMatch(this::supports); if (!requireLogin) { return ACCESS_ABSTAIN; // 不处理,交给其他投票器 } // 用户未认证,投反对票 if (authentication == null || !authentication.isAuthenticated() || authentication instanceof AnonymousAuthenticationToken) { return ACCESS_DENIED; } // 用户已认证,投赞成票 return ACCESS_GRANTED; } }
3. 组合投票器构建AuthorizationManager
使用DelegatingAuthorizationManager组合自定义投票器与默认权限投票器,并配置决策策略区分异常场景:
@Bean public AuthorizationManager<MethodInvocation> methodAuthorizationManager() { // 自定义登录要求投票器 LoginRequiredVoter loginRequiredVoter = new LoginRequiredVoter(); // Spring Security默认权限投票器,处理@PreAuthorize等表达式 AuthorityAuthorizationManager<MethodInvocation> authorityManager = AuthorityAuthorizationManager.methodDefaults(); // 组合多个投票器 DelegatingAuthorizationManager<MethodInvocation> delegatingManager = DelegatingAuthorizationManager.builder() .addManager(VoterAuthorizationManager.create(loginRequiredVoter)) .addManager(authorityManager) .build(); // 自定义决策策略:区分未认证与权限不足 delegatingManager.setDecisionStrategy((authenticated, decisions, configAttributes) -> { int denyCount = (int) decisions.stream().filter(decision -> !decision.isGranted()).count(); int grantCount = (int) decisions.stream().filter(AuthorizationDecision::isGranted).count(); if (denyCount > 0) { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth == null || !auth.isAuthenticated() || auth instanceof AnonymousAuthenticationToken) { throw new AuthorizationException("未登录,请先登录"); } else { throw new AccessDeniedException("无访问权限"); } } if (grantCount > 0) { return new AuthorizationDecision(true); } // 全部弃权则拒绝访问 throw new AccessDeniedException("无访问权限"); }); return delegatingManager; }
4. 配置SecurityFilterChain与异常处理器
配置安全过滤链,绑定自定义AuthorizationManager,并处理两类异常:
@Configuration @EnableMethodSecurity // 开启方法级安全验证 public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .anyRequest().authenticated() ) .formLogin(form -> form .loginPage("/login") // 指定自定义登录页路径 .permitAll() ) .exceptionHandling(ex -> ex // 处理AccessDeniedException,返回404 .accessDeniedHandler((request, response, ex) -> { response.sendError(HttpServletResponse.SC_NOT_FOUND, "Not Found"); }) // 处理AuthorizationException,触发登录流程 .authenticationEntryPoint((request, response, ex) -> { response.sendRedirect("/login"); }) ) .methodSecurity(method -> method .authorizationManager(methodAuthorizationManager()) ); return http.build(); } }
5. 在接口中使用自定义注解
标记需要登录验证的接口方法:
@RestController public class DemoController { @RequireLogin @GetMapping("/user-only") public String userOnlyResource() { return "仅登录用户可访问"; } @PreAuthorize("hasRole('ADMIN')") @GetMapping("/admin-only") public String adminOnlyResource() { return "仅管理员可访问"; } }
内容的提问来源于stack exchange,提问作者Slevin
相关产品推荐
相关产品推荐

