«1. Обзор
Безопасность — это первостепенная задача в экосистеме Spring. Поэтому неудивительно, что OAuth2 может работать с Spring Web MVC практически без настройки.
Однако нативное решение Spring — не единственный способ реализовать уровень представления. Jersey, реализация, совместимая с JAX-RS, также может работать в тандеме со Spring OAuth2.
В этом руководстве мы узнаем, как защитить приложение на Джерси с помощью Spring Social Login, реализованного с использованием стандарта OAuth2.
2. Зависимости Maven
Давайте добавим артефакт spring-boot-starter-jersey для интеграции Jersey в приложение Spring Boot:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jersey</artifactId>
</dependency>
Для настройки безопасности OAuth2 нам понадобится spring-boot-starter-security и spring-security-oauth2-client:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-client</artifactId>
</dependency>
Мы будем управлять всеми этими зависимостями, используя Spring Boot Starter Parent версии 2.
3. Уровень представления Джерси
Нам понадобится класс ресурсов с пара конечных точек для использования Джерси в качестве уровня представления.
3.1. Класс ресурсов
Вот класс, который содержит определения конечных точек:
@Path("/")
public class JerseyResource {
// endpoint definitions
}
Сам класс очень прост — он имеет только аннотацию @Path. Значение этой аннотации определяет базовый путь для всех конечных точек в теле класса.
Возможно, стоит упомянуть, что этот класс ресурсов не содержит стереотипной аннотации для сканирования компонентов. На самом деле, это даже не обязательно должен быть bean-компонент Spring. Причина в том, что мы не полагаемся на Spring для обработки сопоставления запросов.
3.2. Страница входа
Вот метод, который обрабатывает запросы на вход:
@GET
@Path("login")
@Produces(MediaType.TEXT_HTML)
public String login() {
return "Log in with <a href=\"/oauth2/authorization/github\">GitHub</a>";
}
Этот метод возвращает строку для запросов GET, которые нацелены на конечную точку /login. Тип содержимого text/html указывает браузеру пользователя отображать ответ со ссылкой, по которой можно щелкнуть.
Мы будем использовать GitHub в качестве провайдера OAuth2, поэтому ссылка /oauth2/authorization/github. Эта ссылка вызовет перенаправление на страницу авторизации GitHub.
3.3. Домашняя страница
Давайте определим другой метод для обработки запросов к корневому пути:
@GET
@Produces(MediaType.TEXT_PLAIN)
public String home(@Context SecurityContext securityContext) {
OAuth2AuthenticationToken authenticationToken = (OAuth2AuthenticationToken) securityContext.getUserPrincipal();
OAuth2AuthenticatedPrincipal authenticatedPrincipal = authenticationToken.getPrincipal();
String userName = authenticatedPrincipal.getAttribute("login");
return "Hello " + userName;
}
Этот метод возвращает домашнюю страницу, которая представляет собой строку, содержащую имя пользователя, вошедшего в систему. Обратите внимание, в данном случае мы извлекли имя пользователя из атрибута входа. Однако другой поставщик OAuth2 может использовать другой атрибут для имени пользователя.
Очевидно, что описанный выше метод работает только для аутентифицированных запросов. Если запрос не прошел проверку подлинности, он будет перенаправлен на конечную точку входа. Мы увидим, как настроить это перенаправление в разделе 4.
3.4. Регистрация Джерси в контейнере Spring
Давайте зарегистрируем класс ресурсов в контейнере сервлетов, чтобы включить службы Джерси. К счастью, это довольно просто:
@Component
public class RestConfig extends ResourceConfig {
public RestConfig() {
register(JerseyResource.class);
}
}
Регистрируя JerseyResource в подклассе ResourceConfig, мы сообщаем контейнеру сервлетов обо всех конечных точках в этом классе ресурсов.
Последний шаг — зарегистрировать подкласс ResourceConfig, в данном случае RestConfig, в контейнере Spring. Мы реализовали эту регистрацию с помощью аннотации @Component.
4. Настройка безопасности Spring
Мы можем настроить безопасность для Джерси так же, как для обычного приложения Spring:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/login")
.permitAll()
.anyRequest()
.authenticated()
.and()
.oauth2Login()
.loginPage("/login");
}
}
Самый важный метод в данной цепочке — это oauth2Login. Этот метод настраивает поддержку проверки подлинности с помощью поставщика OAuth 2.0. В этом руководстве поставщиком является GitHub.
Еще одна заметная конфигурация — страница входа. Предоставляя строку «/login» методу loginPage, мы указываем Spring перенаправлять неаутентифицированные запросы на конечную точку /login.
Обратите внимание, что конфигурация безопасности по умолчанию также предоставляет автоматически сгенерированную страницу в /login. Следовательно, даже если бы мы не настроили страницу входа, неаутентифицированный запрос все равно был бы перенаправлен на эту конечную точку.
Разница между конфигурацией по умолчанию и явной настройкой заключается в том, что в случае по умолчанию приложение возвращает сгенерированную страницу, а не нашу пользовательскую строку.
5. Конфигурация приложения
«Чтобы иметь приложение, защищенное OAuth2, нам нужно зарегистрировать клиент у поставщика OAuth2. После этого добавьте учетные данные клиента в приложение.
5.1. Регистрация клиента OAuth2
Давайте начнем процесс регистрации с регистрации приложения GitHub. После перехода на страницу разработчика GitHub нажмите кнопку «Новое приложение OAuth», чтобы открыть форму «Зарегистрировать новое приложение OAuth».
Далее заполните отображаемую форму соответствующими значениями. В качестве имени приложения введите любую строку, которая сделает приложение узнаваемым. URL-адрес домашней страницы может быть http://localhost:8083, а URL-адрес обратного вызова авторизации — http://localhost:8083/login/oauth2/code/github.
URL-адрес обратного вызова — это путь, на который перенаправляется браузер после того, как пользователь аутентифицируется с помощью GitHub и предоставляет доступ к приложению.
Вот как может выглядеть регистрационная форма:
Теперь нажмите на кнопку Зарегистрировать приложение. Затем браузер должен перенаправить на домашнюю страницу приложения GitHub, которая показывает идентификатор клиента и секрет клиента.
5.2. Настройка приложения Spring Boot
Давайте добавим файл свойств с именем jersey-application.properties в путь к классам:
Не забудьте заменить заполнители \u003cyour-client-id\u003e и \u003cyour-client-secret\u003e со значениями из нашего собственного приложения GitHub.
server.port=8083
spring.security.oauth2.client.registration.github.client-id=<your-client-id>
spring.security.oauth2.client.registration.github.client-secret=<your-client-secret>
Наконец, добавьте этот файл в качестве источника свойств в приложение Spring Boot:
6. Аутентификация в действии
@SpringBootApplication
@PropertySource("classpath:jersey-application.properties")
public class JerseyApplication {
public static void main(String[] args) {
SpringApplication.run(JerseyApplication.class, args);
}
}
Давайте посмотрим, как мы можем войти в наше приложение после регистрации на GitHub.
6.1. Доступ к приложению
Давайте запустим приложение, затем зайдем на домашнюю страницу по адресу localhost:8083. Поскольку запрос не аутентифицирован, мы будем перенаправлены на страницу входа:
Теперь, когда мы нажмем ссылку GitHub, браузер перенаправит на страницу авторизации GitHub:
Глядя на URL-адрес, мы видим, что перенаправленный запрос содержал много параметров запроса, таких как response_type, client_id и scope:
Значением response_type является код, что означает, что тип гранта OAuth2 является кодом авторизации. Между тем, параметр client_id помогает идентифицировать наше приложение. Чтобы узнать о значениях всех параметров, перейдите на страницу разработчика GitHub.
Когда появится страница авторизации, нам нужно авторизовать приложение, чтобы продолжить. После успешной авторизации браузер перенаправит на предопределенную конечную точку в нашем приложении вместе с несколькими параметрами запроса:
За кулисами приложение затем обменяет код авторизации на токен доступа. После этого он использует этот токен для получения информации о вошедшем в систему пользователе.
https://github.com/login/oauth/authorize?response_type=code&client_id=c30a16c45a9640771af5&scope=read:user&state=dpTme3pB87wA7AZ--XfVRWSkuHD3WIc9Pvn17yeqw38%3D&redirect_uri=http://localhost:8083/login/oauth2/code/github
После возврата запроса к localhost:8083/login/oauth2/code/github браузер возвращается на домашнюю страницу. На этот раз мы должны увидеть приветственное сообщение с нашим собственным именем пользователя:
6.2. Как получить имя пользователя?
http://localhost:8083/login/oauth2/code/github?code=561d99681feeb5d2edd7&state=dpTme3pB87wA7AZ--XfVRWSkuHD3WIc9Pvn17yeqw38%3D
Понятно, что имя пользователя в приветственном сообщении является нашим именем пользователя GitHub. В этот момент может возникнуть вопрос: как мы можем получить имя пользователя и другую информацию от аутентифицированного пользователя?
В нашем примере мы извлекли имя пользователя из атрибута входа. Однако это не одинаково для всех поставщиков OAuth2. Другими словами, провайдер может предоставлять данные в определенных атрибутах по своему усмотрению. Поэтому можно сказать, что стандартов в этом плане просто нет.
В случае с GitHub мы можем найти нужные нам атрибуты в справочной документации. Точно так же другие поставщики OAuth2 предоставляют свои собственные ссылки.
Другое решение состоит в том, что мы можем запустить приложение в режиме отладки и установить точку останова после создания объекта OAuth2AuthenticatedPrincipal. При просмотре всех атрибутов этого объекта мы получим информацию о пользователе.
7. Тестирование
Давайте напишем несколько тестов для проверки поведения приложения.
7.1. Настройка среды
Вот класс, который будет содержать наши тестовые методы:
«
«Вместо использования реального идентификатора клиента GitHub мы определили тестовый идентификатор для клиента OAuth2. Затем этот идентификатор устанавливается в свойство spring.security.oauth2.client.registration.github.client-id.
Все аннотации в этом тестовом классе являются общими для тестирования Spring Boot, поэтому мы не будем рассматривать их в этом руководстве. Если какая-либо из этих аннотаций неясна, перейдите к разделу Тестирование в Spring Boot, Интеграционное тестирование в Spring или Изучение Spring Boot TestRestTemplate.
7.2. Домашняя страница
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
@TestPropertySource(properties = "spring.security.oauth2.client.registration.github.client-id:test-id")
public class JerseyResourceUnitTest {
@Autowired
private TestRestTemplate restTemplate;
@LocalServerPort
private int port;
private String basePath;
@Before
public void setup() {
basePath = "http://localhost:" + port + "/";
}
// test methods
}
Мы докажем, что когда неаутентифицированный пользователь пытается получить доступ к домашней странице, он будет перенаправлен на страницу входа для аутентификации:
7.3. Страница входа
Давайте проверим, что доступ к странице входа приведет к возврату пути авторизации:
7.4. Конечная точка авторизации
@Test
public void whenUserIsUnauthenticated_thenTheyAreRedirectedToLoginPage() {
ResponseEntity<Object> response = restTemplate.getForEntity(basePath, Object.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FOUND);
assertThat(response.getBody()).isNull();
URI redirectLocation = response.getHeaders().getLocation();
assertThat(redirectLocation).isNotNull();
assertThat(redirectLocation.toString()).isEqualTo(basePath + "login");
}
Наконец, при отправке запроса на конечную точку авторизации браузер перенаправит на страницу авторизации провайдера OAuth2 с соответствующими параметрами:
8. Заключение
@Test
public void whenUserAttemptsToLogin_thenAuthorizationPathIsReturned() {
ResponseEntity response = restTemplate.getForEntity(basePath + "login", String.class);
assertThat(response.getHeaders().getContentType()).isEqualTo(TEXT_HTML);
assertThat(response.getBody()).isEqualTo("Log in with <a href="\"/oauth2/authorization/github\"">GitHub</a>");
}
В этом руководстве мы настроили Spring Social Login с приложением на Джерси. В учебник также включены шаги по регистрации приложения у поставщика GitHub OAuth2.
Полный исходный код можно найти на GitHub.
@Test
public void whenUserAccessesAuthorizationEndpoint_thenTheyAresRedirectedToProvider() {
ResponseEntity response = restTemplate.getForEntity(basePath + "oauth2/authorization/github", String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FOUND);
assertThat(response.getBody()).isNull();
URI redirectLocation = response.getHeaders().getLocation();
assertThat(redirectLocation).isNotNull();
assertThat(redirectLocation.getHost()).isEqualTo("github.com");
assertThat(redirectLocation.getPath()).isEqualTo("/login/oauth/authorize");
String redirectionQuery = redirectLocation.getQuery();
assertThat(redirectionQuery.contains("response_type=code"));
assertThat(redirectionQuery.contains("client_id=test-id"));
assertThat(redirectionQuery.contains("scope=read:user"));
}
«
In this tutorial, we have set up Spring Social Login with a Jersey application. The tutorial also included steps for registering an application with the GitHub OAuth2 provider.
The complete source code can be found over on GitHub.