You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现结合自定义投票器的Spring Security AuthorizationManager?

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.06 03:16:14