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
相关产品推荐
相关产品推荐

