Долгая история Spring Security OAuth2

Spring Boot задняя часть
содержание

Настройка OAuth2 на основе Spring Boot — относительно простое дело, но когда вы видите связанные демонстрационные проекты, вы часто сбиваетесь с толку. Это потому, что я не понимаю базовых понятий, столкнувшись с кучей аннотаций и инъекций, я никак не могу понять, как между ними взаимосвязь и какие обязанности они несут.

В этой статье мы попытаемся разбить сложные проблемы на более простые, чтобы облегчить понимание, но эта статья немного затянулась. Если есть какие-либо ошибки, пожалуйста, свяжитесь с 27952278@qq.com!

замена сцены

Существует множество веб-сайтов, которые должны поддерживать стороннюю авторизацию (например, QQ, Weibo), например, посещениеjoin.thoughtworks.cnНажмите "Регистрация | Войти" в правом верхнем углу, всплывающее окно:

Всплывающее окно для входа в стороннее приложение
第三方登陆弹出框

Щелкните значок QQ, чтобы перейти на авторизованную страницу входа QQ:

Целевая страница авторизации QQ
QQ授权登陆页面

Введите имя пользователя и пароль QQ и после успешной проверки перейдите кjoin.thoughtworks.cnанкета, заполненная личными данными. Пока вход через QQjoin.thoughtworks.cnзавершенный.

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

В приведенном выше сценарии QQ является поставщиком услуг, аjoin.thoughtworks.cnявляется сторонним приложением.

Конечно, сценарии, в которых большинство людей могут получить доступ к OAuth, также являются вышеупомянутыми сценариями входа в QQ/Weibo/WeChat. Сценариев приложений, с которыми могут столкнуться программисты, больше. Например, в следующих сценариях OAuth можно использовать для решения проблемы проверки авторизации в системе:

простая структура системы
简单系统结构

В этой простой системе пользователи (например, посетители, администраторы) получают доступ к службе A и службе B через браузер, и служба A также может вызывать службу B. Относительно управления правами в этой системе:

  • Когда пользователи посещают A и B, если они не вошли в систему, им необходимо перейти на страницу входа.

  • Когда пользователь отправляет форму входа, запрос будет отправлен в службу входа.

  • После того, как пользователь успешно войдет в систему, служба A и служба B больше не будут переходить к интерфейсу входа в систему и будут решать, принять или отклонить их в соответствии с разрешениями.

  • Разные пользователи могут получить доступ к разным ресурсам в сервисе A и сервисе B.

  • Служба A должна иметь доступ только к некоторым ресурсам службы B.

Кажется ли это сложным, Spring Security OAuth2 может помочь решить проблему в этом сценарии, но проблема не решается в одночасье, необходимо анализировать и шаг за шагом решать некоторые небольшие проблемы.

Изолировать ресурсы и службы аутентификации

Для простоты мы различаем серверы ресурсов и серверы аутентификации:

  • Сервер аутентификации предоставляет интерфейс, который может генерировать зашифрованные токены на основе имен пользователей и паролей.

  • Сервер ресурсов предоставляет услуги для доступа пользователей, и пользователям необходимо иметь при себе токен, сгенерированный выше, при доступе.

  • Сервер аутентификации и сервер ресурсов не нуждаются в сетевом взаимодействии.

Разделив два шага, мы можем рассмотреть наш тестовый сценарий:

  • получить жетон

    curl -XPOST "http://资源服务器地址/oauth/token" -d "grant_type=password&username=reader&password=reader"
    
  • При доступе к серверу ресурсов с правильным токеном будет возвращено правильное сообщение.

    curl -H "Authorization: Bearer 获取到的TOKEN" "http://资源服务器地址/"
    
  • Доступ к серверу ресурсов с неправильным токеном не даст разрешения

    curl -H "Authorization: Bearer 任意错的TOKEN" "http://资源服务器地址/"
    

    или

    curl -H "http://资源服务器地址/"
    

Исходя из вышеизложенных соображений, мы возобновим два проекта, один из которых является сервером аутентификации, а другой — сервером ресурсов. Поскольку это первый пример, будет показано больше кода. Если вы не хотите видеть код, вы можете сразу перейти к разделу сводки.

Сервер аутентификации

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

  • Есть такая пара ключей (KeyA, KeyB), KeyA не равен KeyB

  • Шифрованный текст, зашифрованный ключом А, может быть расшифрован только ключом Б. Точно так же зашифрованный текст, зашифрованный ключом Б, может быть расшифрован только ключом А.

При асимметричном шифровании сервер ресурсов и сервер аутентификации могут взаимодействовать косвенно и безопасно:

  • Сервер аутентификации шифрует информацию о пользователе с помощью KeyA, чтобы сгенерировать зашифрованный токен и отправить его пользователю.

  • Сервер аутентификации публикует KeyB, а сервер ресурсов может скопировать и сохранить KeyB перед запуском.

  • Пользователь обращается к серверу ресурсов с токеном, и сервер ресурсов расшифровывает токен с помощью ключа B. Если расшифровка прошла успешно, можно подтвердить, что токен сгенерирован сервером аутентификации.

в Spring Security Frameworkspring-security-jwtЭто решение для асимметричного шифрования предоставляется, но перед кодированием нам необходимо сгенерировать пару асимметричных ключей.

  • Создайте файл jwt.jks для ключа хранения jwt

    keytool -genkeypair -alias jwt -keyalg RSA -dname "CN=jwt, L=Berlin, S=Berlin, C=DE" -keypass mySecretKey -keystore jwt.jks -storepass mySecretKey
    
  • Затем сгенерируйте пару ключей, обратите внимание на сохранение части открытого ключа сгенерированной пары ключей в файле public.cert для последующего распространения на сервер ресурсов для использования.

    keytool -list -rfc --keystore jwt.jks | openssl x509 -inform pem -pubkey
    
  • Скопируйте файлы jwt.jks и public.cert в папку проекта src/main/resources.

Следующим шагом является использование Spring Boot для создания приложения, а также настройки и внедрения параметров/классов, связанных с безопасностью:

  • Создайте запись для программы Spring Boot

    @SpringBootApplication
    public class JwtAuthServerApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(JwtAuthServerApplication.class, args);
        }
    }
    

    Также устанавливается в src/main/resources/application.propertiesserver.port=9999.

  • Добавьте конфигурацию веб-безопасности, чтобы она могла входить в систему по имени пользователя (читатель) и паролю (пароль), вам нужно использоватьWebSecurityConfigurerAdapterи аннотации@EnableWebSecurity

    @Configuration
    @EnableWebSecurity
    protected static class WebSecurityConfig extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.inMemoryAuthentication()
                    .withUser("reader")
                    .password("reader")
                    .authorities("FOO_READ", "ACTUATOR");
        }
    }
    
    @RestController
    protected static class HelloController {
        @RequestMapping("/hello")
        private String hello() {
            return "hello";
        }
    }
    

    После доступа к узлу приветствияcurl -u reader:reader "http://localhost:9999/hello", вы можете получить возвращаемый результат "hello". При доступе с неверным именем пользователя будет возвращена ошибка 401.

У пользователя есть две роли: FOO_READ и ACTUATOR. FOO_READ — это наша настраиваемая роль, а роль ACTUATOR используется для просмотра конфигурации и связанной статистики функций, например доступа к /mappings для просмотра всех сопоставлений маршрутов.

  • Для настройки приложения в качестве сервера аутентификации необходимо использоватьAuthorizationServerConfigurerAdapterи аннотации@EnableAuthorizationServer

    @Configuration
    @EnableAuthorizationServer
    protected static class OAuth2Configuration extends AuthorizationServerConfigurerAdapter {
    
        @Override
        public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
            super.configure(security);
        }
    
        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            super.configure(clients);
        }
    
        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            super.configure(endpoints);
        }
    }
    

    После перезапуска программы, если вы получаете доступhttp://localhost:9999/mappings, вы обнаружите, что было добавлено много узлов, связанных с oauth, например:

    /oauth/authorize
    /oauth/token
    /oauth/check_token
    /oauth/confirm_access
    /oauth/error
    

    Но на этот раз пытаюсь использовать командуcurl -u reader:reader "http://localhost:9999/oauth/token"Получить токен не удастся, запрос

    {
      "timestamp": 1507853421226,
      "status": 401,
      "error": "Unauthorized",
      "message": "Error creating bean with name 'scopedTarget.clientDetailsService' defined in org.springframework.security.oauth2.config.annotation.configuration.ClientDetailsServiceConfiguration: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.security.oauth2.provider.ClientDetailsService]: Factory method 'clientDetailsService' threw exception; nested exception is java.lang.UnsupportedOperationException: Cannot build client services (maybe use inMemory() or jdbc()).",
      "path": "/oauth/token"
    }
    

    Общая идея заключается в том, что создание экземпляра ClientDetailsService завершилось неудачно.

  • Настройте ClientDetailsService, просто переопределитеconfigure(ClientDetailsServiceConfigurer clients)реализация

    clients.inMemory()
            .withClient("web_app")
            .scopes("FOO")
            .authorities("FOO_READ")
            .authorizedGrantTypes("refresh_token", "password");
    

    inMemoryОзначает, что и client, и clientDetails хранятся в памяти.

    Следующим шагом является настройка клиента: clientId означает, что идентификатор клиента — «web_app», scope означает, что область приложения — «FOO», полномочия перечисляют роли авторизации, а authorGrantTypes указывает режим авторизации клиента.

    В этот момент, если вы посетитеcurl -XPOST "web_app:@localhost:9999/oauth/token" -d "grant_type=password&username=reader&password=reader"

    Будет отображена ошибка:

    {"error":"unsupported_grant_type","error_description":"Unsupported grant type: password"}
    

    Явно уже настроенauthorizedGrantTypes("refresh_token", "password"), но появляется эта ошибка, проверьте обработчик /auth/tokenисходный кодв настоящее время:

    OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
      if (token == null) {
          throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
      }
    

    Означает, что если сгенерированный токен пуст, будет выдано это исключение. Согласно предыдущему предположению, мы намерены использовать закрытый ключ пары асимметричных ключей для создания зашифрованного токена.

  • Прочитайте сгенерированный файл jwt.jks с закрытым ключом и сгенерируйте TokenStore.

    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(jwtTokenEnhancer());
    }
    
    @Bean
    protected JwtAccessTokenConverter jwtTokenEnhancer() {
        KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "mySecretKey".toCharArray());
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setKeyPair(keyStoreKeyFactory.getKeyPair("jwt"));
        return converter;
    }
    
  • Полученный TokenStore внедряется в класс AuthorizationServerEndpointsConfigurer.

    нуждаться вOAuth2ConfigurationМетод configure реализован в таком классе, как:

    @Autowired
    @Qualifier("authenticationManagerBean")
    private AuthenticationManager authenticationManager;
    
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.tokenStore(tokenStore()).tokenEnhancer(jwtTokenEnhancer()).authenticationManager(authenticationManager);
    }
    

    здесьauthenticationManagerвWebSecurityConfigОпределено в классе:

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    

    пройти черезauthenticationManager,WebSecurityConfigиOAuth2Configurationсвязанный.

    пройти черезcurl -XPOST "web_app:@localhost:9999/oauth/token" -d "grant_type=password&username=reader&password=reader"Вы можете получить токен, например

    {"access_token":"eyJhbGciOiJSUzI","token_type":"bearer","refresh_token":"eyJhbGciOiJSUzI1NiIs","expires_in":43199,"scope":"FOO","jti":"648f4380-d339-47dc-acd1-24f38b50e512"}
    

    Примечание. Поскольку сгенерированные токены относительно длинные, здесь они усекаются.

    На данный момент наш сервер аутентификации настроен, и токен можно получить с сервера аутентификации через имя пользователя и пароль.

сервер ресурсов

Сервер ресурсов предоставляет служебный интерфейс для пользователей, и ему необходимо оценить, является ли запрос законным и имеет ли он разрешение на доступ к определенному ресурсу интерфейса в соответствии с запрошенным токеном.

  • Создать ресурс с доступом к ресурсам
    @SpringBootApplication
    public class JwtResourceServerApplication {
        public static void main(String[] args) {
            SpringApplication.run(JwtResourceServerApplication.class);
        }
    
        @RestController
        @RequestMapping("/")
        protected static class HelloController {
            @GetMapping
            public String home() {
                return "home";
            }
        }
    }
    

    Отключите проверку безопасности по умолчанию в application.properties.

    server.port=9998
    security.basic.enabled=false
    management.security.enabled=false
    

    В настоящее времяcurl http://localhost:9998/получит возвращаемое значение «дом».

  • Используйте public.cert для разрешения токена

    @Configuration
     protected static class JwtConfiguration {
        @Autowired
        JwtAccessTokenConverter jwtAccessTokenConverter;
    
    
        @Bean
        @Qualifier("tokenStore")
        public TokenStore tokenStore() {
    
            System.out.println("Created JwtTokenStore");
            return new JwtTokenStore(jwtAccessTokenConverter);
        }
    
        @Bean
        protected JwtAccessTokenConverter jwtTokenEnhancer() {
            JwtAccessTokenConverter converter =  new JwtAccessTokenConverter();
            Resource resource = new ClassPathResource("public.cert");
            String publicKey = null;
            try {
                publicKey = new String(FileCopyUtils.copyToByteArray(resource.getInputStream()));
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
            converter.setVerifierKey(publicKey);
            return converter;
        }
    }
    
  • Настроить сервер ресурсов
    @Configuration
    @EnableResourceServer
    protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
        @Override
        public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
            resources.tokenStore(tokenStore);
        }
    
        @Autowired
        TokenStore tokenStore;
    }
    

    На этом настройка сервера ресурсов завершена, и тест можно выполнить после запуска сервера ресурсов:

    curl "localhost:9998/"вернет 401 без разрешения:

    {"error":"unauthorized","error_description":"Full authentication is required to access this resource"}
    

    И если запрос приходит с правильным токеном (тот, который получен от сервера аутентификации), он вернет «домой».

    export TOKEN=eyJhbGciOiJSUzI
    curl -H "Authorization: Bearer $TOKEN" "localhost:9998/"
    
        

Если команда curl содержит неверный токен или неверный public.cert, будет выведено сообщение invalid_token, не удается преобразовать токен доступа в JSON.

  • Чтобы немного расшириться, мы не видели роль FOO_READ на сервере ресурсов, так как же это на самом деле работает?

    Если мы добавим аннотации к классу JwtResourceServerApplication@EnableGlobalMethodSecurity(prePostEnabled = true)и добавить аннотации к ресурсу@PreAuthorize("hasAuthority('FOO_WRITE')"). В настоящее времяcurl -H "Authorization: Bearer $TOKEN" "localhost:9998/"также вернетсяAccess is denied. Поскольку роль, полученная из токена,FOO_READ, а здесь требуетсяFOO_WRITEдоступна только роль.

резюме

На основе сгенерированного асимметричного ключа для сервера аутентификации:

  • Настройте информацию для входа (имя пользователя и пароль) аутентифицированного пользователя, интегрировав WebSecurityConfigurerAdapter, и назначьте пользователю (клиенту) различные роли.

  • Настройте сервер OAuth2, интегрировав AuthorizationServerConfigurerAdapter.

  • Инициализировать различную клиентскую информацию службы OAuth2 и установить режим авторизации клиента.

  • Прочитайте закрытый ключ в паре ключей и сгенерируйтеJwtAccessTokenConverterи впрыснуть егоJwtTokenStore

  • настроитьAuthorizationServerEndpointsConfigurer,инъекцияJwtTokenStoreи из WebSecurityConfigurerAdapterAuthenticationManager

Для серверов ресурсов:

  • Создайте ресурсы, к которым необходимо получить доступ, что часто является доступным узлом API.

  • Прочитайте открытый ключ в паре ключей, введите verifierKey, открытым ключом которого является JwtAccessTokenConverter, и поместитеJwtAccessTokenConverterи впрыснуть егоJwtTokenStoreсередина

  • будетJwtTokenStoreвводить вResourceServerConfigurerAdapterсередина

  • Для расширений доступа к ресурсам вы можете@EnableGlobalMethodSecurity(prePostEnabled = true)чтобы включить переключатель для проверки безопасности перед доступом к ресурсам,@PreAuthorize("hasAuthority('XXXXX')")Чтобы установить клиентские роли, которым разрешен доступ для каждого обработчика

Единый вход (SSO)

В примере из предыдущей главы изолируется сервер аутентификации от сервера ресурсов.Пользователь входит на сервер ресурсов, чтобы получить токен, а затем переносит токен для доступа к серверу ресурсов для получения ресурса. Сервер ресурсов не имеет прямого взаимодействия со службой аутентификации.

Сценарий, обсуждаемый в этой главе, заключается в том, что когда пользователь входит в систему с токеном, сервер ресурсов должен отправить запрос на сервер аутентификации для получения проверки токена.

Кластер службы аутентификации

использованная литература