如何为 Spring Boot RESTful Web 服务配置多级身份验证?

Posted

技术标签:

【中文标题】如何为 Spring Boot RESTful Web 服务配置多级身份验证?【英文标题】:How to config multiple level authentication for spring boot RESTful web service? 【发布时间】: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)。

谢谢。

【问题讨论】:

【参考方案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 中,我选择了以下解决方案

    我使用 WebSecurityConfigurerAdapter 来满足 ApiKey 要求

    .authorizeRequests()
        .anyRequest().hasAuthority("API_KEY")
    

    我通过启用它来使用方法级别的安全性

    @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

以上是关于如何为 Spring Boot RESTful Web 服务配置多级身份验证?的主要内容,如果未能解决你的问题,请参考以下文章

如何为 Spring Boot Rest 使用自定义异常?

如何为具有 Spring Security 配置的 Spring Boot API 编写单元测试

如何为 Spring Boot 应用程序配置端口

如何为 Spring Boot 2 添加自定义 MeterRegisty

如何为批量插入配置spring boot和data jpa

如何为我的 Spring Boot webapp 选择 URL?