【问题标题】:Feign client custom oauth2 responseFeign 客户端自定义 oauth2 响应
【发布时间】:2018-02-12 20:38:43
【问题描述】:

我有两个微服务。

  • auth-service(使用 spring-security-oauth2)
  • 物业服务

property-microservice 实现一个 feign 客户端,以便通过链接从 auth-service 获取用户信息

/auth/users/get/{USER_ID}

property-microservice 使用 oauth2 身份验证来访问上面的 auth-service 端点(工作正常,我可以得到响应)

但 auth-service 不返回默认响应数据,因此 feign 客户端拦截器无法从响应中解析身份验证令牌。

需要明确的是,这是 spring 提供的 auth-service 的默认响应:

{
    "access_token": "6e7519de-f211-47ca-afc0-b65ede51bdfc",
    "token_type": "bearer",
    "refresh_token": "6146216f-bedd-42bf-b4e5-95131b0c6380",
    "expires_in": 7199,
    "scope": "ui"
}

但我确实返回这样的响应:

{
    "code": 0,
    "message": {
        "type": "message",
        "status": 200,
        "result": 200,
        "message": "Token aquired successfully."
    },
    "data": {
        "access_token": "6e7519de-f211-47ca-afc0-b65ede51bdfc",
        "token_type": "bearer",
        "refresh_token": "6146216f-bedd-42bf-b4e5-95131b0c6380",
        "expires_in": 7199,
        "scope": "ui"
    }
}

因此,fiegn 客户端查找标准响应数据,但由于我所做的修改而无法找到它。如果我可以在 OAuth2AccessTokenSupport 类中覆盖 ResponseExtractor ,我就可以正确解析响应。我如何管理解析来自 feign 客户端的自定义 oauth2 响应(或有其他解决方案)?

Application.java(属性服务)

// For jsr310 java 8 java.time.* support for JPA
@EntityScan(basePackageClasses = {Application.class, Jsr310JpaConverters.class})
@SpringBootApplication
@EnableResourceServer
@EnableOAuth2Client
@EnableFeignClients
@EnableHystrix
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableConfigurationProperties
@Configuration
@EnableAutoConfiguration
public class Application extends ResourceServerConfigurerAdapter {
    @Autowired
    private ResourceServerProperties sso;

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);

        HystrixDummy.start();
    }

    @Bean
    @ConfigurationProperties(prefix = "security.oauth2.client")
    public ClientCredentialsResourceDetails clientCredentialsResourceDetails() {
        return new ClientCredentialsResourceDetails();
    }

    @Bean
    public RequestInterceptor oauth2FeignRequestInterceptor() {
        return new OAuth2FeignRequestInterceptor(new DefaultOAuth2ClientContext(), clientCredentialsResourceDetails());
    }

    @Bean
    public OAuth2RestTemplate clientCredentialsRestTemplate() {
        return new OAuth2RestTemplate(clientCredentialsResourceDetails());
    }

    @Bean
    public ResourceServerTokenServices tokenServices() {
        return new CustomUserInfoTokenServices(this.sso.getUserInfoUri(), this.sso.getClientId());
    }
}

AuthServiceClient(属性服务)

@FeignClient(name = "auth-service", fallbackFactory = AuthServiceClient.AuthServiceClientFallback.class)
public interface AuthServiceClient {
    @RequestMapping(path = "/auth/users/get/{userId}", method = RequestMethod.GET)
    RestResponse get(@PathVariable(value = "userId") Long userId);

    @Component
    class AuthServiceClientFallback implements FallbackFactory<AuthServiceClient> {
        @Override
        public AuthServiceClient create(Throwable cause) {
            return userId -> new RestResponse(null, AppConstant.CODE_FAILURE, null);
        }
    }
}

Application.java(身份验证服务)

@SpringBootApplication
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

UserController.java(身份验证服务)

@RestController
@RequestMapping("/users")
public class UserController {
    @Autowired
    private UserService userService;

    @PreAuthorize("#oauth2.hasScope('server')")
    @RequestMapping(value = "/get/{userId}", method = RequestMethod.GET)
    public ResponseEntity<RestResponse> get(@Valid @PathVariable Long userId) throws UserNotFoundException {
        User user = this.userService.findOne(userId);

        RestResponse response = new RestResponse();

        RestMessage message = new RestMessage();
        message.setMessage(AppConstant.MESSAGE_USER_FETCHED_SUCCESS);
        message.setResult(AppConstant.CODE_USER_FETCHED);
        message.setStatus(HttpStatus.OK.value());

        response.setCode(AppConstant.CODE_SUCCESS);
        response.setMessage(message);
        response.setData(user);

        return new ResponseEntity<>(response, HttpStatus.OK);
    }
}

【问题讨论】:

    标签: spring-cloud spring-security-oauth2 netflix-feign


    【解决方案1】:

    我刚刚编写了自定义的FeignClientRequestInterceptorFeignClientAccessTokenProvider,如下所示:

    FeignClientAccessTokenProvider.java

    public class FeignClientAccessTokenProvider extends ClientCredentialsAccessTokenProvider {
        private ObjectMapper mapper = new ObjectMapper();
    
        @Override
        protected OAuth2AccessToken retrieveToken(AccessTokenRequest request, OAuth2ProtectedResourceDetails resource, MultiValueMap<String, String> form, HttpHeaders headers) throws OAuth2AccessDeniedException {
            OAuth2AccessToken token = super.retrieveToken(request, resource, form, headers);
    
            if (token != null && token.getValue() == null && token.getAdditionalInformation() != null) {
                if (token.getAdditionalInformation().containsKey("data")) {
                    token = this.mapper.convertValue(token.getAdditionalInformation().get("data"), OAuth2AccessToken.class);
                }
            }
    
            return token;
        }
    }
    

    FeignClientRequestInterceptor .java

    public class FeignClientRequestInterceptor implements RequestInterceptor {
    
        public static final String BEARER = "Bearer";
    
        public static final String AUTHORIZATION = "Authorization";
    
        private final OAuth2ClientContext oAuth2ClientContext;
    
        private final OAuth2ProtectedResourceDetails resource;
    
        private final String tokenType;
    
        private final String header;
    
        private AccessTokenProvider accessTokenProvider = new AccessTokenProviderChain(Arrays
                .<AccessTokenProvider>asList(new AuthorizationCodeAccessTokenProvider(),
                        new ImplicitAccessTokenProvider(),
                        new ResourceOwnerPasswordAccessTokenProvider(),
                        new FeignClientAccessTokenProvider()));
    
        /**
         * Default constructor which uses the provided OAuth2ClientContext and Bearer tokens
         * within Authorization header
         *
         * @param oAuth2ClientContext provided context
         * @param resource            type of resource to be accessed
         */
        public FeignClientRequestInterceptor(OAuth2ClientContext oAuth2ClientContext,
                                             OAuth2ProtectedResourceDetails resource) {
            this(oAuth2ClientContext, resource, BEARER, AUTHORIZATION);
        }
    
        /**
         * Fully customizable constructor for changing token type and header name, in cases of
         * Bearer and Authorization is not the default such as "bearer", "authorization"
         *
         * @param oAuth2ClientContext current oAuth2 Context
         * @param resource            type of resource to be accessed
         * @param tokenType           type of token e.g. "token", "Bearer"
         * @param header              name of the header e.g. "Authorization", "authorization"
         */
        public FeignClientRequestInterceptor(OAuth2ClientContext oAuth2ClientContext,
                                             OAuth2ProtectedResourceDetails resource, String tokenType, String header) {
            this.oAuth2ClientContext = oAuth2ClientContext;
            this.resource = resource;
            this.tokenType = tokenType;
            this.header = header;
        }
    
        /**
         * Create a template with the header of provided name and extracted extract
         *
         * @see RequestInterceptor#apply(RequestTemplate)
         */
        @Override
        public void apply(RequestTemplate template) {
            template.header(this.header, extract(this.tokenType));
        }
    
        /**
         * Extracts the token extract id the access token exists or returning an empty extract
         * if there is no one on the context it may occasionally causes Unauthorized response
         * since the token extract is empty
         *
         * @param tokenType type name of token
         * @return token value from context if it exists otherwise empty String
         */
        protected String extract(String tokenType) {
            OAuth2AccessToken accessToken = getToken();
            return String.format("%s %s", tokenType, accessToken.getValue());
        }
    
        /**
         * Extract the access token within the request or try to acquire a new one by
         * delegating it to {@link #acquireAccessToken()}
         *
         * @return valid token
         */
        public OAuth2AccessToken getToken() {
    
            OAuth2AccessToken accessToken = this.oAuth2ClientContext.getAccessToken();
            if (accessToken == null || accessToken.isExpired()) {
                try {
                    accessToken = acquireAccessToken();
                } catch (UserRedirectRequiredException e) {
                    this.oAuth2ClientContext.setAccessToken(null);
                    String stateKey = e.getStateKey();
                    if (stateKey != null) {
                        Object stateToPreserve = e.getStateToPreserve();
                        if (stateToPreserve == null) {
                            stateToPreserve = "NONE";
                        }
                        this.oAuth2ClientContext.setPreservedState(stateKey, stateToPreserve);
                    }
                    throw e;
                }
            }
            return accessToken;
        }
    
        /**
         * Try to acquire the token using a access token provider
         *
         * @return valid access token
         * @throws UserRedirectRequiredException in case the user needs to be redirected to an
         *                                       approval page or login page
         */
        protected OAuth2AccessToken acquireAccessToken()
                throws UserRedirectRequiredException {
            AccessTokenRequest tokenRequest = this.oAuth2ClientContext.getAccessTokenRequest();
            if (tokenRequest == null) {
                throw new AccessTokenRequiredException(
                        "Cannot find valid context on request for resource '"
                                + this.resource.getId() + "'.",
                        this.resource);
            }
            String stateKey = tokenRequest.getStateKey();
            if (stateKey != null) {
                tokenRequest.setPreservedState(
                        this.oAuth2ClientContext.removePreservedState(stateKey));
            }
            OAuth2AccessToken existingToken = this.oAuth2ClientContext.getAccessToken();
            if (existingToken != null) {
                this.oAuth2ClientContext.setAccessToken(existingToken);
            }
            OAuth2AccessToken obtainableAccessToken;
            obtainableAccessToken = this.accessTokenProvider.obtainAccessToken(this.resource,
                    tokenRequest);
            if (obtainableAccessToken == null || obtainableAccessToken.getValue() == null) {
                throw new IllegalStateException(
                        " Access token provider returned a null token, which is illegal according to the contract.");
            }
            this.oAuth2ClientContext.setAccessToken(obtainableAccessToken);
            return obtainableAccessToken;
        }
    }
    

    希望这对面临此问题的任何人有所帮助。

    【讨论】:

      猜你喜欢
      • 2020-09-25
      • 1970-01-01
      • 2019-02-18
      • 2017-12-23
      • 2019-05-17
      • 2019-11-12
      • 2021-06-12
      • 2017-02-22
      • 2015-07-27
      相关资源
      最近更新 更多