spring 安全如何拒绝访问而不是重定向到登录页面

How can spring security deny access instead of redirecting to the login page

当使用不同于允许的 IP 地址访问特定 URL 时,spring 安全会自动将请求转发到登录页面,而不是拒绝请求。

这是我的 Spring 安全表达式

http.authorizeRequests()
            .antMatchers("/" + WELCOME_PAGE,
                    "/" + FOOD_SELECTION_PAGE + "/**",
                    "/" + CHECKOUT_URL,
                    "/" + VERIFY_BADGE_URL,
                    "/" + VERIFY_NAME_URL).hasIpAddress("x.x.x.x")
            .antMatchers("/" + ADMIN_PAGE,
                    "/" + NEW_FOOD_PAGE,
                    "/" + HIDE_FOOD_URL,
                    "/" + SHOW_FOOD_URL,
                    "/" + DELETE_FOOD_URL,
                    "/" + EDIT_FOOD_URL,
                    "/" + REFRESH_EMPS_URL).hasAnyAuthority("ROLES_USER")
            .antMatchers("/**").permitAll()
            .and().formLogin().loginPage("/" + LOGIN_PAGE).usernameParameter("username").passwordParameter("pin").defaultSuccessUrl("/" + ADMIN_PAGE).permitAll().failureUrl("/" + LOGIN_PAGE + "?badlogin");

如果我登录,它会将我带回原始请求 URL 并且会抛出 403 拒绝。我不希望它转发到登录页面,我只希望它立即拒绝。

我什至尝试了 denyAll() 而不是 hasIpAddress(),它做了同样的事情。

我在 https://docs.spring.io/spring-security/site/docs/current/reference/html5/ 各种 google 搜索中浏览了 spring 文档,但我找不到任何专门讨论这个的内容。

我希望用户仍然能够转到第二个 antMatchers 中的任何页面,并在他们尚未通过身份验证时自动转发到登录页面。

所以我写了一篇评论说这很难google,这是真的,而你想要的是自定义AuthenticationFailureHandler,这是错误的。

其实你要的是自定义AuthenticationEntryPoint。造成您不喜欢的行为的 class 是 ExceptionTranslatorFilter。从它的 javadoc:

If an AuthenticationException is detected, the filter will launch the authenticationEntryPoint. [...] If an AccessDeniedException is detected, the filter will determine whether or not the user is an anonymous user. If they are an anonymous user, the authenticationEntryPoint will be launched. If they are not an anonymous user, the filter will delegate to the AccessDeniedHandler.

事实证明,当用户没有所需的角色以及请求没有允许的远程地址之一时,都会抛出 AccessDeniedException。所以 ExceptionTranslatorFilter 在这两种情况下都调用它的 authenticationEntryPoint 。当您调用 HttpSecurity.formLogin() 时设置 authenticationEntryPoint;具体来说,它被设置为 LoginUrlAuthenticationEntryPoint.

Spring 安全默认配置的这一特殊位是您想要覆盖的内容——您希望 ExceptionTranslationFilter 使用一个 AuthenticationEntryPoint 来根据它的请求做不同的事情处理。这就是 Spring 安全提供的 DelegatingAuthenticationEntryPoint 的用途。

自定义身份验证入口点的方法是在您的 HttpSecurity 上调用 exceptionHandling()。因此,我们得出您在以下 Spring 启动测试中看到的配置:

import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

// many non-static imports

@SpringBootTest
class SecurityTest {
    @Autowired WebApplicationContext wac;
    MockMvc mvc;

    @RestController
    static class BigController {
        @GetMapping("/needs-ip")
        public String needsIp() {
            return "Needs IP";
        }

        @GetMapping("/needs-no-ip")
        public String needsNoIp() {
            return "Needs no IP";
        }
    }

    @TestConfiguration
    @EnableWebSecurity
    static class ConfigurationForTest extends WebSecurityConfigurerAdapter {
        @Bean BigController controller() { return new BigController(); }
        @Override
        public void configure(HttpSecurity http) throws Exception {
            LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> map = new LinkedHashMap<>();
            // Http403ForbiddenEntryPoint basically just says "don't bother with authentication, return 403 instead"
            map.put(new AntPathRequestMatcher("/needs-ip"), new Http403ForbiddenEntryPoint());
            // normally, Spring Boot adds this authentication entry point on its own, but we've taken
            // over the configuration so we do it ourselves
            map.put(new AntPathRequestMatcher("/needs-no-ip"), new LoginUrlAuthenticationEntryPoint("/login"));

            http.authorizeRequests()
                .antMatchers("/needs-ip").hasIpAddress("192.168.12.13")
                .antMatchers("/needs-no-ip").hasRole("USER")
                .anyRequest().permitAll() // more readable than "antMatchers("/**")"
                .and().formLogin() // with the default url, which is "/login"
                // the line that follows is the interesting one
                .and().exceptionHandling().authenticationEntryPoint(new DelegatingAuthenticationEntryPoint(map));
            ;
        }
    }

    @BeforeEach
    public void configureMockMvc() {
        this.mvc = MockMvcBuilders.webAppContextSetup(wac).apply(springSecurity()).build();
    }

    @Test
    void needsIpCorrectIp() throws Exception {
        mvc.perform(get("/needs-ip").with(req -> {req.setRemoteAddr("192.168.12.13"); return req;}))
                .andExpect(status().isOk())
                .andExpect(content().string("Needs IP"));
    }

    @Test
    void needsIpWrongIpAnonymousUser() throws Exception {
        mvc.perform(get("/needs-ip"))
            .andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser(roles = "USER")
    void needsIpWrongIpLoggedInUser() throws Exception {
        mvc.perform(get("/needs-ip"))
                .andExpect(status().isForbidden());
    }

    @Test
    void needsNoIpAnonymousUser() throws Exception {
        mvc.perform(get("/needs-no-ip"))
                .andExpect(status().isFound())
                // apparently, the default hostname in Spring MockMvc tests is localhost
                // you can change it, but why bother?
                .andExpect(header().string("Location", "http://localhost/login"));
    }

    @Test
    @WithMockUser(roles = "USER")
    void needsNoIpAuthorizedUser() throws Exception {
        mvc.perform(get("/needs-no-ip"))
                .andExpect(status().isOk())
                .andExpect(content().string("Needs no IP"));
    }

    @Test
    @WithMockUser(roles = "NOTUSER")
    void needsNoIpUnauthorizedUser() throws Exception {
        mvc.perform(get("/needs-no-ip"))
                .andExpect(status().isForbidden());
    }
}

如果您从 Spring Initializr 下载项目,请添加 spring-security-test、spring-starter-web 和 spring-starter-security 作为依赖项,和 运行 mvn verify,测试应该通过。我已经包括了一个 @RestController 所以很明显快乐之路也按预期工作。当然,这个配置没有你的复杂,而且我不倾向于为 Stack Overflow 的答案编写可维护的代码。 (那里应该有更多的常量和更少的字符串)。但是您可能可以修改配置以使其适合您。

顺便说一下,我不确定您的配置是否复杂足够 -- 就个人而言,如果没有 authentication/authorization,我什至希望来自白名单 IP 地址的请求失败存在 -- 但我不知道您的安全要求,所以我无法真正判断。