- замена сцены
- Изолировать ресурсы и службы аутентификации
- Единый вход (SSO)
- Кластер службы аутентификации
- использованная литература
Настройка OAuth2 на основе Spring Boot — относительно простое дело, но когда вы видите связанные демонстрационные проекты, вы часто сбиваетесь с толку. Это потому, что я не понимаю базовых понятий, столкнувшись с кучей аннотаций и инъекций, я никак не могу понять, как между ними взаимосвязь и какие обязанности они несут.
В этой статье мы попытаемся разбить сложные проблемы на более простые, чтобы облегчить понимание, но эта статья немного затянулась. Если есть какие-либо ошибки, пожалуйста, свяжитесь с 27952278@qq.com!
замена сцены
Существует множество веб-сайтов, которые должны поддерживать стороннюю авторизацию (например, QQ, Weibo), например, посещениеjoin.thoughtworks.cnНажмите "Регистрация | Войти" в правом верхнем углу, всплывающее окно:
Щелкните значок 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.properties
server.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)
В примере из предыдущей главы изолируется сервер аутентификации от сервера ресурсов.Пользователь входит на сервер ресурсов, чтобы получить токен, а затем переносит токен для доступа к серверу ресурсов для получения ресурса. Сервер ресурсов не имеет прямого взаимодействия со службой аутентификации.
Сценарий, обсуждаемый в этой главе, заключается в том, что когда пользователь входит в систему с токеном, сервер ресурсов должен отправить запрос на сервер аутентификации для получения проверки токена.