Недавно вышел очередной релиз Spring Authorization Server (если быть точнее, то версия 0.2.3 вышла 24 марта 2022). Многих может смутить, что проект еще не добрался до заветных цифр «1.0.0» и до "релизной" версии может измениться до неузнаваемости, однако с середины прошлого года, а именно с версии 0.2.0, проект выведен из ряда экспериментальных проектов. Это означает, что глобальных изменений в структуре не будет, и Spring Authorization Server −“production ready”.
Главной причиной появления проекта Spring Authorization Server является необходимость замены Spring Security OAuth, для которого в январе 2018 анонсировали остановку работы по причине несовершенств протоколов безопасности и для унификации с другими проектами и документацией Spring Security. С начала 2018 Spring Security Oauth помечен как «deprecated», т.е. в него не будут добавлять новый функционал, и в нём будут исправляться только критические баги.
На март 2022 года в Spring Authorization Server реализован следующий функционал в версии 0.2.3 :
С помощью Spring Authorization Server реализую сервер для авторизации. Первым шагом будет создание Spring Boot проекта с минимальным набором зависимостей: Spring Web, Spring Security. Также необходимо добавить зависимость Spring Authorization Server в наш проект для пользователей maven-a:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
<version>0.2.3</version>
</dependency>
для Gradle (Kotlin):
implementation("org.springframework.security:spring-security-oauth2-authorization-server:0.2.3")
В конфигурационном классе создадим RegisteredClient. В этом случае это будет InMemoryRegisteredClientRepositor, так как проект ознакомительный.
Коротко пройдемся по настройкам. С помощью UUID получаем уникальный id, задаем имя нашему клиенту(clienId) и secret. Далее выбираем методы авторизации. Выбираем CLIENT_SECRET_POST, так как в теле POST запроса токена видны параметры и grand types. RedirectUri-ы - это uri OAuth Client-а, который в случае необходимости будет задействован при авторизации клиента.
Создаем страницу для логина с помощью authorizationServerSecurityFilterChain и cоздаем енд-поинт для проверки валидности нашего токена в providerSettings. В jwkSource() реализуется подпись токенов RSA ключами.
В jwtCustomizer добавляем информацию о роли пользователей в токен, в дальнейшем будем использовать роли для обеспечения разных уровней доступа. Создаем страницу для логина с помощью authorizationServerSecurityFilterChain и cоздаем енд-поинт для проверки валидности нашего токена в providerSettings. В jwkSource() реализована подпись токенов RSA ключами.
В jwtCustomizer добавляем информацию о роли пользователей в токен, в дальнейшем будем использовать роли для обеспечения разных уровней доступа.
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
SecurityFilterChain configureSecurityFilterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated())
.formLogin(Customizer.withDefaults())
.build();
}
@Bean
public UserDetailsService userDetailsService() {
PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
UserDetails admin = User.withUsername("admin")
.password(encoder.encode("admin"))
.roles("ADMIN")
.build();
UserDetails user = User.withUsername("user")
.password(encoder.encode("user"))
.roles("USER")
.build();
return new InMemoryUserDetailsManager(admin, user);
}
@Bean
OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() {
return context -> {
if (context.getTokenType() == OAuth2TokenType.ACCESS_TOKEN) {
Authentication principal = context.getPrincipal();
Set<String> authorities = principal.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toSet());
context.getClaims().claim("authorities", authorities);
}
};
}
}
Добавили реализацию WebSecurity, пользователя admin и user-a с ролями ADMIN и USER , соответственно, для дальнейшего тестирования.
Запускаем сервер для проверки работоспособности: для это сформируем запрос на получение кода авторизации (в запросе необходимо указать имя нашего клиента, тип ответа - code, scopes и ссылку для перехода).
http://localhost:8080/oauth2/authorize?client_id=es-client&response_type=code&scope=openid&redirect_uri=http://127.0.0.1:8090/authorized - итоговый запрос.
Переходим по ссылке выше в браузере, и после заполнения формы входа получаем код авторизации:
Время жизни одноразового кода авторизации ограничено (обычно 60 секунд), но не более 10 минут в соответствии со стандартом OAuth 2.0. Полученный код авторизации передаем в POST запрос для получения токена:
curl --location --request POST 'http://localhost:8080/oauth2/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=es-client' \
--data-urlencode 'client_secret=clientSecret' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'redirect_uri=http://127.0.0.1:8090/authorized' \
--data-urlencode 'code=7dinznFfaJ4MqLUoYuq5uSHg0EecSnRta84QTdp2T_0FIOWBPM4hEnHUCiit7CA589K12kK-hYOdPmZtXh1rUZ3DCm7eomygB5WzC8vXtNpzZ1OIqCCMCaoNgOkGm3fG'
В результате выполнения запроса выше получаем token-ы для входа и обновления, которые декодирую на jwt.io :
{
"access_token": "eyJraWQiOiIxNjFkZjQxNC1mZDdiLTQ5NDktYWE0NC1iMTM0N2YxYWVhYzEiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImF1ZCI6ImVzLWNsaWVudCIsIm5iZiI6MTY0ODE2MjA5MSwic2NvcGUiOlsib3BlbmlkIl0sImlzcyI6Imh0dHA6XC9cL2xvY2FsaG9zdDo4MDgwIiwiZXhwIjoxNjQ4MTYyMzkxLCJpYXQiOjE2NDgxNjIwOTEsImF1dGhvcml0aWVzIjpbIlJPTEVfQURNSU4iXX0.naXlDXscfZ34JpbqMkAktYdxD6_0RMjDCrOOdbiQmUUyA-xZuI222zQ93YefqG6yYNpbnhA5tXsrP90wgLaNP0LzRkDfwezeEs-GUxkM2aYBOU9Fx0nkdhkzS1fWfUnWV45t-v2VPKXRJhp9UfB-YI7RJXiX2wh9WKSQp4lVPY8c2fSunPAa-U_nBXo50IW7um8cO8007c5Y6CW6TzEri3UfYbp8sQW5QopYVlp8qRFj7L0MvgnJC5TaQS2miUNK1TLQWUx7Rjyr9JxgxL1Jj8rucQfW2ZdCziR1s4Q98BserKRW55sNM31_qb8IhUi_YAbZ9hCI5Z0Djev4G8Gn6w",
"refresh_token": "6uzRedNfrixXCL7rqfmxfwP8yELyKQRIyF_iNDxNNQKXT6eArPoAZOC7Yo9fiB4Rlp5LrXnNwZqbGIJjgSIz8-FaT5rK9f-CpG7j9x79QHKEtLmFYPZE5IVnh2fiyp_i",
"scope": "openid",
"id_token": "eyJraWQiOiIxNjFkZjQxNC1mZDdiLTQ5NDktYWE0NC1iMTM0N2YxYWVhYzEiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImF1ZCI6ImVzLWNsaWVudCIsImF6cCI6ImVzLWNsaWVudCIsImlzcyI6Imh0dHA6XC9cL2xvY2FsaG9zdDo4MDgwIiwiZXhwIjoxNjQ4MTYzODkxLCJpYXQiOjE2NDgxNjIwOTF9.Y2tIoAgzDML2WfnQbxEaisVbRkhfnth8u2QGJKjoJvlxxo1IcdMnB_2tGtSj4GdHQw271C8L6wjDQD94Nozw5moOdDnLRIs_cn-khHU7Nw3OykYxo366cTdGDoHr5iDTJE55vDqFp415_s1b2MF85mysgZqtw7V9QtSfPX_UgI_m9vWYAnglcLlV6Dfw8yEYBOkB8FrmsP0l9hYfo_J-wm_ac5ZJ4DeKTgNW3NzPsRs2aYMRNCas4SqWtx8EctKIF-MgVhP-hN44B9jTdbuTyt-Fbfv9kvpBYVHzNlZ_hDS63BZyCv3UUL-ZsclqAmzsd6OhbDHMWuegRL_svuRptg",
"token_type": "Bearer",
"expires_in": 299
}
Завершив настройку и проверку нашего Spring Authorization Server, давайте создадим сервис ресурсов для проверки работы нашего сервера авторизации и проверки работы с jwt-токенами.
Создаем Spring Boot проект со следующими зависимостями: Spring Web, Spring Security и OAuth2 Resource Server. После создания в application.properties необходимо указать порт и url нашего сервера авторизации:
server.port=8081
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8080
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:8080/oauth2/jwks
Последние поле опционально, но сервер поддерживает возможность предоставления JWT.
Настраиваем SecurityConfiguration: отключаем создание сессии, добавляем @EnableGlobalMethodSecurity(securedEnabled = true) - добавляет возможность ограничивать доступ на уровне методов. Конфигурируем oauth2ResourceServer с помощью jwt и добавляем SimpleAuthenticationConverter, в котором конвертируем роли из токена.
@EnableGlobalMethodSecurity(securedEnabled = true)
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
.anyRequest().authenticated().and()
.oauth2ResourceServer()
.jwt().jwtAuthenticationConverter(new SimpleAuthenticationConverter());
return http.build();
}
} public class SimpleAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {
private final Converter<Jwt, Collection<SimpleGrantedAuthority>> authoritiesConverter = new JwtRolesConverter();
@Override
public AbstractAuthenticationToken convert(@NonNull Jwt jwt) {
Collection<SimpleGrantedAuthority> authorities = authoritiesConverter.convert(jwt);
return new JwtAuthenticationToken(jwt, authorities);
}
static class JwtRolesConverter implements Converter<Jwt, Collection<SimpleGrantedAuthority>> {
@Override
public List<SimpleGrantedAuthority> convert(Jwt jwt) {
JSONArray roles = (JSONArray) jwt.getClaims().get("authorities");
return Optional.ofNullable(roles).stream()
.flatMap(Collection::stream)
.filter(Objects::nonNull)
.map(String.class::cast)
.map(role -> new SimpleGrantedAuthority(role.toUpperCase()))
.toList();
}
}
}
"authorities" – поле, которое добавили в SecurityConfiguration.jwtCustomizer. С помощью него конвертируем роли пользователей.
Создаем контроллер с двумя методами, которые предоставляют собой набор тестовых данных. Один доступен только для пользователей с ролью ADMIN, второй для пользователей с ролью USER.
@RestController
@RequestMapping(value = "api/v1/companies", produces = MediaType.APPLICATION_JSON_VALUE)
public class CompanyController {
@Secured("ROLE_USER")
@GetMapping
public ResponseEntity<List<String>> getCompanies() {
return ResponseEntity.ok(List.of("mend", "demand", "northern", "comp", "court"));
}
@Secured("ROLE_ADMIN")
@GetMapping("/admin")
public ResponseEntity<List<String>> getAdminCompanies() {
return ResponseEntity.ok(List.of("thumb", "every", "clear", "mystery", "however"));
}
}
Результаты выполнения запросов на наши ресурсы для пользователей с ролями ADMIN и USER:
Статус 403 Forbidden информирует о том, что у пользователя нет прав для выполнения запроса.
Коротко рассмотрим плюсы и минусы Spring Authorization Server.
Плюсы:
Минусы:
Вывод:
Spring Authorization Server – это решение авторизации и аутентификации для небольших проектов с любой архитектурой: монолиты, микр сервисы, который можно гибко настроить под собственные нужны. Из минусов стоит отметить, что нет графического интерфейса для администрирования, настройки происходят непосредственно в коде.
В крупных проектах стоит отдать предпочтение типовым решения, таким как KeyCloack, Okta, AWS Cognito и другим.
Ссылки :
Автор статьи- Дмитрий Дулик.