Рубрики
Без рубрики

Использование JWT с Spring Security OAuth

Руководство по использованию токенов JWT с Spring Security 5.

Автор оригинала: Sampada Wagde.

1. Обзор

В этом уроке мы обсудим, как заставить нашу реализацию Spring Security OAuth2 использовать веб-токены JSON.

Мы также продолжаем развивать статью Spring REST API + OAuth2 + Angular в этой серии OAuth.

Дальнейшее чтение:

Выход из защищенного приложения OAuth

OAuth2 Запомните меня с помощью токена обновления (с использованием устаревшего стека Spring Security OAuth)

OAuth2 для API Spring REST – Обработайте токен обновления в Angular

2. Сервер Авторизации OAuth2

Ранее стек Spring Security OAuth предлагал возможность настройки сервера авторизации в качестве приложения Spring. Затем нам пришлось настроить его на использование JwtTokenStore , чтобы мы могли использовать токены JWT.

Однако стек OAuth устарел к весне, и теперь мы будем использовать Keycloak в качестве сервера авторизации.

Поэтому на этот раз мы настроим наш сервер авторизации как встроенный сервер блокировки ключей в приложении Spring Boot . По умолчанию он выдает токены JWT, поэтому в этом отношении нет необходимости в какой-либо другой конфигурации.

3. Сервер ресурсов

Теперь давайте посмотрим, как настроить наш сервер ресурсов для использования JWT.

Мы сделаем это в файле application.yml :

server: 
  port: 8081
  servlet: 
    context-path: /resource-server

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://localhost:8083/auth/realms/baeldung
          jwk-set-uri: http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/certs

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

Свойство issuer-uri указывает на URL-адрес базового сервера авторизации, который также можно использовать для проверки утверждения iss в качестве дополнительной меры безопасности.

Кроме того, если свойство jwk-set-url не задано, сервер ресурсов попытается использовать uri эмитента для определения местоположения этого ключа из конечной точки метаданных сервера авторизации .

Важно отметить, что добавление issuer-uri свойства требует, чтобы у нас был запущен Сервер авторизации, прежде чем мы сможем запустить приложение Сервера ресурсов .

Теперь давайте посмотрим, как мы можем настроить поддержку JWT с помощью конфигурации Java:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors()
            .and()
              .authorizeRequests()
                .antMatchers(HttpMethod.GET, "/user/info", "/api/foos/**")
                  .hasAuthority("SCOPE_read")
                .antMatchers(HttpMethod.POST, "/api/foos")
                  .hasAuthority("SCOPE_write")
                .anyRequest()
                  .authenticated()
            .and()
              .oauth2ResourceServer()
                .jwt();
    }
}

Здесь мы переопределяем конфигурацию безопасности Http по умолчанию; нам нужно явно указать, что мы хотим, чтобы это вело себя как сервер ресурсов, и что мы будем использовать маркеры доступа в формате JWT, используя методы oauth2ResourceServer() и jwt () , соответственно.

Приведенная выше конфигурация JWT-это то, что предоставляет нам экземпляр Spring Boot по умолчанию. Это также можно настроить, как мы вскоре увидим.

4. Пользовательские утверждения в токене

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

Давайте возьмем пример пользовательского утверждения organization , которое будет содержать имя организации данного пользователя.

4.1. Конфигурация сервера Авторизации

Для этого нам нужно добавить пару конфигураций в наш файл определения области, baeldung-realm.json :

  • Добавьте атрибут организация нашему пользователю [email protected] :

  • Добавьте сопоставитель протоколов вызываемый организация в Клиент jwt конфигурация:

Для автономной настройки ключевого ключа это также можно сделать с помощью консоли администратора.

Важно помнить, что приведенная выше конфигурация JSON специфична для Keycloak и может отличаться для других серверов OAuth .

Когда эта новая конфигурация будет запущена и запущена, мы получим дополнительный атрибут organization в полезной нагрузке токена для [email protected] :

{
  jti: "989ce5b7-50b9-4cc6-bc71-8f04a639461e"
  exp: 1585242462
  nbf: 0
  iat: 1585242162
  iss: "http://localhost:8083/auth/realms/baeldung"
  sub: "a5461470-33eb-4b2d-82d4-b0484e96ad7f"
  typ: "Bearer"
  azp: "jwtClient"
  auth_time: 1585242162
  session_state: "384ca5cc-8342-429a-879c-c15329820006"
  acr: "1"
  scope: "profile write read"
  organization: "baeldung"
  preferred_username: "[email protected]"
}

4.2. Используйте Маркер доступа в Клиенте Angular

Затем мы захотим использовать информацию о токенах в нашем клиентском приложении Angular. Для этого мы будем использовать библиотеку angular2-jwt .

Мы воспользуемся утверждением organization в нашем сервисе приложений и добавим функцию getOrganization :

getOrganization(){
  var token = Cookie.get("access_token");
  var payload = this.jwtHelper.decodeToken(token);
  this.organization = payload.organization; 
  return this.organization;
}

Эта функция использует JwtHelperService из библиотеки angular2-jwt для декодирования маркера доступа и получения пользовательского утверждения. Теперь все, что нам нужно сделать, это отобразить его в нашем компоненте приложения :

@Component({
  selector: 'app-root',
  template: `
`
})

export class AppComponent implements OnInit {
  public organization = "";
  constructor(private service: AppService) { }  
   
  ngOnInit() {  
    this.organization = this.service.getOrganization();
  }  
}

5. Доступ к дополнительным утверждениям на сервере ресурсов

Но как мы можем получить доступ к этой информации на стороне сервера ресурсов?

5.1. Требования Сервера Аутентификации Доступа

Это действительно просто, нам просто нужно извлечь его из org.springframework.security.oauth2.jwt.Jwt ‘s AuthenticationPrincipal, | как мы сделали бы для любого другого атрибута в UserInfoController :

@GetMapping("/user/info")
public Map getUserInfo(@AuthenticationPrincipal Jwt principal) {
    Map map = new Hashtable();
    map.put("user_name", principal.getClaimAsString("preferred_username"));
    map.put("organization", principal.getClaimAsString("organization"));
    return Collections.unmodifiableMap(map);
}

5.2. Конфигурация для добавления/удаления/переименования утверждений

Теперь, что, если мы хотим добавить больше утверждений на стороне сервера ресурсов? Или удалить или переименовать некоторые?

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

Для этого нам нужно добавить класс, который реализует интерфейс Converter и использует MappedJwtClaimSetConverter для преобразования утверждений :

public class OrganizationSubClaimAdapter implements 
  Converter, Map> {
    
    private final MappedJwtClaimSetConverter delegate = 
      MappedJwtClaimSetConverter.withDefaults(Collections.emptyMap());

    public Map convert(Map claims) {
        Map convertedClaims = this.delegate.convert(claims);
        String organization = convertedClaims.get("organization") != null ? 
          (String) convertedClaims.get("organization") : "unknown";
        
        convertedClaims.put("organization", organization.toUpperCase());

        return convertedClaims;
    }
}

Затем в нашем классе SecurityConfig нам нужно добавить свой собственный JwtDecoder экземпляр , чтобы переопределить экземпляр, предоставленный Spring Boot , и установить наш OrganizationSubClaimAdapter в качестве преобразователя утверждений :

@Bean
public JwtDecoder customDecoder(OAuth2ResourceServerProperties properties) {
    NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(
      properties.getJwt().getJwkSetUri()).build();
    
    jwtDecoder.setClaimSetConverter(new OrganizationSubClaimAdapter());
    return jwtDecoder;
}

Теперь , когда мы нажмем наш /user/info API для пользователя [email protected] , мы получим organization как UNKNOWN .

Обратите внимание, что переопределение по умолчанию JwtDecoder bean, настроенного Spring Boot, должно быть выполнено тщательно, чтобы убедиться, что все необходимые настройки все еще включены.

6. Загрузка Ключей Из Хранилища Ключей Java

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

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

6.1. Создание файла хранилища ключей JKS Java

Давайте сначала сгенерируем ключи, а точнее файл .jks , используя инструмент командной строки keytool :

keytool -genkeypair -alias mytest 
                    -keyalg RSA 
                    -keypass mypass 
                    -keystore mytest.jks 
                    -storepass mypass

Давайте сначала сгенерируем ключи, а точнее файл .jks , используя инструмент командной строки

Также убедитесь, что keypass и storepass совпадают.

6.2. Экспорт Открытого Ключа

Далее нам нужно экспортировать ваш открытый ключ из сгенерированного JKS. Для этого мы можем использовать следующую команду:

keytool -list -rfc --keystore mytest.jks | openssl x509 -inform pem -pubkey

Пример ответа будет выглядеть следующим образом:

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgIK2Wt4x2EtDl41C7vfp
OsMquZMyOyteO2RsVeMLF/hXIeYvicKr0SQzVkodHEBCMiGXQDz5prijTq3RHPy2
/5WJBCYq7yHgTLvspMy6sivXN7NdYE7I5pXo/KHk4nz+Fa6P3L8+L90E/3qwf6j3
DKWnAgJFRY8AbSYXt1d5ELiIG1/gEqzC0fZmNhhfrBtxwWXrlpUDT0Kfvf0QVmPR
xxCLXT+tEe1seWGEqeOLL5vXRLqmzZcBe1RZ9kQQm43+a9Qn5icSRnDfTAesQ3Cr
lAWJKl2kcWU1HwJqw+dZRSZ1X4kEXNMyzPdPBbGmU6MHdhpywI7SKZT7mX4BDnUK
eQIDAQAB
-----END PUBLIC KEY-----
-----BEGIN CERTIFICATE-----
MIIDCzCCAfOgAwIBAgIEGtZIUzANBgkqhkiG9w0BAQsFADA2MQswCQYDVQQGEwJ1
czELMAkGA1UECBMCY2ExCzAJBgNVBAcTAmxhMQ0wCwYDVQQDEwR0ZXN0MB4XDTE2
MDMxNTA4MTAzMFoXDTE2MDYxMzA4MTAzMFowNjELMAkGA1UEBhMCdXMxCzAJBgNV
BAgTAmNhMQswCQYDVQQHEwJsYTENMAsGA1UEAxMEdGVzdDCCASIwDQYJKoZIhvcN
AQEBBQADggEPADCCAQoCggEBAICCtlreMdhLQ5eNQu736TrDKrmTMjsrXjtkbFXj
Cxf4VyHmL4nCq9EkM1ZKHRxAQjIhl0A8+aa4o06t0Rz8tv+ViQQmKu8h4Ey77KTM
urIr1zezXWBOyOaV6Pyh5OJ8/hWuj9y/Pi/dBP96sH+o9wylpwICRUWPAG0mF7dX
eRC4iBtf4BKswtH2ZjYYX6wbccFl65aVA09Cn739EFZj0ccQi10/rRHtbHlhhKnj
iy+b10S6ps2XAXtUWfZEEJuN/mvUJ+YnEkZw30wHrENwq5QFiSpdpHFlNR8CasPn
WUUmdV+JBFzTMsz3TwWxplOjB3YacsCO0imU+5l+AQ51CnkCAwEAAaMhMB8wHQYD
VR0OBBYEFOGefUBGquEX9Ujak34PyRskHk+WMA0GCSqGSIb3DQEBCwUAA4IBAQB3
1eLfNeq45yO1cXNl0C1IQLknP2WXg89AHEbKkUOA1ZKTOizNYJIHW5MYJU/zScu0
yBobhTDe5hDTsATMa9sN5CPOaLJwzpWV/ZC6WyhAWTfljzZC6d2rL3QYrSIRxmsp
/J1Vq9WkesQdShnEGy7GgRgJn4A8CKecHSzqyzXulQ7Zah6GoEUD+vjb+BheP4aN
hiYY1OuXD+HsdKeQqS+7eM5U7WW6dz2Q8mtFJ5qAxjY75T0pPrHwZMlJUhUZ+Q2V
FfweJEaoNB9w9McPe1cAiE+oeejZ0jq0el3/dJsx3rlVqZN+lMhRJJeVHFyeb3XF
lLFCUGhA7hxn2xf3x1JW
-----END CERTIFICATE-----

6.3. Конфигурация Maven

Мы не хотим, чтобы файл JKS был выбран процессом фильтрации maven, поэтому мы обязательно исключим его в pom.xml :


    
        
            src/main/resources
            true
            
                *.jks
            
        
    

Если мы используем Spring Boot, нам нужно убедиться, что наш файл JKS добавлен в путь к классу приложения с помощью плагина Spring Boot Maven addResources :


    
        
            org.springframework.boot
            spring-boot-maven-plugin
            
                true
            
        
    

6.4. Сервер авторизации

Теперь мы настроим Keycloak для использования вашей пары ключей из my test.jks , добавив ее в раздел KeyProvider файла определения области следующим образом:

{
  "id": "59412b8d-aad8-4ab8-84ec-e546900fc124",
  "name": "java-keystore",
  "providerId": "java-keystore",
  "subComponents": {},
  "config": {
    "keystorePassword": [ "mypass" ],
    "keyAlias": [ "mytest" ],
    "keyPassword": [ "mypass" ],
    "active": [ "true" ],
    "keystore": [
            "src/main/resources/mytest.jks"
          ],
    "priority": [ "101" ],
    "enabled": [ "true" ],
    "algorithm": [ "RS256" ]
  }
},

Здесь мы установили приоритет на 101 , больше, чем любая другая пара ключей для вашего сервера авторизации, и установите active в true . Это делается для того, чтобы гарантировать, что наш сервер ресурсов выберет эту конкретную пару ключей из свойства jwk-set-url , которое мы указали ранее.

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

7. Заключение

В этой краткой статье мы сосредоточились на настройке нашего проекта Spring Security OAuth2 для использования веб-токенов JSON.

Полную реализацию этой статьи можно найти на GitHub .