使用API​​密钥和密钥保护Spring Boot API

Vit*_*siv 28 java api spring spring-security spring-boot

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

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

你能告诉我怎样才能做到这一点?谢谢!

Mar*_*all 47

创建一个过滤器,用于获取您用于身份验证的标头.

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";
    }

}
Run Code Online (Sandbox Code Playgroud)

在Web Security配置中配置过滤器.

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();
    }

}
Run Code Online (Sandbox Code Playgroud)

  • @PhillipStack您应该能够使用不同的身份验证管理器配置两个WebSecurityConfigurerAdapter:/sf/ask/2352220951/ (2认同)
  • @marcellorvalle 通常,使用 API 密钥保护的 API 的客户端是另一个服务。如果您推断此 API 的客户端将是 Web 浏览器,我建议您研究 OAuth / JWT 令牌以进行用户授权。 (2认同)
  • 有用的必读:[REST 安全备忘单/API 密钥](https://cheatsheetseries.owasp.org/cheatsheets/REST_Security_Cheat_Sheet.html#api-keys) (2认同)

小智 13

以 @MarkOfHall 的答案为基础,WebSecurityConfigurerAdapter已被弃用(请参阅https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter)。所以他的版本APISecurityConfig现在看起来像:

package com.fasset.ledger.auth;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
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;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
@Order(1)
public class APISecurityConfig {

@Value("${yourapp.http.auth-token-header-name}")
private String principalRequestHeader;

@Value("${yourapp.http.auth-token}")
private String principalRequestValue;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) 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;
        }
    });
    http.antMatcher("/api/**").
            csrf().disable().
            sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).
            and().addFilter(filter).authorizeRequests().anyRequest().authenticated();

    return http.build();
  }
}
Run Code Online (Sandbox Code Playgroud)


mat*_*the 9

我意识到我在这个游戏上有点晚了,但我也设法让 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

还有过滤器

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

  • @MuhammadAbuBakr,`@Transient` 指示 Spring 不要为此请求创建 HttpSession。对于使用 API 密钥的无状态请求,您不需要创建 HttpSession,并且如果客户端碰巧忽略从服务器发回的 cookie,则每个请求最终都会在服务器上创建一个全新的 HttpSession。如果您需要在请求之间存储状态,并且您的客户保存并尊重 cookie,请随时将其取出。 (2认同)

Are*_*efe 9

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

yourapp.http.auth-token-header-name=X-API-KEY
yourapp.http.auth-token=abc123
Run Code Online (Sandbox Code Playgroud)

在Postman中设置认证值如下:

在此输入图像描述

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

$ curl -H "X-API-KEY: abc123" "http://localhost:8080/api/v1/property/1"
Run Code Online (Sandbox Code Playgroud)

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


Vic*_*scu 7

基于 @zawar 和 @MarkOfHall 的答案以及https://github.com/gregwhitaker/springboot-apikey-example

截至 2022 年 12 月 8 日,现代解决方案如下所示:

package com.mygloriousapp.auth;
import javax.servlet.http.HttpServletRequest;
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;

/**
 * Filter responsible for getting the api key off of incoming requests that need to be authorized.
 */
public class ApiKeyAuthFilter extends AbstractPreAuthenticatedProcessingFilter {

  private final String headerName;

  public ApiKeyAuthFilter(final String headerName) {
    this.headerName = headerName;
  }

  @Override
  protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
    return request.getHeader(headerName);
  }

  @Override
  protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
    // No credentials when using API key
    return null;
  }
}




package com.mygloriousapp.config;

import com.mygloriousapp.auth.ApiKeyAuthFilter;
import java.util.Objects;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
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.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
@Order(1)
public class SecurityConfig {

  @Value("${app.http.auth-token-header-name}")
  private String principalRequestHeader;

  @Value("${app.http.auth-token}")
  private String principalRequestValue;

  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    ApiKeyAuthFilter filter = new ApiKeyAuthFilter(principalRequestHeader);
    filter.setAuthenticationManager(
        authentication -> {
          String principal = (String) authentication.getPrincipal();
          if (!Objects.equals(principalRequestValue, principal)) {
            throw new BadCredentialsException(
                "The API key was not found or not the expected value.");
          }
          authentication.setAuthenticated(true);
          return authentication;
        });
    http.antMatcher("/**")
        .csrf()
        .disable()
        .sessionManagement()
        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
        .addFilter(filter)
        .authorizeRequests()
        .anyRequest()
        .authenticated();

    return http.build();
  }
}
Run Code Online (Sandbox Code Playgroud)

application.properties中所需配置:

app.http.auth-token-header-name=X-API-Key
app.http.auth-token=109353c6-6432-4acf-8e77-ef842f64a664
Run Code Online (Sandbox Code Playgroud)

pom.xml中的依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>    
Run Code Online (Sandbox Code Playgroud)

如果您使用的是 Postman,请单击集合并编辑“授权”选项卡: 邮递员授权

  • 感谢您更新并将这些内容以真实的代码风格组合在一起 (3认同)