Статьи

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

1. Обзор

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

2. Цель

Во-первых, давайте рассмотрим участников — типичное приложение с поддержкой Spring Security должно проходить проверку подлинности на основе чего-либо — что это может быть база данных, LDAP или служба REST. База данных является наиболее распространенным сценарием; однако служба RESTful UAA (учетная запись пользователя и аутентификация) может работать так же хорошо.

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

3. Клиент

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

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

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

1
2
3
4
5
6
7
@Component
public class CustomUserDetailsService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) {
      ...
    }
}

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

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

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

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

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@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, обеспечивающим операцию аутентификации для фактической службы. Сама операция может быть реализована с любой библиотекой, поддерживающей HTTP — в этом случае реализация использует RestTemplate :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
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 можно использовать для настройки шаблона RestTemplate в контексте .

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

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

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

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

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

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

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

1
2
3
4
5
6
7
8
9
@Test
public void whenAuthenticating_then200IsReceived() {
    // When
    ResponseEntity<Principal> response =
        authenticationRestTemplate.authenticate("admin", "adminPass");
 
    // Then
    assertThat(response.getStatusCode().value(), is(200));
}

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

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

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

Ссылка: Аутентификация на RESTсервисе с Spring Security от нашего партнера JCG Евгения Параскива в блоге baeldung .