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

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

У нас есть SSO через OneLogin, и микросервисы Kotlin под управлением Spring Boot находятся на нашем золотом пути, поэтому я использовал их как путь наименьшего сопротивления.

(Это будет работать с другими поставщиками SSO, такими как Okta, Google, Microsoft, Facebook и т. д. Просто подставьте их имя и URL-адреса в приведенную ниже конфигурацию.)

Результат: когда пользователь, не прошедший проверку подлинности, посещает мой сервис, он перенаправляется на OneLogin, обычно он уже авторизован там, но если нет, то он входит в систему, а затем возвращается на тот URL-адрес в моем сервисе, который им нужен. Если они обновляются, то они не возвращаются через OneLogin (пока срок их аутентификации не истечет через несколько часов). Хороший!

Настройка: Привет, мир с Spring Boot

Я предполагаю, что у вас уже есть веб-приложение, обслуживающее запросы. Например. http://localhost:9000/hello отображает текст hello world .

Достижение этого выходит за рамки этого практического руководства, но вот некоторые зависимости, которые я использую:

    val springBootVersion = "2.7.9"
    val springBootDependencies = "org.springframework.boot:spring-boot-dependencies:$springBootVersion"
    implementation(platform(springBootDependencies))
    kapt(platform(springBootDependencies))
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-actuator")

Добавить весеннюю безопасность

Добавьте несколько новых зависимостей:

    implementation("org.springframework.boot:spring-boot-starter-security")
    implementation("org.springframework.security.oauth.boot:spring-security-oauth2-autoconfigure:2.6.8")
    implementation("org.springframework.security:spring-security-config")
    implementation("org.springframework.security:spring-security-oauth2-client")
    implementation("org.springframework.security:spring-security-oauth2-jose")

Добавьте немного конфигурации в свой application.yaml :

spring:
  security:
    oauth2:
      client:
        registration:
          onelogin:
            client-id: "your-client-id"
            client-secret: ${ONELOGIN_CLIENT_SECRET} # set your env var securely
            scope:
              - openid
              - profile
              - email
            redirect-uri: https://localhost:9000/login/oauth2/code/onelogin # different in prod
        provider:
          onelogin:
            user-info-uri:  https://your-subdomain.onelogin.com/oidc/2/me
            token-uri: https://your-subdomain.onelogin.com/oidc/2/token
            authorization-uri: https://your-subdomain.onelogin.com/oidc/2/auth
            issuer-uri: https://your-subdomain.onelogin.com/oidc/2
            jwk-set-uri: https://your-subdomain.onelogin.com/oidc/2/certs

Spring Security теперь будет перехватывать запросы и пытаться аутентифицировать каждого пользователя браузера. Попробуйте перейти к http://localhost:9000/hello, и он должен попросить вас где-нибудь войти в систему, но никогда не вернет вас к тому URL-адресу, показывающему hello world. Это не сработает, пока вы…

Настроить HttpSecurity

Spring не будет знать, какой тип входа вы хотите применить, и какие пути оставить неаутентифицированными (и будут некоторые — например, проверки работоспособности).

Сообщите ему эту информацию, создав класс, который создает bean-компонент нужного типа где-то под src/main/kotlin:

@Configuration
@EnableWebSecurity
@Order(98)
class SecurityConfiguration {
    @Bean
    fun springSecurityConfig(http: HttpSecurity): SecurityFilterChain? {
        http.authorizeRequests()
            .antMatchers(
                "/health-check/**", "/login**", "/favicon.ico", "/**.css", "/**.png"
            ).permitAll()
            .anyRequest().authenticated()
            .and()
            .oauth2Login()
            .and()
            .csrf().disable()

        return http.build()
    }
}

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

В автоматических тестах вообще не нужна никакая аутентификация (если только вы не собираетесь тестировать аутентификацию, что является большой темой, которую я не буду рассматривать в этой статье).

Создайте аналогичный класс где-нибудь под src/test/kotlin:

@Configuration
@EnableWebSecurity
@Profile("test")
@Order(99)
class TestConfiguration {
    @Bean
    fun noSpringSecurityInTests(http: HttpSecurity): SecurityFilterChain? {
        http.authorizeRequests().antMatchers("/**").permitAll()
        return http.build()
    }
}

Теперь Spring Security знает, куда отправлять пользователей браузера для аутентификации: OneLogin со всеми нужными параметрами. Однако OneLogin ничего не знает о том, что ваш сервис является одним из его клиентов, и будет отклонять все запросы.

Создание клиентского приложения OneLogin

В основном это вопрос перехода к https://<your-subdomain>.onelogin.com, щелчка по пользовательскому интерфейсу создания приложения и ввода таких сведений, как название вашего приложения.

Вы можете в основном следовать их инструкциям, но будьте осторожны! Их фрагменты кода относятся к устаревшим версиям Spring и Spring Security, поэтому используйте мой код и конфигурацию выше. (Вот почему я не дал ссылку на документы OneLogin вверху. Мне также были полезны эти инструкции Okta.)

Получите свой идентификатор клиента и секрет из пользовательского интерфейса OneLogin.

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

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

Вы можете создать отдельные приложения OneLogin для разработки и производства, если хотите, и в этом случае вам нужно будет использовать другой документ yaml в application.yaml, чтобы установить идентификатор клиента для локальной разработки:

---

spring:
  config:
    activate:
      on-profile: test
  security:
    oauth2:
      client:
        registration:
          onelogin:
            client-id: "id for a different OneLogin app"

Вы по-прежнему не должны записывать секрет своего приложения в исходный код, даже если он «только» для вашего локального приложения.

Скопируйте свой redirect-uri в пользовательский интерфейс OneLogin и нажмите «Сохранить». Он должен быть одинаковым в вашем исходном коде и в OneLogin. Вы можете использовать те же различные документы yaml и трюк с профилями Spring, что и выше, чтобы установить разные значения локально по сравнению с продом.

Успех!

Теперь ты готовишь. Укажите в браузере http://localhost:9000/hello, и вы должны:

  1. Кратко просмотрите этот URL-адрес в адресной строке.
  2. Быть перенаправленным примерно через 5 URL-адресов в разделе https://your-subdomain.onelogin.com. (Войдите, если нужно.)
  3. Будьте перенаправлены обратно на исходный URL.
  4. Смотрите hello world !
  5. Обновите и убедитесь, что вы не перенаправлены через OneLogin.

Совет: откройте инструменты разработчика вашего браузера на вкладке «Сеть», чтобы просмотреть поток URL-адресов, которые он открывает.

… Пока у вас есть только один узел

Это ловушка для неосторожных.

Если вы выполняете развертывание в prod и ваш сервис работает более чем на одном узле (или более чем на 1 модуле Kubertnetes, или как он называется вашим PaaS/IaaS), вы можете обнаружить странное поведение:

Аутентификация работает. Затем он не работает, выдавая некоторые труднопонятные ошибки от OneLogin или Spring Security о «недопустимом запросе» или «недопустимых учетных данных». Тогда это работает для вас, но не для вашего коллеги. Тогда это наоборот. Он продолжает метаться между работой и ошибками.

Происходит то, что Spring Security кэширует что-то в памяти о том, какие запросы он перенаправил в OneLogin, и если OneLogin перенаправляет браузер обратно на узел, у которого этого нет в памяти, он отклоняет запрос. Иногда он снова отправляет вас обратно в OneLogin, иногда решает, что вы пытались слишком часто, и просто убивает запрос. Однажды он перекинул меня на OneLogin 8 раз подряд.

Вот что происходит:

Узел 1 → OneLogin → Узел 1 🎉 Успехов!

Узел 1 → OneLogin → Узел 2 ❌☠️✋⛔️ Я никогда не отправлял вас в OneLogin!!!

Это проблема, которую я еще не решил. Я попытался указать Spring хранить данные сеанса в файлах cookie и URL-адресах, но не нашел ничего, что сработало. Также не помогло то, что цикл разработки был ужасно медленным, потому что я выполнял развертывание каждый раз (это всегда работало локально, потому что локально всегда был один процесс).

В конце концов я обошел это (на данный момент), обслуживая с одного узла. 😉