使用feign-reactor-webclient访问Spring Oauth2 资源服务器

895 阅读2分钟

Spring Security 官方建议使用Spring WebClient 来访问Oauth2资源服务器:

@Bean
WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
    ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
            new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
    return WebClient.builder()
            .apply(oauth2Client.oauth2Configuration())
            .build();
}
@Autowired
private WebClient webClient;

@Test
void test_2022_01_26_16_19_52() {
    Object obj = webClient.get()
            .uri("http://资源服务器中某个api的地址")
            //以client credentials 客户端凭证的模式访问资源,这种模式简单很多,如果需要用户信息可以显式传递
            .attributes(ServletOAuth2AuthorizedClientExchangeFilterFunction
                    .clientRegistrationId("资源服务器在yml中的注册id"))
            .retrieve()
            .bodyToMono(Object.class)
            //阻塞式调用
            .block();
}

如果我们使用Oauth2来保护我们两个服务之间的调用,这样使用资源服务器的形式访问接口api既可,但是如果接口比较多,显然使用feign更为方便,但是官方提供Oauth2认证信息的附加是通过WebClinet添加的,而WebClinet是Spring-webflux 非阻塞式HTTP Client ,但是不管是open-feign 还是SpringCould-Feign 都不支持异步客户端,不过好在官方文档中提醒我们,可以使用社区的feign-reactor来使用异步客户端。

所以我们引入feign-reactor-webclient依赖

<dependency>
   <groupId>com.playtika.reactivefeign</groupId>
   <artifactId>feign-reactor-webclient</artifactId>
   <version>3.1.5</version>
</dependency>

构建我们的 Feign接口

@Headers({"Accept: application/json"})
public interface XXXServiceApi {
    String URI = "资源服务器地址/XXXServiceApi";

    @RequestLine("GET" + URI + "/obj")
    Mono<Result<Object>> getObj(@Param("id") Long id); //Result是自定义的通用返回类
    
    @RequestLine("GET" + URI + "/void")
    Mono<Result<Void>> doSomeThing(); //api本身无返回值,但是可能报异常,异常时api返回Result
}

其中,如果被调用的api通过抛出异常来中断执行,并且配置了全局异常处理器,那么最好不管是异常还是正常,返回结果都通过通用返回类来包装:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> implements Serializable {
    private static final long serialVersionUID = 1L;
    private Integer code;
    private List<String> messages;
    private T data;
    
    public Result<T> code(Integer code) {
        this.code = code;
        return this;
    }
    
    public Result<T> messages(Collection<String> messages) {
        this.messages = new ArrayList<>(messages);
        return this;
    }

    @SuppressWarnings("unchecked")
    public <O> Result<O> data(O data) {
        this.data = (T) data;
        return (Result<O>) this;
    }
    
    public static <O> Result<O> ok() {
        return new Result<O>().code(200).messages("success");
    }
    public static <O> Result<O> ok(O data) {
        return ok().data(data);
    }
    public static <O> Result<O> err() {
        return new Result<O>().code(-1);
    }
}

通过code判断是否成功,在异常处理器中将错误信息全部放在messages字段, 方法返回值放在data字段中,这样feign就可以很方便的反序列化正常返回值和异常返回值。

然后只需要将WebClinet OAuth2配置 和 feign-reactor 结合使用即可 👇

@Bean
public XXXServiceApi xxxServiceApi(OAuth2AuthorizedClientManager authorizedClientManager) {
    return WebReactiveFeign
            .<XXXServiceApi>builder(WebClient.builder()
                    .apply(new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager).oauth2Configuration())
                    .defaultRequest(spec -> spec.attributes(ServletOAuth2AuthorizedClientExchangeFilterFunction.clientRegistrationId("资源服务器在yml中的注册id")))
            )
            .target(XXXServiceApi.class, "资源服务器地址");
}

直接调用 XXXServiceApi ,或者再将XXXServiceApi 包装一层,阻塞调用并判断code不为200时抛出异常:

public abstract class FeignServiceUtil {

    public static <T> T checkAndReturn(Mono<Result<T>> result) {
        return checkReturn(result.block());
    }

    public static <T> T checkAndReturn(Result<T> result) {
        if (result == null) {
            return null;
        }
        if (result.getCode() != 200) {
            List<String> err = new ArrayList<>(result.getMessages());
            throw new RuntimeException(StringUtils.collectionToCommaDelimitedString(err));
        }
        return result.getData();
    }
}