Статьи

Spring Oauth2 с образцом JWT

Иногда назад мы публиковали одну статью, в которой описан индивидуальный подход к реализации сеанса без сохранения состояния в облачной среде. Сегодня давайте рассмотрим еще один популярный пример использования аутентификации Oauth2 для приложения Spring Boot. В этом примере мы будем использовать JSON Web Token (JWT) в качестве формата токена Oauth2.

Этот образец был разработан частично на основе официального образца Spring Security Oauth 2. Однако мы сосредоточимся на понимании принципа запроса Oauth 2.

Исходный код находится по адресу https://github.com/tuanngda/spring-boot-oauth2-demo.git.

Фон

Oauth2 и JWT

Мы не будем вдаваться в подробности, когда вы захотите использовать Oauth2 и JWT. В общем, вы можете принять Oauth, если вам нужно разрешить другим людям создавать приложение для ваших служб. Мы ориентируемся на Oauth2 и JWT, потому что они являются самой популярной средой аутентификации и протоколом на рынке.

Spring Security Oauth 2

Spring Security Oauth2 — это реализация Oauth 2, построенная на основе Spring Security, которая является очень расширяемой средой аутентификации.

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

Поскольку в этом примере наше приложение предоставляет и использует токен, Spring Security Oauth 2 не должен быть единственным уровнем аутентификации для приложения. Нам нужен другой механизм аутентификации для защиты конечной точки поставщика токена.

Для кластерной среды токен или секрет для подписи токена (для JWT) предполагается сохраненным, но мы пропустили этот шаг, чтобы упростить пример. Точно так же аутентификация пользователя и идентификационные данные клиента жестко закодированы.

Системный дизайн

обзор

В нашем приложении нам нужно настроить 3 компонента

  • Конечная точка авторизации и конечная точка токена, чтобы помочь предоставить токен Oauth 2.
  • WebSecurityConfigurerAdapter, который является уровнем аутентификации с жестко заданным порядком 3 (согласно Дейву Сайеру ). Этот уровень аутентификации устанавливает аутентификацию и принципал для любого запроса, который содержит токен Oauth 2.
  • Другой механизм аутентификации для защиты конечной точки токена и других ресурсов, если токен отсутствует. В этом примере мы выбираем базовую аутентификацию для ее простоты при написании тестов. Так как мы не указываем порядок, он будет принимать значение по умолчанию 100. С безопасностью Spring, более низкий порядок, более высокий приоритет; поэтому мы должны ожидать, что Oauth 2 появится до базовой аутентификации в FilterChainProxy. Проверка в IDE докажет, что наша установка верна.

FilterChain

На изображении выше Oauth2AuthenticationProcessingFilter отображается перед BasicAuthenticationFilter.

Настройка сервера авторизации

Вот наш конфиг для авторизации и конечной точки токена

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
 
    @Value("${resource.id:spring-boot-application}")
    private String resourceId;
     
    @Value("${access_token.validity_period:3600}")
    int accessTokenValiditySeconds = 3600;
 
    @Autowired
    private AuthenticationManager authenticationManager;
     
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        return new JwtAccessTokenConverter();
    }
 
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
            .authenticationManager(this.authenticationManager)
            .accessTokenConverter(accessTokenConverter());
    }
     
    @Override
    public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
        oauthServer.tokenKeyAccess("isAnonymous() || hasAuthority('ROLE_TRUSTED_CLIENT')")
            .checkTokenAccess("hasAuthority('ROLE_TRUSTED_CLIENT')");
    }
 
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
            .withClient("normal-app")
                .authorizedGrantTypes("authorization_code", "implicit")
                .authorities("ROLE_CLIENT")
                .scopes("read", "write")
                .resourceIds(resourceId)
                .accessTokenValiditySeconds(accessTokenValiditySeconds)
        .and()
            .withClient("trusted-app")
                .authorizedGrantTypes("client_credentials", "password")
                .authorities("ROLE_TRUSTED_CLIENT")
                .scopes("read", "write")
                .resourceIds(resourceId)
                .accessTokenValiditySeconds(accessTokenValiditySeconds)
                .secret("secret");
    }
}

Есть несколько вещей, на которые стоит обратить внимание в этой реализации.

  • Настроить токен JWT так же просто, как использовать JwtAccessTokenConverter. Поскольку мы никогда не настраиваем ключ подписи, он генерируется случайным образом. Если мы намеревались развернуть наше приложение в облаке, необходимо синхронизировать ключ подписи на всех серверах авторизации.
  • Вместо того, чтобы создавать менеджер аутентификации, мы решили добавить существующий менеджер аутентификации из контейнера Spring. На этом шаге мы можем поделиться менеджером аутентификации с фильтром базовой аутентификации.
  • Возможно иметь доверенное приложение и недоверенное приложение. Доверенное приложение может иметь собственный секрет. Это необходимо для предоставления полномочий на авторизацию клиента. За исключением учетных данных клиента, для всех трех других разрешений требуются учетные данные владельца ресурса.
  • Мы разрешаем анонимно проверять конечную точку токена. В этой конфигурации проверочный токен доступен без базовой аутентификации или токена Oauth 2.

Конфигурация сервера ресурсов

Вот наша конфигурация для конфигурации сервера ресурсов

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
     
    @Value("${resource.id:spring-boot-application}")
    private String resourceId;
     
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.resourceId(resourceId);
    }
 
    @Override
    public void configure(HttpSecurity http) throws Exception {
         http.requestMatcher(new OAuthRequestedMatcher())
                .authorizeRequests()
                 .antMatchers(HttpMethod.OPTIONS).permitAll()
                    .anyRequest().authenticated();
    }
     
    private static class OAuthRequestedMatcher implements RequestMatcher {
        public boolean matches(HttpServletRequest request) {
            String auth = request.getHeader("Authorization");
            // Determine if the client request contained an OAuth Authorization
            boolean haveOauth2Token = (auth != null) && auth.startsWith("Bearer");
            boolean haveAccessToken = request.getParameter("access_token")!=null;
   return haveOauth2Token || haveAccessToken;
        }
    }
 
}

Вот несколько вещей, чтобы принять к сведению:

  • Добавлен OAuthRequestedMatcher, чтобы фильтр Oauth обрабатывал только запросы Oauth2. Мы добавили это, чтобы неавторизованный запрос был отклонен на уровне базовой аутентификации вместо уровня Oauth 2. Это может не иметь никакого значения с точки зрения функциональности, но мы добавили это для удобства использования. Для клиента они получат 401 HTTP Status с этим новым заголовком по сравнению со старым заголовком:
    • WWW-Authenticate: базовая область = «область»
    • WWW-Authenticate: Bearer realm = «spring-boot-application», error = «unauthorized», error_description = «Для доступа к этому ресурсу требуется полная аутентификация»
  • С новым заголовком ответа браузер автоматически запросит у пользователя имя пользователя и пароль. Если вы не хотите, чтобы ресурс был доступен любому другому механизму аутентификации, этот шаг не требуется.
  • Некоторым браузерам, таким как Chrome, нравится отправлять запрос OPTIONS на поиск CORS перед вызовом AJAX. Поэтому лучше всегда разрешать запросы OPTIONS.

Базовая настройка безопасности аутентификации

Как упоминалось ранее, потому что мы должны защитить конечную точку поставщика токенов.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
     
    @Autowired
    public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("user").password("password").roles("USER").and().withUser("admin")
                .password("password").roles("USER", "ADMIN");
    }
     
    @Override
    protected void configure(HttpSecurity http) throws Exception {
     http
        .authorizeRequests()
            .antMatchers(HttpMethod.OPTIONS).permitAll()
            .anyRequest().authenticated()
            .and().httpBasic()
            .and().csrf().disable();
    }
     
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

Есть несколько вещей, чтобы принять к сведению:

  • Мы предоставляем бин AuthenticationManager, чтобы наши два адаптера безопасности аутентификации могли совместно использовать один менеджер аутентификации.
  • Spring Security CSRF без проблем работает с JSP, но мешает RestAPI. Поскольку мы хотим, чтобы этот пример приложения использовался в качестве основы для пользователей при разработке собственного приложения, мы отключили CSRF и добавили фильтр CORS, чтобы его можно было сразу использовать.

тестирование

Мы написали один тестовый сценарий для каждого типа разрешения авторизации, точно следуя спецификациям Oauth2. Поскольку Spring Security Oauth 2 является реализацией, основанной на среде Spring Security, наш интерес направлен на то, чтобы увидеть, как создаются базовая аутентификация и принципал.

Прежде чем подвести итоги эксперимента, давайте кратко рассмотрим кое-что для заметок.

  • Большинство запросов к конечным точкам провайдера токенов были отправлены с использованием запросов POST, но они содержат учетные данные пользователя в качестве параметров. Несмотря на то, что мы помещаем эти учетные данные как часть URL для удобства, никогда не делайте этого в вашем клиенте Oauth 2.
  • Мы создали 2 конечные точки / resources / Principal и / resources / role, чтобы захватить принципала и полномочия для аутентификации Oauth 2.

Вот наша установка:

пользователь Тип Власти мандат
пользователь владелец ресурса ROLE_USER Y
админ владелец ресурса ROLE_ADMIN Y
нормально-приложение клиент ROLE_CLIENT N
доверенный-приложение клиент ROLE_TRUSTED_CLIENT Y

Тип гранта пользователь клиент принципал Власти
Код авторизации пользователь нормально-приложение пользователь ROLE_USER
Учетные данные клиента Не Доступно доверенный-приложение доверенный-приложение Нет полномочий
неявный пользователь нормально-приложение пользователь ROLE_USER
Учетные данные пароля владельца ресурса пользователь доверенный-приложение пользователь ROLE_USER

Этот результат довольно ожидаемый, за исключением учетных данных клиента. Интересно, что даже несмотря на то, что клиент получает токен Oauth 2 по учетным данным клиента, в утвержденном запросе по-прежнему отсутствуют какие-либо полномочия клиента, а только учетные данные клиента. Я думаю, что это имеет смысл, потому что токен из Implicit Grant Type нельзя использовать повторно. Вот что мы узнаем

Ссылка: Spring Oauth2 с JWT Образец от нашего партнера по JCG Нгуен Ань Туана в блоге для разработчиков .