Могу ли я добавить некоторую информацию в конечную точку oauth/check_token и получить ее на сервере авторизации?

Предисловие

Я работаю над приложением OAuth для обеспечения безопасности между двумя серверами. У меня есть OAuth Server и Resource Server. В Resource Server развернут один .war, содержащий 4 APIs.

Единая ответственность

  1. OAuth server должен проверить access token, который был передан API (1 из 4) из того же .war.
  2. OAuth server должен хранить hit count для конкретного accessToken для конкретного API. Если hit count превысит настроенное hits, OAuth server выдаст ошибку 403: Forbidden.
  3. Каждый API в .war должен сначала проверить accessToken из OAuth server и, если он подтвержден, затем приступить к предоставлению ответа.

Что я наделал:

Если .war имеет один API, то я могу просто заставить два сервера обмениваться данными, используя webHook, ниже приведен код, который это делает.

На стороне сервера ресурсов:

Мои URL-адреса для разных API:

  • localhost:8080/API/API1
  • localhost:8080/API/API2

Код ниже направляет любой запрос, если у них есть /API/anything к spring security filters

<http pattern="/API/**" create-session="never" authentication-manager-ref="authenticationManager" entry-point-ref="oauthAuthenticationEntryPoint" xmlns="http://www.springframework.org/schema/security">
        <anonymous enabled="false" />        
        <intercept-url pattern="/places/**" method="GET" access="IS_AUTHENTICATED_FULLY" />
        <custom-filter ref="resourceServerFilter" before="PRE_AUTH_FILTER" />
        <access-denied-handler ref="oauthAccessDeniedHandler" />
</http>

Я использовал службы удаленных токенов и определил webHook для маршрутизации запроса к OAuth server.

<bean id="tokenServices"  class="org.springframework.security.oauth2.provider.token.RemoteTokenServices">
    <property name="checkTokenEndpointUrl" value="http://localhost:8181/OUTPOST/oauth/check_token"/>
    <property name="clientId" value="atlas"/>
    <property name="clientSecret" value="atlas"/>
</bean>

Конфигурация сервера аутентификации

@EnableAuthorizationServer
@Configuration
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    private static String REALM="OUTPOST_API";

    @Autowired
    private ClientDetailsService clientService;

    @Autowired
    public AuthorizationServerConfig(AuthenticationManager authenticationManager,RedisConnectionFactory redisConnectionFactory) {
        this.authenticationManager = authenticationManager;
        this.redisTokenStore = new RedisTokenStore(redisConnectionFactory);
    }

//  @Autowired
//  @Qualifier("authenticationManagerBean")
    private AuthenticationManager authenticationManager;

    private TokenStore redisTokenStore;

    @Autowired
    private UserApprovalHandler userApprovalHandler;

    @Autowired
    private RedisConnectionFactory redisConnectionFactory;

    @Override

    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {

        security.tokenKeyAccess("isAuthenticated()")
                .checkTokenAccess("isAuthenticated()").
        realm(REALM+"/client");

    }

    @Override

    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {

        clients

                .inMemory()
                .withClient("cl1")
                .secret("pwd")
                .authorizedGrantTypes("password", "client_credentials", "refresh_token")
                .authorities("ROLE_CLIENT", "ROLE_ADMIN")
                .scopes("read", "write", "trust")/*
                .resourceIds("sample-oauth")*/              
                .accessTokenValiditySeconds(1000)               
                .refreshTokenValiditySeconds(5000)
                .and()
                .withClient("atlas")
                .secret("atlas");



    }

    @Bean
    @Autowired
    public TokenStore tokenStore(RedisConnectionFactory redisConnectionFactory) {
        this.redisTokenStore = new RedisTokenStore(redisConnectionFactory);
         return this.redisTokenStore;
    }

    @Bean
    public WebResponseExceptionTranslator loggingExceptionTranslator() {
        return new DefaultWebResponseExceptionTranslator() {
            @Override
            public ResponseEntity<OAuth2Exception> translate(Exception e) throws Exception {
                // This is the line that prints the stack trace to the log. You can customise this to format the trace etc if you like
                e.printStackTrace();

                // Carry on handling the exception
                ResponseEntity<OAuth2Exception> responseEntity = super.translate(e);
                HttpHeaders headers = new HttpHeaders();
                headers.setAll(responseEntity.getHeaders().toSingleValueMap());
                OAuth2Exception excBody = responseEntity.getBody();
                return new ResponseEntity<>(excBody, headers, responseEntity.getStatusCode());
            }
        };
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {

        endpoints.tokenStore(redisTokenStore).userApprovalHandler(userApprovalHandler)
                .authenticationManager(authenticationManager)
                .exceptionTranslator(loggingExceptionTranslator());
    }

    public void setRedisConnectionFactory(RedisConnectionFactory redisConnectionFactory) {
        this.redisConnectionFactory = redisConnectionFactory;
    }



        @Bean
        public TokenStoreUserApprovalHandler userApprovalHandler(){
            TokenStoreUserApprovalHandler handler = new TokenStoreUserApprovalHandler();
            handler.setTokenStore(redisTokenStore);
            handler.setRequestFactory(new DefaultOAuth2RequestFactory(clientService));
            handler.setClientDetailsService(clientService);
            return handler;
        }

        @Bean
        @Autowired
        public ApprovalStore approvalStore() throws Exception {
            TokenApprovalStore store = new TokenApprovalStore();
            store.setTokenStore(redisTokenStore);
            return store;
        }

        @Bean
        @Primary
        @Autowired
        public DefaultTokenServices tokenServices() {
            DefaultTokenServices tokenServices = new DefaultTokenServices();
            tokenServices.setSupportRefreshToken(true);
            tokenServices.setTokenStore(redisTokenStore);
            return tokenServices;
        }

    }

    @Component
    class MyOAuth2AuthenticationEntryPoint extends OAuth2AuthenticationEntryPoint{}

В чем мне нужна помощь:

Проблема в поддержке одиночных .war и multiple API. Проблема в том, что конфигурация spring создается на уровне пакета, из-за чего все APIs в .war имеют одинаковые clientID и clientSecret.

Как мой сервер OAuth узнает, к каким конкретным API осуществляется доступ и из каких API необходимо вычесть hitCount.

Возможное решение? Я думал о настройке RemoteTokenService и добавлении параметра запроса на webHoot URL, а затем использовании фильтра на сервере OAuth для получения переданного tag (если можно так назвать)

Это вообще возможно? Есть ли лучший подход, чем этот, который не включает все эти обходные пути?


person Community    schedule 15.02.2018    source источник
comment
Управление оставшимися обращениями к API для любого пользователя является работой сервера авторизации. Сервер аутентификации проверяет оставшиеся попадания, а затем авторизует запрос. Сервер ресурсов будет уделять больше внимания API, его работе, а не управлению обращениями.   -  person    schedule 15.02.2018


Ответы (1)


Эврика!! Наконец-то я нашел способ решить эту проблему.

Все, что вам нужно сделать, это:

Конфигурация на сервере ресурсов

Вместо использования RemoteTokenService создайте custom remote token service, который добавляет некоторые данные (параметр запроса) в сгенерированный запрос.

public class CustomRemoteTokenService implements ResourceServerTokenServices {

protected final Log logger = LogFactory.getLog(getClass());

private RestOperations restTemplate;

private String checkTokenEndpointUrl;

private String clientId;

private String clientSecret;

private String tokenName = "token";

private AccessTokenConverter tokenConverter = new DefaultAccessTokenConverter();

@Autowired
public CustomRemoteTokenService() {
    restTemplate = new RestTemplate();
    ((RestTemplate) restTemplate).setErrorHandler(new DefaultResponseErrorHandler() {
        @Override
        // Ignore 400
        public void handleError(ClientHttpResponse response) throws IOException {
            if (response.getRawStatusCode() != 400) {
                super.handleError(response);
            }
        }
    });
}

public void setRestTemplate(RestOperations restTemplate) {
    this.restTemplate = restTemplate;
}

public void setCheckTokenEndpointUrl(String checkTokenEndpointUrl) {
    this.checkTokenEndpointUrl = checkTokenEndpointUrl;
}

public void setClientId(String clientId) {
    this.clientId = clientId;
}

public void setClientSecret(String clientSecret) {
    this.clientSecret = clientSecret;
}

public void setAccessTokenConverter(AccessTokenConverter accessTokenConverter) {
    this.tokenConverter = accessTokenConverter;
}

public void setTokenName(String tokenName) {
    this.tokenName = tokenName;
}

@Override
public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException {

    /*
     * This code needs to be more dynamic. Every time an API is added we have to add its entry in the if check for now.
     * Should be changed later.
     */
    HttpServletRequest request = Context.getCurrentInstance().getRequest();         
    MultiValueMap<String, String> formData = new LinkedMultiValueMap<String, String>();
    String uri = request.getRequestURI();

    formData.add(tokenName, accessToken);       

    if(request != null) {
        if(uri.contains("API1")) {
            formData.add("api", "1");
        }else if(uri.contains("API2")) {
            formData.add("api", "2");
        } 
    }

    HttpHeaders headers = new HttpHeaders();
    headers.set("Authorization", getAuthorizationHeader(clientId, clientSecret));
    Map<String, Object> map = postForMap(checkTokenEndpointUrl, formData, headers);

    if (map.containsKey("error")) {
        logger.debug("check_token returned error: " + map.get("error"));
        throw new InvalidTokenException(accessToken);
    }



    Assert.state(map.containsKey("client_id"), "Client id must be present in response from auth server");
    return tokenConverter.extractAuthentication(map);
}

@Override
public OAuth2AccessToken readAccessToken(String accessToken) {
    throw new UnsupportedOperationException("Not supported: read access token");
}

private String getAuthorizationHeader(String clientId, String clientSecret) {
    String creds = String.format("%s:%s", clientId, clientSecret);
    try {
        return "Basic " + new String(Base64.encode(creds.getBytes("UTF-8")));
    }
    catch (UnsupportedEncodingException e) {
        throw new IllegalStateException("Could not convert String");
    }
}

private Map<String, Object> postForMap(String path, MultiValueMap<String, String> formData, HttpHeaders headers) {
    if (headers.getContentType() == null) {
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
    }
    @SuppressWarnings("rawtypes")
    Map map = restTemplate.exchange(path, HttpMethod.POST,
            new HttpEntity<MultiValueMap<String, String>>(formData, headers), Map.class).getBody();
    @SuppressWarnings("unchecked")
    Map<String, Object> result = map;
    return result;
}

}

Внедрив ResourceServerTokenServices, вы можете изменить запрос, отправляемый resource server в auth server, для аутентификации и авторизации.

настройка на сервере аутентификации

Переопределить контроллер безопасности spring. Под перезапуском я подразумеваю создание custom controller, чтобы запрос на oauth/check_token обрабатывался вашим пользовательским контроллером, а не контроллером, определенным пружиной.

@RestController
public class CustomCheckTokenEndpoint {

private ResourceServerTokenServices resourceServerTokenServices;

private AccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter();

protected final Log logger = LogFactory.getLog(getClass());

private WebResponseExceptionTranslator exceptionTranslator = new DefaultWebResponseExceptionTranslator();

@Autowired
KeyHitManager keyHitManager;

public CustomCheckTokenEndpoint(ResourceServerTokenServices resourceServerTokenServices) {
    this.resourceServerTokenServices = resourceServerTokenServices;
}

/**
 * @param exceptionTranslator
 *            the exception translator to set
 */
public void setExceptionTranslator(WebResponseExceptionTranslator exceptionTranslator) {
    this.exceptionTranslator = exceptionTranslator;
}

/**
 * @param accessTokenConverter
 *            the accessTokenConverter to set
 */
public void setAccessTokenConverter(AccessTokenConverter accessTokenConverter) {
    this.accessTokenConverter = accessTokenConverter;
}

@RequestMapping(value = "/oauth/check_token")
@ResponseBody
public Map<String, ?> customCheckToken(@RequestParam("token") String value, @RequestParam("api") int api) {

    OAuth2AccessToken token = resourceServerTokenServices.readAccessToken(value);
    if (token == null) {
        throw new InvalidTokenException("Token was not recognised");
    }

    if (token.isExpired()) {
        throw new InvalidTokenException("Token has expired");
    }

    OAuth2Authentication authentication = resourceServerTokenServices.loadAuthentication(token.getValue());

    Map<String, ?> response = accessTokenConverter.convertAccessToken(token, authentication);

    String clientId = (String) response.get("client_id");
    if (!keyHitManager.isHitAvailble(api,clientId)) {
        throw new InvalidTokenException(
                "Services for this key has been suspended due to daily/hourly transactions limit");
    }

    return response;
}

@ExceptionHandler(InvalidTokenException.class)
public ResponseEntity<OAuth2Exception> handleException(Exception e) throws Exception {
    logger.info("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage());
    // This isn't an oauth resource, so we don't want to send an
    // unauthorized code here. The client has already authenticated
    // successfully with basic auth and should just
    // get back the invalid token error.
    @SuppressWarnings("serial")
    InvalidTokenException e400 = new InvalidTokenException(e.getMessage()) {
        @Override
        public int getHttpErrorCode() {
            return 400;
        }
    };
    return exceptionTranslator.translate(e400);
}
}
person truekiller    schedule 22.03.2018
comment
Привет, спасибо за решение, но я не могу инициализировать ResourceServerTokenServices в конструкторе CustomCheckTokenEndpoint. Вы можете помочь в этом? - person Ashish Malhotra; 07.06.2018
comment
@AshishMalhotra Создайте компонент для TokenService. Я добавил DefaultTokenServices в какой-то другой файл конфигурации. - person truekiller; 07.06.2018
comment
@AshishMalhotra поддержите этот ответ, если он вам помог. - person truekiller; 07.06.2018
comment
Как вы получили контекст для инициализации строки: HttpServletRequest request = Context.getCurrentInstance().getRequest();? У меня нет ссылки на контекст при использовании вашего решения - person Luke Davidson; 27.05.2021
comment
@LukeDavidson Если вы реализуете ResourceServerTokenServices, вы получите контекст из родительского класса. Прошло более трех лет. У меня сейчас нет этого кода. Извиняюсь. - person truekiller; 27.05.2021