Аутентификация на REST-сервисе с помощью Spring Security

Содержание

  • ссылка: #overview[ 1. Обзор]

  • ссылка: # цель[ 2. Цель]

  • ссылка: #client[ 3. Клиент]

  • ссылка: #testing[ 4. Тестирование службы аутентификации]

  • ссылка: # заключение[ 5. заключение]

1. Обзор

Эта статья посвящена тому, как проходить аутентификацию на основе безопасного REST API , который предоставляет RESTful User Account и службу аутентификации.

2. Цель

Во-первых, давайте рассмотрим участников - типичное приложение с поддержкой Spring Security должно проходить проверку подлинности на основе чего-либо - что-то может быть:

  • база данных

  • LDAP

  • ОТДЫХ

База данных является наиболее распространенным сценарием; однако служба RESTful UAA (учетная запись пользователя и аутентификация) может работать так же хорошо

Для целей данной статьи служба REST UAA предоставит одну операцию GET для /authentication , которая вернет основную информацию , необходимую Spring Security для выполнения процесса полной аутентификации.

3. Клиент

Как правило, простое приложение с поддержкой Spring Security будет использовать простой пользовательский сервис в качестве источника аутентификации:

<authentication-manager alias="authenticationManager">
    <authentication-provider user-service-ref="customUserDetailsService"/>
</authentication-manager>

Это позволит реализовать org.springframework.security.core.userdetails.UserDetailsService и будет возвращать принципала на основе предоставленного username :

@Component
public class CustomUserDetailsService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) {
      ...
    }
}

Когда клиент проходит проверку подлинности с помощью службы RESTful UAA, работы только с username больше не будет достаточно - клиенту теперь нужны полные учетные данные - и username , и password - при отправке запроса на проверку подлинности службе. Это имеет смысл, так как сама служба защищена, поэтому для правильной обработки запрос должен содержать учетные данные аутентификации.

С точки зрения Spring Security это не может быть сделано изнутри loadUserByUsername , поскольку password больше не доступен на этом этапе - нам нужно быстрее взять под контроль процесс аутентификации .

Мы можем сделать это, предоставив провайдера полной аутентификации Spring Security:

<authentication-manager alias="authenticationManager">
    <authentication-provider ref="restAuthenticationProvider"/>
</authentication-manager>

Переопределение всего провайдера аутентификации дает нам гораздо больше свободы для выполнения пользовательского извлечения Принципала из Сервиса, но это действительно сопряжено со значительной сложностью. Стандартный поставщик аутентификации - DaoAuthenticationProvider - имеет большую часть того, что нам нужно, поэтому хорошим подходом было бы просто расширить его и изменить только то, что необходимо.

К сожалению, это невозможно, так как retrieveUser - метод, который мы хотели бы расширить - это final . Это несколько не интуитивно понятно (здесь есть a JIRA, где обсуждается проблема ) - похоже, что задуманное здесь - просто предоставить альтернативную реализацию, которая не идеальна, но не является серьезной проблемой - наш RestAuthenticationProvider копирует и вставляет большую часть реализации DaoAuthenticationProvider и переписывает то, что ему нужно - получение принципала из службы:

@Override
protected UserDetails retrieveUser(String name, UsernamePasswordAuthenticationToken auth){
    String password = auth.getCredentials().toString();
    UserDetails loadedUser = null;
    try {
        ResponseEntity<Principal> authenticationResponse =
            authenticationApi.authenticate(name, password);
        if (authenticationResponse.getStatusCode().value() == 401) {
            return new User("wrongUsername", "wrongPass",
                Lists.<GrantedAuthority> newArrayList());
        }
        Principal principalFromRest = authenticationResponse.getBody();
        Set<String> privilegesFromRest = Sets.newHashSet();
       //fill in the privilegesFromRest from the Principal
        String[]authoritiesAsArray =
            privilegesFromRest.toArray(new String[privilegesFromRest.size()]);
        List<GrantedAuthority> authorities =
            AuthorityUtils.createAuthorityList(authoritiesAsArray);
        loadedUser = new User(name, password, true, authorities);
    } catch (Exception ex) {
        throw new AuthenticationServiceException(repositoryProblem.getMessage(), ex);
    }
    return loadedUser;
}

Давайте начнем с самого начала - HTTP-связь со службой REST - это обрабатывается authenticationApi - простым API, обеспечивающим операцию authenticate для фактической службы. Сама операция может быть реализована с помощью любой библиотеки, поддерживающей HTTP - в этом случае реализация использует RestTemplate__:

public ResponseEntity<Principal> authenticate(String username, String pass) {
   HttpEntity<Principal> entity = new HttpEntity<Principal>(createHeaders(username, pass))
   return restTemplate.exchange(authenticationUri, HttpMethod.GET, entity, Principal.class);
}

HttpHeaders createHeaders(String email, String password) {
    HttpHeaders acceptHeaders = new HttpHeaders() {
        {
            set(com.google.common.net.HttpHeaders.ACCEPT,
                MediaType.APPLICATION__JSON.toString());
        }
    };
    String authorization = username + ":" + password;
    String basic = new String(Base64.encodeBase64
        (authorization.getBytes(Charset.forName("US-ASCII"))));
    acceptHeaders.set("Authorization", "Basic " + basic);

    return acceptHeaders;
}

FactoryBean может использоваться для связи:/how-to-use-resttemplate-with-basic-authentication-in-spring # resttemplate[установить шаблон RestTemplate в контексте].

  • Далее ** , если запрос аутентификации привел к HTTP 401 Unauthorized , скорее всего, из-за неправильных учетных данных от клиента, участник с неправильными учетными данными возвращается, чтобы процесс аутентификации Spring Security мог отклонить их:

return new User("wrongUsername", "wrongPass", Lists.<GrantedAuthority> newArrayList());

Наконец, Spring Security Principal нуждается в некоторых полномочиях - привилегиях, которые этот конкретный участник будет иметь и использовать локально после процесса аутентификации. Операция/ authenticate получила полный принципал, включая привилегии, поэтому их необходимо извлечь из результата запроса и преобразовать в объекты GrantedAuthority , как того требует Spring Security.

Детали того, как хранятся эти привилегии, здесь не имеют значения - они могут храниться как простые строки или как сложная структура Role-Privilege - но независимо от деталей нам нужно использовать только их имена для создания объектов GrantedAuthoritiy . После создания окончательного участника Spring Security он возвращается к стандартному процессу аутентификации:

List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList(authoritiesAsArray);
loadedUser = new User(name, password, true, authorities);

4. Тестирование службы аутентификации

Написание интеграционного теста, который использует службу REST аутентификации на happy-path, достаточно просто:

@Test
public void whenAuthenticating__then200IsReceived() {
   //When
    ResponseEntity<Principal> response
        = authenticationRestTemplate.authenticate("admin", "adminPass");

   //Then
    assertThat(response.getStatusCode().value(), is(200));
}

После этого простого теста могут быть также реализованы более сложные интеграционные тесты - однако это выходит за рамки этого поста.

5. Заключение

В этой статье объясняется, как выполнять аутентификацию с помощью REST API, а не на локальной системе, такой как база данных.

Для полной реализации защищенного сервиса RESTful, который можно использовать в качестве поставщика аутентификации, обратитесь к проекту GitHub .

Related