【问题标题】:How to config multiple level authentication for spring boot RESTful web service?如何为 Spring Boot RESTful Web 服务配置多级身份验证?
【发布时间】:2019-06-05 15:38:13
【问题描述】:

我们正在使用 Spring Boot 构建一个 RESTful Web 服务。我们希望有 2 级身份验证来保护端点。

首先,对于每个请求,我们要检查请求头中是否有指定的apiKey,如果没有,我们将拒绝该请求。如果请求有 apiKey,我们将为 一些请求 使用用户名/密码登录进行下一个身份验证。有public endpoint只需要apiKey认证,private endpoint需要先apiKey认证,然后需要用户名/密码认证才能访问。

对于apiKey认证,我复制了代码here,我也可以找到很多关于用户名/密码认证的例子。

我的问题是:如何在 WebSecurityConfigurerAdapter 中进行 Java 配置以将它们组合在一起。

现在我为这 2 个身份验证过滤器定义了 2 个扩展 WebSecurityConfigurerAdapter 的配置类,但请求只会通过其中一个,具体取决于我将哪一个设置为 @Order(1)。

谢谢。

【问题讨论】:

    标签: java spring-boot authentication spring-security


    【解决方案1】:

    这个完整的答案是由一个工作的 Spring Boot 应用程序支持的,并带有单元测试来确认它。

    如果你觉得这个答案有帮助,请投票。

    简短的回答是您的安全配置可能如下所示

        http
            .sessionManagement()
                .disable()
            //application security
            .authorizeRequests()
                .anyRequest().hasAuthority("API_KEY")
                .and()
            .addFilterBefore(new ApiKeyFilter(), HeaderWriterFilter.class)
            .addFilterAfter(new UserCredentialsFilter(), ApiKeyFilter.class)
            .csrf().ignoringAntMatchers(
                "/api-key-only",
                "/dual-auth"
        )
            ;
            // @formatter:on
        }
    
    }
    

    让我告诉你发生了什么。我鼓励您查看我的示例,特别是涵盖您的许多场景的 unit tests

    我们有两个安全级别 1. 每个 API 都必须由 ApiKey 保护 2. 只有部分 API 必须由 UserCredentials 保护

    在我的example project 中,我选择了以下解决方案

    1. 我使用 WebSecurityConfigurerAdapter 来满足 ApiKey 要求

      .authorizeRequests()
          .anyRequest().hasAuthority("API_KEY")
      
    2. 我通过启用它来使用方法级别的安全性

      @EnableGlobalMethodSecurity(prePostEnabled = true)

    然后在我的控制器中要求它

        @PreAuthorize("hasAuthority('USER_CREDENTIALS')")
        public String twoLayersOfAuth() {
            //only logic here
        }
    

    ApiKey 过滤器超级简单

    public class ApiKeyFilter extends OncePerRequestFilter {
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
    
            final String authorization = request.getHeader("Authorization");
            final String prefix = "ApiKey ";
            if (hasText(authorization) && authorization.startsWith(prefix)) {
                String key = authorization.substring(prefix.length());
                if ("this-is-a-valid-key".equals(key)) {
                    RestAuthentication<SimpleGrantedAuthority> authentication = new RestAuthentication<>(
                        key,
                        Collections.singletonList(new SimpleGrantedAuthority("API_KEY"))
                    );
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
            filterChain.doFilter(request, response);
    
        }
    }
    

    第二层身份验证甚至很简单(它依赖于第一层来执行)

    public class UserCredentialsFilter extends OncePerRequestFilter {
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
            final String userCredentials = request.getHeader("X-User-Credentials");
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            if ("valid-user".equals(userCredentials) && authentication instanceof RestAuthentication) {
                RestAuthentication<SimpleGrantedAuthority> restAuthentication =
                    (RestAuthentication<SimpleGrantedAuthority>)authentication;
                restAuthentication.addAuthority(new SimpleGrantedAuthority("USER_CREDENTIALS"));
            }
            filterChain.doFilter(request, response);
    
        }
    }
    

    请注意:每个过滤器如何不关心在没有认证或认证不充分时会发生什么。这一切都为你处理好了。您的过滤器只需验证正确的数据;

    Spring、Spring Boot 和 Spring Security 有一些出色的测试设施。

    我可以调用具有两种安全级别的 api-only 端点

        mvc.perform(
            post("/api-key-only")
                .header("Authorization", "ApiKey this-is-a-valid-key")
                .header("X-User-Credentials", "valid-user")
        )
            .andExpect(status().isOk())
            .andExpect(authenticated()
                .withAuthorities(
                    asList(
                        new SimpleGrantedAuthority("API_KEY"),
                        new SimpleGrantedAuthority("USER_CREDENTIALS")
                    )
                )
            )
            .andExpect(content().string("API KEY ONLY"))
        ;
    

    或者我可以通过第一级安全并被第二级拒绝

        mvc.perform(
            post("/dual-auth")
                .header("Authorization", "ApiKey this-is-a-valid-key")
        )
            .andExpect(status().is4xxClientError())
            .andExpect(authenticated()
                .withAuthorities(
                    asList(
                        new SimpleGrantedAuthority("API_KEY")
                    )
                )
            )
        ;
    

    当然,我们总是有幸福的道路

        mvc.perform(
            post("/dual-auth")
                .header("Authorization", "ApiKey this-is-a-valid-key")
                .header("X-User-Credentials", "valid-user")
        )
            .andExpect(status().isOk())
            .andExpect(content().string("DUAL AUTH"))
            .andExpect(authenticated()
                .withAuthorities(
                    asList(
                        new SimpleGrantedAuthority("API_KEY"),
                        new SimpleGrantedAuthority("USER_CREDENTIALS")
                    )
                )
            )
        ;
    

    【讨论】:

    • 示例代码很棒,可以满足我的需求。我是 Spring Security 的新手,最初我想也许我可以为端点使用不同的 antMatchers() 为一种身份验证方法定义 2 个安全配置。但从我的测试来看,它不适用于重叠的端点。我是不是做错了什么,或者我确实不能以这种方式解决我的问题?
    • 您绝对可以通过多种不同的方式解决问题。包括为不同的路径使用匹配器。您的过滤器必须在其中包含匹配器,以便它们知道何时触发。你可以使用RequestMatcher 接口来做各种聪明的事情。唯一需要注意的是,如果它太复杂而无法弄清楚,那么维护起来就太难了
    • 我在使用您的示例代码时发现了一些问题,http GET 方法工作正常,但是当我尝试在 POST 方法上使用时,即使我没有为该端点设置 @PreAuthorize,我包括apiKey 在标题中,我仍然得到 {"timestamp":"2019-01-14T18:09:15.148+0000","status":403,"error":"Forbidden","message":"Forbidden","路径":"/users/sign-up"}
    • 更新:我发现我需要在 httpSecury 配置中禁用 csrf 才能使 POST 方法工作。像“httpSecurity.csrf().disable()”。
    • 感谢更新,我将示例更改为 POST 并禁用 CSRF
    猜你喜欢
    • 2020-11-13
    • 2020-01-04
    • 1970-01-01
    • 2012-06-19
    • 2016-02-20
    • 1970-01-01
    • 2018-01-29
    • 2013-06-11
    • 1970-01-01
    相关资源
    最近更新 更多