Spring Authorization Server: плюсы, минусы, как использовать

  • 3 года назад
  • читать 9 мин.

Недавно вышел очередной релиз 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 :       

  • The OAuth 2.1 Authorization Framework
    • Authorization Grant
      • Authorization Code
      • Client Credentials
      • Refresh Token
    • Client Authentication
      • HTTP Basic
      • HTTP POST
    • JSON Web Token (JWT)
      • private_key_jwt
      • client_secret_jwt
    • User Consent
      • Authorization Code Grant
    • Proof Key for Code Exchange by OAuth Public Clients (PKCE)
    • OAuth 2.0 Token Revocation
    • OAuth 2.0 Token Introspection
    • OAuth 2.0 Authorization Server Metadata
    • JSON Web Token (JWT)
    • JSON Web Signature (JWS)
    • JSON Web Key (JWK)
    • OpenID Connect Core 1.0
      • Authorization Code Flow
      • UserInfo Endpoint
    • OpenID Connect Discovery 1.0
      • Provider Configuration Endpoint
    • OpenID Connect Dynamic Client Registration 1.0
      • Client Registration Endpoint
      • Client Configuration Endpoint

 

С помощью 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.

 Плюсы:

  1. Простота: настройка схожа с остальными проектами Spring Security, что сильно упрощает настройку сервера авторизации.
  2. Гибкость, скорость и легковесность: настройки легко меняются под наши нужды. Скромные размеры и высокая скорость в сравнении с облачными решениями.
  3. Spring: развитое комьюнити, и, как итог, множество статей и примеров.

 

Минусы:

  1. Ограниченный функционал: на текущее время он не такой широкий, как у других identity provider-ов: KeyCloack, Okta, AWS Cognito и т.д.
  2. Отсутствие графического интерфейса для администрирования.
  3. Spring: по причине того, что это проект команды Spring, информации будет связана с экосистемой Spring, а найти информацию о настройке клиента, написанную на другом фреймворке, достаточно сложно.

 

Вывод:

           Spring Authorization Server – это решение авторизации и аутентификации для небольших проектов с любой архитектурой: монолиты, микр сервисы, который можно гибко настроить под собственные нужны. Из минусов стоит отметить, что нет графического интерфейса для администрирования, настройки происходят непосредственно в коде.

В крупных проектах стоит отдать предпочтение типовым решения, таким как KeyCloack, Okta, AWS Cognito и другим.

 

Ссылки :

  1. Authorization Server
  2. Resource server
  3. The Spring Authorization Server

Автор статьи- Дмитрий Дулик.

title

content