Spring 5.7+如何基于IP地址与凭证限制资源访问
问题根因
你的配置有5个核心错误,导致自定义IP校验逻辑完全不生效:
- 核心规则配置错误:
security.authorizeRequests().anyRequest().permitAll()直接放行所有请求,请求根本不会进入认证流程,你写的CustomIpAuthenticationProvider完全没有触发机会。 - 配置类语法错误:
SecurityConfig里filterChain方法外多写了一个闭合大括号,导致userDetailsService的Bean定义不在配置类范围内,既不会被Spring注册,也和你自定义的认证逻辑完全脱节。 - 认证逻辑冲突:你手动禁用了
formLogin,但测试用例却在请求formLogin专属的/login路径,这个路径在禁用formLogin后根本不存在;同时你自定义Provider里硬编码了账号密码校验,和你注册的内存用户完全无关,等于内存用户的配置是无效代码。 - 不必要的自动配置排除:手动排除
SecurityAutoConfiguration会打乱Spring Security默认的过滤器链注册逻辑,反而会导致自定义配置加载异常。 - IP校验逻辑有遗漏:本地访问默认会走IPv6地址
0:0:0:0:0:0:0:1,不在你写的白名单里,本地测试时会误拦截;如果服务前有反向代理,直接取remoteAddress只能拿到代理IP,拿不到真实用户IP。
正确实现方案
1. 修正主启动类
去掉不必要的Security自动配置排除:
@SpringBootApplication public class MainApp { public static void main(String[] args) { SpringApplication.run(MainApp.class, args); } }
2. 修正安全配置类
修复语法错误,调整权限规则为所有请求需认证,注册自定义认证提供者,开启HTTP Basic作为凭证传输方式(也可根据需求替换为其他认证方式,默认登录页已禁用):
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public CustomIpAuthenticationProvider customIpAuthenticationProvider( UserDetailsService userDetailsService, PasswordEncoder passwordEncoder ) { return new CustomIpAuthenticationProvider(userDetailsService, passwordEncoder); } @Bean public SecurityFilterChain filterChain(HttpSecurity security) throws Exception { security .csrf().disable() // 禁用默认登录表单 .formLogin().disable() // 开启HTTP Basic作为凭证校验方式,可根据业务替换 .httpBasic(Customizer.withDefaults()) .authorizeHttpRequests(auth -> auth // 所有请求必须认证通过才能访问 .anyRequest().authenticated() ) .authenticationProvider(customIpAuthenticationProvider( userDetailsService(), PasswordEncoderFactories.createDelegatingPasswordEncoder() )); return security.build(); } @Bean public InMemoryUserDetailsManager userDetailsService() { PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); UserDetails user = User.withUsername("test_user") .password(encoder.encode("password")) .roles("USER") .build(); return new InMemoryUserDetailsManager(user); } }
3. 修正自定义认证提供者
去掉硬编码的账号密码校验,注入Spring管理的UserDetailsService和密码编码器做凭证校验,补充IPv6本地地址到白名单方便本地测试:
public class CustomIpAuthenticationProvider implements AuthenticationProvider { private final Set<String> whitelist = new HashSet<>(); private final UserDetailsService userDetailsService; private final PasswordEncoder passwordEncoder; public CustomIpAuthenticationProvider(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) { this.userDetailsService = userDetailsService; this.passwordEncoder = passwordEncoder; // 初始化IP白名单 whitelist.add("11.11.11.11"); whitelist.add("127.0.0.1"); whitelist.add("0:0:0:0:0:0:0:1"); // 兼容IPv6本地地址 } @Override public Authentication authenticate(Authentication auth){ WebAuthenticationDetails details = (WebAuthenticationDetails) auth.getDetails(); String userIp = details.getRemoteAddress(); System.out.println("当前请求来源IP:" + userIp); // 先校验IP白名单 if (!whitelist.contains(userIp)) { throw new BadCredentialsException("IP地址不在白名单范围内"); } // 校验账号密码 String username = auth.getName(); String presentedPassword = auth.getCredentials().toString(); UserDetails user = userDetailsService.loadUserByUsername(username); if (!passwordEncoder.matches(presentedPassword, user.getPassword())) { throw new BadCredentialsException("用户名或密码错误"); } return new UsernamePasswordAuthenticationToken(user, presentedPassword, user.getAuthorities()); } @Override public boolean supports(Class<?> authentication) { return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication); } }
4. 提供测试接口与修正测试用例
先写一个测试用的POST接口用于验证效果:
@RestController public class ResourceController { @PostMapping("/api/resource") public String getProtectedResource() { return "受保护资源访问成功"; } }
用MockMvc做单元测试,可手动模拟请求来源IP,避免RestAssured测试时无法修改来源IP的问题:
@SpringBootTest @AutoConfigureMockMvc public class IpAuthenticationTest { @Autowired private MockMvc mockMvc; @Test public void givenUserWithWrongIPReturnForbidden() throws Exception { mockMvc.perform( MockMvcRequestBuilders.post("/api/resource") .with(request -> { // 模拟非白名单IP request.setRemoteAddr("12.12.12.12"); return request; }) .with(SecurityMockMvcRequestPostProcessors.httpBasic("test_user", "password")) ).andExpect(MockMvcResultMatchers.status().isForbidden()); } @Test public void givenUserWithCorrectIPAndCredentialReturnSuccess() throws Exception { mockMvc.perform( MockMvcRequestBuilders.post("/api/resource") .with(request -> { // 模拟白名单IP request.setRemoteAddr("11.11.11.11"); return request; }) .with(SecurityMockMvcRequestPostProcessors.httpBasic("test_user", "password")) ).andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.content().string("受保护资源访问成功")); } }
注意事项
- 如果服务前部署了Nginx等反向代理,需要在application配置中添加
server.forward-headers-strategy=native,让Spring能从X-Forwarded-For请求头获取真实客户端IP,否则白名单校验拿到的永远是代理服务器的内网IP。 - IP白名单校验如果要提前拦截减少资源消耗,可以自定义OncePerRequestFilter在过滤器链最前面做校验,不用等走到认证Provider层再判断。
- 生产环境不要硬编码IP白名单,建议放到配置文件或者配置中心动态维护。
内容的提问来源于stack exchange,提问作者Fitz
相关产品推荐
相关产品推荐

