【问题标题】:Securing Spring Boot API with API key and secret使用 API 密钥和秘密保护 Spring Boot API
【发布时间】:2018-07-04 22:16:59
【问题描述】:

我想保护 Spring Boot API,以便只有具有有效 API 密钥和机密的客户端才能访问它。但是,程序内部没有身份验证(使用用户名和密码的标准登录),因为所有数据都是匿名的。我想要实现的只是所有 API 请求都只能用于特定的第三方前端。

我发现了很多关于如何通过用户身份验证保护 Spring Boot API 的文章。但我不需要用户身份验证。我正在考虑的只是为我的客户提供 API 密钥和秘密,以便他可以访问端点。

您能否建议我如何实现这一目标?谢谢!

【问题讨论】:

  • 唯一的区别是你称它为 API 密钥而不是用户名还是有别的?
  • 以下答案是否解决了您的问题?你是如何管理你的用户和每个用户的 api 密钥的?

标签: java spring api spring-boot spring-security


【解决方案1】:

@MarkOfHall 的答案是正确的,我只想添加更多细节。获得代码后,您需要将属性值添加到 application.properties 文件中,如下所示:

yourapp.http.auth-token-header-name=X-API-KEY
yourapp.http.auth-token=abc123

在 Postman 中设置认证值如下:

您可以使用 Postman,但如果您使用 cURL,请求将与下面提供的类似:

$ curl -H "X-API-KEY: abc123" "http://localhost:8080/api/v1/property/1"

除非提供正确的键和值,否则应用程序将无法运行。

【讨论】:

    【解决方案2】:

    我意识到我在这个游戏上有点晚了,但我还设法让 API 密钥与 Spring Boot 一起使用,并与用户名/密码身份验证相结合。我对使用 AbstractPreAuthenticatedProcessingFilter 的想法并不疯狂,因为在阅读 JavaDoc 时,这似乎是对那个特定类的滥用。

    我最终创建了一个新的 ApiKeyAuthenticationToken 类以及一个非常简单的原始 servlet 过滤器来完成此操作:

    import java.util.Collection;
    
    import org.springframework.security.authentication.AbstractAuthenticationToken;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.Transient;
    
    @Transient
    public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken {
    
        private String apiKey;
        
        public ApiKeyAuthenticationToken(String apiKey, Collection<? extends GrantedAuthority> authorities) {
            super(authorities);
            this.apiKey = apiKey;
            setAuthenticated(true);
        }
    
        @Override
        public Object getCredentials() {
            return null;
        }
    
        @Override
        public Object getPrincipal() {
            return apiKey;
        }
    }
    

    过滤器

    import javax.servlet.Filter;
    import javax.servlet.FilterChain;
    import javax.servlet.ServletException;
    import javax.servlet.ServletRequest;
    import javax.servlet.ServletResponse;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.springframework.security.core.authority.AuthorityUtils;
    import org.springframework.security.core.context.SecurityContextHolder;
    
    public class ApiKeyAuthenticationFilter implements Filter {
    
        static final private String AUTH_METHOD = "api-key";
        
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
                throws IOException, ServletException
        {
            if(request instanceof HttpServletRequest && response instanceof HttpServletResponse) {
                String apiKey = getApiKey((HttpServletRequest) request);
                if(apiKey != null) {
                    if(apiKey.equals("my-valid-api-key")) {
                        ApiKeyAuthenticationToken apiToken = new ApiKeyAuthenticationToken(apiKey, AuthorityUtils.NO_AUTHORITIES);
                        SecurityContextHolder.getContext().setAuthentication(apiToken);
                    } else {
                        HttpServletResponse httpResponse = (HttpServletResponse) response;
                        httpResponse.setStatus(401);
                        httpResponse.getWriter().write("Invalid API Key");
                        return;
                    }
                }
            }
            
            chain.doFilter(request, response);
            
        }
    
        private String getApiKey(HttpServletRequest httpRequest) {
            String apiKey = null;
            
            String authHeader = httpRequest.getHeader("Authorization");
            if(authHeader != null) {
                authHeader = authHeader.trim();
                if(authHeader.toLowerCase().startsWith(AUTH_METHOD + " ")) {
                    apiKey = authHeader.substring(AUTH_METHOD.length()).trim();
                }
            }
            
            return apiKey;
        }
    }
    

    此时剩下的就是将过滤器注入到链中的适当位置。就我而言,我希望在任何用户名/密码身份验证之前评估 API 密钥身份验证,以便它可以在应用程序尝试重定向到登录页面之前对请求进行身份验证:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf()
                .disable()
            .addFilterBefore(new ApiKeyAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
            .authorizeRequests()
                .anyRequest()
                    .fullyAuthenticated()
                    .and()
            .formLogin();
    }
    

    我要说您应该注意的另一件事是,您的 API 密钥身份验证请求不会在您的服务器上创建和放弃一堆 HttpSessions。

    【讨论】:

    • 这真的对我有用。但是,是否建议在生产中使用?
    • API 密钥通常不如 OAuth 安全。但它们更简单,这是吸引力的一部分。天气与否这种权衡是否值得取决于您的需求以及应用程序的部署方式。我的特定应用程序是一个内部应用程序,不接受来自外部世界的连接,因此在我的情况下进行权衡是值得的。但是,例如,我不会将 API 密钥作为唯一的安全机制部署到移动应用程序,因为该应用程序的任何用户都可以获得该 API 密钥。
    • @mattforsythe 你是对的,但 api 密钥通常用于私人环境。理论上,要在移动应用程序中使用它,您需要在后端创建某种代理。
    • @WannyMiarelli,正确。正如我在评论中提到的,我的应用程序是一个私有的内部应用程序,不接受来自外部互联网的连接。这就是我认为理想的原因。我想我们说的是同一件事,对吧?
    【解决方案3】:

    创建一个过滤器来抓取您用于身份验证的任何标头。

    import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
    
    public class APIKeyAuthFilter extends AbstractPreAuthenticatedProcessingFilter {
    
        private String principalRequestHeader;
    
        public APIKeyAuthFilter(String principalRequestHeader) {
            this.principalRequestHeader = principalRequestHeader;
        }
    
        @Override
        protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
            return request.getHeader(principalRequestHeader);
        }
    
        @Override
        protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
            return "N/A";
        }
    
    }
    

    在您的网络安全配置中配置过滤器。

    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.core.annotation.Order;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.authentication.BadCredentialsException;
    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.config.http.SessionCreationPolicy;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.AuthenticationException;
    
    @Configuration
    @EnableWebSecurity
    @Order(1)
    public class APISecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Value("${yourapp.http.auth-token-header-name}")
        private String principalRequestHeader;
    
        @Value("${yourapp.http.auth-token}")
        private String principalRequestValue;
    
        @Override
        protected void configure(HttpSecurity httpSecurity) throws Exception {
            APIKeyAuthFilter filter = new APIKeyAuthFilter(principalRequestHeader);
            filter.setAuthenticationManager(new AuthenticationManager() {
    
                @Override
                public Authentication authenticate(Authentication authentication) throws AuthenticationException {
                    String principal = (String) authentication.getPrincipal();
                    if (!principalRequestValue.equals(principal))
                    {
                        throw new BadCredentialsException("The API key was not found or not the expected value.");
                    }
                    authentication.setAuthenticated(true);
                    return authentication;
                }
            });
            httpSecurity.
                antMatcher("/api/**").
                csrf().disable().
                sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).
                and().addFilter(filter).authorizeRequests().anyRequest().authenticated();
        }
    
    }
    

    【讨论】:

    • 这很有帮助。我有一个应用程序需要同时支持用户名/密码和基于 ApiKey 的身份验证。我有用户名/密码工作,阅读你的帖子后,我可以让 ApiKey 工作。不幸的是,我似乎破坏了用户名/密码。我怀疑这是我的过滤器的顺序或我对用户名/密码和 ApiKey 身份验证使用相同的 AuthenticationManager。有什么建议吗?
    • @PhillipStack 您应该能够使用不同的身份验证管理器配置两个 WebSecurityConfigurerAdapter ala:stackoverflow.com/questions/33603156/…
    • 如果我理解正确,APIKey 不是私有的。任何使用客户端的人都可以打开开发者控制台并检查标题内容。对吗?
    • @marcellorvalle 通常,使用 API 密钥保护的 API 的客户端是另一个服务。如果您推断此 API 的客户端是 Web 浏览器,我建议您查看 OAuth / JWT 令牌以获取用户授权。
    猜你喜欢
    • 2019-11-29
    • 2014-01-05
    • 1970-01-01
    • 2018-07-17
    • 2014-05-25
    • 2012-07-18
    • 2016-03-04
    • 2020-10-31
    • 2012-03-09
    相关资源
    最近更新 更多