如何在Java Spring Boot中从请求头获取Bearer令牌?

30

我想要实现的目标是,在Java Spring Boot RESTApi控制器中获取来自前端提交的Bearer令牌,并使用Feign客户端向另一个微服务发送另一个请求。以下是我所做的:

输入图像描述

上面的图片展示了我在postman中进行请求的方式,下面是我的控制器代码:

@Operation(summary = "Save new")
@PostMapping("/store")
public ResponseEntity<ResponseRequest<TransDeliveryPlanning>> saveNewTransDeliveryPlanning(
        @Valid @RequestBody InputRequest<TransDeliveryPlanningDto> request) {

    TransDeliveryPlanning newTransDeliveryPlanning = transDeliveryPlanningService.save(request);

    ResponseRequest<TransDeliveryPlanning> response = new ResponseRequest<TransDeliveryPlanning>();

    if (newTransDeliveryPlanning != null) {
        response.setMessage(PESAN_SIMPAN_BERHASIL);
        response.setData(newTransDeliveryPlanning);
    } else {
        response.setMessage(PESAN_SIMPAN_GAGAL);
    }

    return ResponseEntity.ok(response);
}

这是我的服务的外观:

public TransDeliveryPlanning save(InputRequest<TransDeliveryPlanningDto> request) {
       Future<List<PartnerDto>> initPartners = execs.submit(getDataFromAccount(transDeliveryPlanningDtSoDtoPartnerIdsSets));

}

public Callable<List<PartnerDto>> getDataFromAccount(Set<Long> ids) {

    String tokenString = "i should get the token from postman, how do i get it to here?";
    List<PartnerDto> partnerDtoResponse = accountFeignClient.getData("Bearer " + tokenString, ids);
    
    return () -> partnerDtoResponse;
}

你可以看到,在“tokenString”中我放置了一个字符串,我想知道如何从Postman将其发送到那里?


好的,现在你遇到了什么问题? - priyranjan
我该如何获取从 Postman(客户端)提交的令牌值?将其作为字符串获取并替换“tokenString”值,以便可以将其用于提交到另一个请求。 - Ke Vin
您是否将此令牌用于其他任何目的?例如,用于验证调用Feign客户端的应用程序中的用户身份? - jccampanero
是的,我使用它来调用另一个微服务,使用Feign客户端,在相同的网关中,因为它在相同的Spring Security后面需要相同的令牌。 - Ke Vin
我理解正确吗,你的第一个微服务,暴露 '/store' 端点,是使用令牌进行身份验证的?如果是这样,它是标准的身份验证方案(例如OAuth2),还是自定义的身份验证方案(自定义JWT令牌身份验证)? - da-sha1
9个回答

31

尽管建议的答案可行,但每次将令牌传递给FeignClient调用仍不是最好的方法。 我建议为Feign请求创建一个拦截器,在那里可以从RequestContextHolder中提取令牌并直接添加到请求头中。 像这样:

    @Component
    public class FeignClientInterceptor implements RequestInterceptor {
    
      private static final String AUTHORIZATION_HEADER = "Authorization";

      public static String getBearerTokenHeader() {
        return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest().getHeader("Authorization");
      }
    
      @Override
      public void apply(RequestTemplate requestTemplate) {

          requestTemplate.header(AUTHORIZATION_HEADER, getBearerTokenHeader());
       
      }
    }

这样,您就可以得到一个干净的解决方案来解决您的问题。


17

这里有几个选项。

例如,您可以使用一个请求作用域的bean,并像您建议的那样使用一个MVC拦截器

基本上,您需要为令牌值定义一个包装器:

public class BearerTokenWrapper {
   private String token;

   // setters and getters
}

接下来,提供一个MVC HandlerInterceptor的实现:

public class BearerTokenInterceptor extends HandlerInterceptorAdapter {

  private BearerTokenWrapper tokenWrapper;

  public BearerTokenInterceptor(BearerTokenWrapper tokenWrapper) {
    this.tokenWrapper = tokenWrapper;
  }

  @Override
  public boolean preHandle(HttpServletRequest request,
          HttpServletResponse response, Object handler) throws Exception {
    final String authorizationHeaderValue = request.getHeader("Authorization");
    if (authorizationHeaderValue != null && authorizationHeaderValue.startsWith("Bearer")) {
      String token = authorizationHeaderValue.substring(7, authorizationHeaderValue.length());
      tokenWrapper.setToken(token);
    }
    
    return true;
  }
}

这个拦截器应该在你的MVC配置中进行注册。例如:

@EnableWebMvc
@Configuration
public class WebConfiguration extends WebConfigurer { /* or WebMvcConfigurerAdapter for Spring 4 */

  @Override
  public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(bearerTokenInterceptor());
  }

  @Bean
  public BearerTokenInterceptor bearerTokenInterceptor() {
      return new BearerTokenInterceptor(bearerTokenWrapper());
  }

  @Bean
  @Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
  public BearerTokenWrapper bearerTokenWrapper() {
    return new BearerTokenWrapper();
  }

}

通过这个设置,你可以在Service中自动装配相应的bean:

@Autowired
private BearerTokenWrapper tokenWrapper;

//...


public TransDeliveryPlanning save(InputRequest<TransDeliveryPlanningDto> request) {
       Future<List<PartnerDto>> initPartners = execs.submit(getDataFromAccount(transDeliveryPlanningDtSoDtoPartnerIdsSets));

}

public Callable<List<PartnerDto>> getDataFromAccount(Set<Long> ids) {

    String tokenString = tokenWrapper.getToken();
    List<PartnerDto> partnerDtoResponse = accountFeignClient.getData("Bearer " + tokenString, ids);
    
    return () -> partnerDtoResponse;
}

在 stack overflow 上提供了类似的解决方案。例如,可以参考这个相关问题

除了基于 Spring 的方法外,您还可以尝试类似于这个 stackoverflow 问题中介绍的解决方案。

老实说,我从未测试过,但它似乎可以直接在 Feign 客户端定义中提供请求头值,例如:

@FeignClient(name="AccountFeignClient")
public interface AccountFeignClient {    
    @RequestMapping(method = RequestMethod.GET, value = "/data")
    List<PartnerDto> getData(@RequestHeader("Authorization") String token, Set<Long> ids);
}

当然,你也可以创建一个通用的Controller,其他Controller可以继承它。这个Controller将提供必要的逻辑来从提供的Authorization头和HTTP请求中获取令牌, 但在我看来,前面提到的任何一种解决方案都更好。


@KeVin,你可以尝试一下提出的解决方案吗? - jccampanero
等我尝试另一个解决方案,但如果悬赏在我选择解决方案之前到期,我会再次开始悬赏以奖励你们中的一位。 - Ke Vin
1
非常感谢您,@KeVin,我真的很感激。 - jccampanero
这是一个非常有帮助的答案。我遇到了一个更复杂的问题,因为我正在使用Spring Boot的Feign Hystrix。对于那些需要解决类似问题的人,请查看关于Hystrix和Threadlocals的这篇文章 https://medium.com/@saurav24081996/java-hystrix-and-threadlocals-95ea9e194e83 以及Hystrix插件 - https://github.com/Netflix/Hystrix/wiki/Plugins#concurrencystrategy - Tony Murphy
非常感谢您的留言@TonyMurphy,我非常感激。 - jccampanero
欢迎 @jccampanero.. 只是额外的更新,使用 Feign Hystrix 的解决方案似乎非常容易 - 只需要使用 Servlet 过滤器来初始化上下文,从 Servlet 请求头中检索承载令牌,然后使用 HystrixRequestVariableDefault 将令牌存储以供以后使用 https://github.com/Netflix/Hystrix/issues/92#issuecomment-260548068 - Tony Murphy

10

从头部获取Bearer Token的简单方法是使用@RequestHeader和头部名称。

请参见下面的代码示例

@PostMapping("/some-endpoint")
public ResponseEntity<String> someClassNmae(@RequestHeader("Authorization") String bearerToken) {

   System.out.println(bearerToken); // print out bearer token

   // some more code
}

3
使用此注释获取前端返回的标头信息: @RequestHeader("Authorization") String token 示例:
@GetMapping("/hello")
    public void hello(@RequestHeader("Authorization") String token){

3
我有一个类似的案例。我拦截了来自一个微服务的请求,获取了令牌并将其设置为我的新 ApiClient,并使用此 ApiClient 调用另一个微服务的端点。但我真的不知道是否有可能预配置 Feign 客户端。你可以做的一件事是创建 DefaultApiFilter,拦截请求,在数据库中保存令牌(或将其设置为某个静态变量、某个单例类或类似物中的值),然后在尝试使用 FeignClient 时调用该服务方法:
package com.north.config;

import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

@Component
public class DefaultApiFilter implements Filter {


@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, 
FilterChain filterChain) throws IOException, ServletException {
    HttpServletRequest req = (HttpServletRequest) servletRequest;

    String auth = req.getHeader("Authorization");

    //TODO if you want you can persist your token here and use it on other place

    //TODO This may be used for verification if it comes from the right endpoint and if you should save the token
    final String requestURI = ((RequestFacade) servletRequest).getRequestURI();

    filterChain.doFilter(servletRequest, servletResponse);
    }
}

doFilter方法会在任何端点被调用之前执行,然后再调用端点。

当使用accountFeignClient.getData("Bearer " + tokenString, ids);调用时,您可以从数据库(或任何其他存储位置)获取它,并在此处设置它。


2

我已经得到了答案,但我认为我还需要等待更好的选择。我的答案是在每个控制器中添加 @RequestHeader 来获取我的 token 值,并使用 String token = headers.getFirst(HttpHeaders.AUTHORIZATION); 来获取 token。这里是我的完整控制器:

@Operation(summary = "Save new")
@PostMapping("/store")
public ResponseEntity<ResponseRequest<TransDeliveryPlanning>> saveNewTransDeliveryPlanning(@RequestHeader HttpHeaders headers, 
        @Valid @RequestBody InputRequest<TransDeliveryPlanningDto> request) {

    String token = headers.getFirst(HttpHeaders.AUTHORIZATION);

    TransDeliveryPlanning newTransDeliveryPlanning = transDeliveryPlanningService.save(token, request);

    ResponseRequest<TransDeliveryPlanning> response = new ResponseRequest<TransDeliveryPlanning>();

    if (newTransDeliveryPlanning != null) {
        response.setMessage(PESAN_SIMPAN_BERHASIL);
        response.setData(newTransDeliveryPlanning);
    } else {
        response.setMessage(PESAN_SIMPAN_GAGAL);
    }

    return ResponseEntity.ok(response);
}

我曾在某处读到有一种叫做Interceptor的东西,这样我们就不必在每个控制器中输入@RequestHeader了,但我不知道这是否是解决方案,或者如何正确使用它。如果有人能提供更好的解决方案,我将接受你的答案。


0

我认为@stacker下面的答案是正确的,但我认为它有些不完整,缺少“如何在Feign中使用它”的部分。

为了举例说明,我将提供一个真实的用例,您可以拦截调用者到您的服务的User-Agent并在Feign调用中转发它

假设您使用基于注释的Feign客户端,这就是您可以在所有Feign客户端调用中使用拦截器而无需任何额外代码的方式。

@Configuration
@EnableFeignClients(
    defaultConfiguration = DefaultFeignConfiguration.class
)
public class FeignConfig
{
}

@Configuration
@Import(FeignClientsConfiguration.class)
public class DefaultFeignConfiguration
{
    @Bean
    public RequestInterceptor userAgentHeaderInterceptor() {
        return UserAgentHeaderInterceptor();
    } 
}

这是用户代理拦截器类

public class UserAgentHeaderInterceptor extends BaseHeaderInterceptor
{

    private static final String USER_AGENT = "User-Agent";


    public UserAgentHeaderInterceptor()
    {
        super(USER_AGENT);
    }
}

public class BaseHeaderInterceptor implements RequestInterceptor
{

    private final String[] headerNames;


    public BaseHeaderInterceptor(String... headerNames)
    {
        this.headerNames = headerNames;
    }


    @Override
    public void apply(RequestTemplate template)
    {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

        if (attributes != null)
        {
            HttpServletRequest httpServletRequest = attributes.getRequest();

            for (String headerName : headerNames)
            {
                String headerValue = httpServletRequest.getHeader(headerName);
                if (headerValue != null && !headerValue.isEmpty())
                {
                    template.header(headerName, headerValue);
                }
            }
        }
    }
}

在您的情况下,您只需要采取这个基类并像 UserAgentHeaderInterceptor 一样创建自己的拦截器即可。

0
您可以使用以下Java代码从安全上下文中获取令牌:
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.jwt.Jwt;
 ...

Jwt authenticationToken = (Jwt) SecurityContextHolder.getContext()
                                                     .getAuthentication()
                                                     .getCredentials();
String tokenString = authenticationToken.getTokenValue();

-2
你可以在一个工具类中创建这个简单的静态方法,并直接重复使用它。
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

public class BearerTokenUtil {

  public static String getBearerTokenHeader() {
    return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest().getHeader("Authorization");
  }
}

你的服务将会是这个样子的

public TransDeliveryPlanning save(InputRequest<TransDeliveryPlanningDto> request) {
       Future<List<PartnerDto>> initPartners = execs.submit(getDataFromAccount(transDeliveryPlanningDtSoDtoPartnerIdsSets));
}

public Callable<List<PartnerDto>> getDataFromAccount(Set<Long> ids) {
    List<PartnerDto> partnerDtoResponse = accountFeignClient.getData(BearerTokenUtil.getBearerTokenHeader(), ids);
    return () -> partnerDtoResponse;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接