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 .