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

Spring Security:如何通过注解让端点对访客开放?

实现基于注解的Spring Security匿名端点配置

1. 自定义@NonSecured注解

先创建一个方法级注解,用来标记无需认证的端点:

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NonSecured {
}

2. 实现自定义SecurityMetadataSource

这个类负责收集所有带@NonSecured注解的端点URL,提供给Spring Security做权限校验:

import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;

import javax.annotation.PostConstruct;
import java.util.*;

public class NonSecuredMetadataSource implements FilterInvocationSecurityMetadataSource {

    private final RequestMappingHandlerMapping requestMappingHandlerMapping;
    private final Set<String> nonSecuredUrls = new HashSet<>();
    private final List<ConfigAttribute> anonymousAttributes = Collections.singletonList(new SecurityConfig("ROLE_ANONYMOUS"));

    public NonSecuredMetadataSource(RequestMappingHandlerMapping requestMappingHandlerMapping) {
        this.requestMappingHandlerMapping = requestMappingHandlerMapping;
    }

    @PostConstruct
    public void init() {
        // 遍历所有注册的控制器方法
        requestMappingHandlerMapping.getHandlerMethods().forEach((requestMappingInfo, handlerMethod) -> {
            // 检查方法是否带有@NonSecured注解
            if (handlerMethod.hasMethodAnnotation(NonSecured.class)) {
                // 获取该方法的URL模式
                PatternsRequestCondition patternsCondition = requestMappingInfo.getPatternsCondition();
                if (patternsCondition != null) {
                    nonSecuredUrls.addAll(patternsCondition.getPatterns());
                }
            }
        });
    }

    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        FilterInvocation fi = (FilterInvocation) object;
        String requestUrl = fi.getRequestUrl();
        // 匹配请求URL是否在非安全列表中
        for (String url : nonSecuredUrls) {
            if (new org.springframework.util.AntPathMatcher().match(url, requestUrl)) {
                return anonymousAttributes;
            }
        }
        // 返回null表示使用默认权限校验规则(需要认证)
        return null;
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return FilterInvocation.class.isAssignableFrom(clazz);
    }
}

3. 实现自定义AccessDecisionManager

这个类处理权限决策,判断当前请求是否属于@NonSecured端点,若是则直接放行:

import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;

import java.util.Collection;

public class NonSecuredAccessDecisionManager implements AccessDecisionManager {

    private final AccessDecisionManager defaultAccessDecisionManager;

    public NonSecuredAccessDecisionManager(AccessDecisionManager defaultAccessDecisionManager) {
        this.defaultAccessDecisionManager = defaultAccessDecisionManager;
    }

    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
        // 如果当前请求匹配@NonSecured的URL,直接允许访问
        if (configAttributes != null && configAttributes.stream().anyMatch(attr -> "ROLE_ANONYMOUS".equals(attr.getAttribute()))) {
            return;
        }
        // 否则使用默认权限决策逻辑
        defaultAccessDecisionManager.decide(authentication, object, configAttributes);
    }

    @Override
    public boolean supports(ConfigAttribute attribute) {
        return defaultAccessDecisionManager.supports(attribute);
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return defaultAccessDecisionManager.supports(clazz);
    }
}

4. 配置Spring Security

在Security配置类中,注册自定义的MetadataSource和AccessDecisionManager,同时设置全局默认需要认证:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import java.util.Collections;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final RequestMappingHandlerMapping requestMappingHandlerMapping;

    public SecurityConfig(RequestMappingHandlerMapping requestMappingHandlerMapping) {
        this.requestMappingHandlerMapping = requestMappingHandlerMapping;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                // 全局默认所有请求需要认证
                .anyRequest().authenticated()
                .and()
                // 替换默认的FilterSecurityInterceptor,使用自定义组件
                .addFilterBefore(customFilterSecurityInterceptor(), FilterSecurityInterceptor.class);
        // 按需添加表单登录、JWT等其他配置
    }

    @Bean
    public FilterSecurityInterceptor customFilterSecurityInterceptor() throws Exception {
        FilterSecurityInterceptor interceptor = new FilterSecurityInterceptor();
        interceptor.setSecurityMetadataSource(nonSecuredMetadataSource());
        interceptor.setAccessDecisionManager(nonSecuredAccessDecisionManager());
        interceptor.setAuthenticationManager(authenticationManagerBean());
        return interceptor;
    }

    @Bean
    public FilterInvocationSecurityMetadataSource nonSecuredMetadataSource() {
        return new NonSecuredMetadataSource(requestMappingHandlerMapping);
    }

    @Bean
    public AccessDecisionManager nonSecuredAccessDecisionManager() {
        // 使用默认的UnanimousBased作为兜底决策器
        return new NonSecuredAccessDecisionManager(new org.springframework.security.access.vote.UnanimousBased(
                Collections.singletonList(new org.springframework.security.access.vote.AuthenticatedVoter())
        ));
    }
}

5. 在控制器中使用@NonSecured注解

直接在需要匿名访问的控制器方法上标注注解即可:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class PublicController {

    @NonSecured
    @GetMapping("/public/hello")
    public String publicHello() {
        return "Hello, anonymous user!";
    }

    @GetMapping("/private/hello")
    public String privateHello() {
        return "Hello, authenticated user!";
    }
}

说明

  • 启动应用后,/public/hello可直接匿名访问,/private/hello需认证后才能访问
  • 所有带@NonSecured注解的方法会在应用启动时被扫描,自动加入允许匿名访问的列表
  • 支持带路径变量的URL(如/public/user/{id}),AntPathMatcher会自动处理匹配逻辑

内容的提问来源于stack exchange,提问作者Vaks

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 15:42:26