1. Обзор
В этом руководстве мы обсудим, как заставить нашу реализацию Spring Security OAuth2 использовать веб-токены JSON.
Мы также продолжаем развивать статью Spring REST API + OAuth2 + Angular в этой серии OAuth.
2. Сервер авторизации OAuth2
Ранее стек Spring Security OAuth предлагал возможность настроить сервер авторизации в качестве приложения Spring. Затем нам пришлось настроить его для использования JwtTokenStore
, чтобы мы могли использовать токены JWT.
Однако стек OAuth устарел в Spring, и теперь мы будем использовать Keycloak в качестве нашего сервера авторизации.
Итак, на этот раз мы настроим наш сервер авторизации как встроенный сервер Keycloak в приложении Spring Boot . Он выдает токены JWT по умолчанию, поэтому в этом отношении нет необходимости в какой-либо другой конфигурации.
3. Сервер ресурсов
Теперь давайте посмотрим, как настроить наш сервер ресурсов для использования JWT.
Мы сделаем это в файле application.yml :
server:
port: 8081
servlet:
context-path: /resource-server
spring:
jpa:
defer-datasource-initialization: true
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://localhost:8083/auth/realms/foreach
jwk-set-uri: http://localhost:8083/auth/realms/foreach/protocol/openid-connect/certs
JWT включают всю информацию в токене, поэтому серверу ресурсов необходимо проверить подпись токена, чтобы убедиться, что данные не были изменены. Свойство jwk-set-uri
содержит открытый ключ , который сервер может использовать для этой цели .
Свойство issuer-uri
указывает на базовый URI сервера авторизации, который также можно использовать для проверки утверждения iss
в качестве дополнительной меры безопасности.
Кроме того, если свойство jwk-set-uri
не задано, сервер ресурсов попытается использовать uri-адрес выпускающего
для определения местоположения этого ключа из конечной точки метаданных сервера авторизации .
Важно отметить, что добавление свойства issuer-uri
требует, чтобы у нас был запущен сервер авторизации, прежде чем мы сможем запустить приложение Resource Server .
Теперь давайте посмотрим, как мы можем настроить поддержку 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 Security по умолчанию; нам нужно явно указать, что мы хотим, чтобы он вел себя как сервер ресурсов, и что мы будем использовать токены доступа в формате JWT с использованием методов oauth2ResourceServer()
и jwt()
соответственно .
Приведенная выше конфигурация JWT — это то, что нам предоставляет экземпляр Spring Boot по умолчанию. Это также можно настроить, как мы вскоре увидим.
4. Пользовательские утверждения в токене
Теперь давайте настроим некоторую инфраструктуру, чтобы иметь возможность добавлять несколько пользовательских утверждений в токен доступа, возвращаемый сервером авторизации . Стандартные утверждения, предоставляемые платформой, хороши, но в большинстве случаев нам потребуется некоторая дополнительная информация в токене для использования на стороне клиента.
Давайте возьмем в качестве примера пользовательское утверждение, организация
, которое будет содержать название организации данного пользователя.
4.1. Конфигурация сервера авторизации
Для этого нам нужно добавить пару конфигураций в наш файл определения области, foreach-realm.json
:
Добавьте организацию
атрибутов нашему пользователюjohn@test.com
:
"attributes" : {
"organization" : "foreach"
},
- Добавьте
организацию
под названием protocolMapperв конфигурацию
jwtClient
:
"protocolMappers": [{
"id": "06e5fc8f-3553-4c75-aef4-5a4d7bb6c0d1",
"name": "organization",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "organization",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "organization",
"jsonType.label": "String"
}
}],
Для автономной установки Keycloak это также можно сделать с помощью консоли администратора.
Важно помнить, что приведенная выше конфигурация JSON специфична для Keycloak и может отличаться для других серверов OAuth .
С этой новой конфигурацией мы получим дополнительный атрибут, организация = foreach
, в полезной нагрузке токена для john@test.com
:
{
jti: "989ce5b7-50b9-4cc6-bc71-8f04a639461e"
exp: 1585242462
nbf: 0
iat: 1585242162
iss: "http://localhost:8083/auth/realms/foreach"
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: "foreach"
preferred_username: "john@test.com"
}
4.2. Используйте токен доступа в клиенте Angular
Далее мы хотим использовать информацию о токене в нашем клиентском приложении Angular. Для этого мы будем использовать библиотеку angular2-jwt .
Мы воспользуемся утверждением организации
в нашем AppService
и добавим функцию getOrganization
:
getOrganization(){
var token = Cookie.get("access_token");
var payload = this.jwtHelper.decodeToken(token);
this.organization = payload.organization;
return this.organization;
}
Эта функция использует JwtHelperService
из библиотеки angular2-jwt
для декодирования токена доступа и получения нашего пользовательского утверждения. Теперь все, что нам нужно сделать, это отобразить его в нашем AppComponent
:
@Component({
selector: 'app-root',
template: `<nav class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="/">Spring Security Oauth - Authorization Code</a>
</div>
</div>
<div class="navbar-brand">
<p>{{organization}}</p>
</div>
</nav>
<router-outlet></router-outlet>`
})
export class AppComponent implements OnInit {
public organization = "";
constructor(private service: AppService) { }
ngOnInit() {
this.organization = this.service.getOrganization();
}
}
5. Доступ к дополнительным утверждениям на сервере ресурсов
Но как мы можем получить доступ к этой информации на стороне сервера ресурсов?
5.1. Доступ к утверждениям сервера аутентификации
Это действительно просто, нам просто нужно извлечь его из `` AuthenticationPrincipal
org.springframework.security.oauth2.jwt.Jwt
, как мы сделали бы для любого другого атрибута в UserInfoController
:
@GetMapping("/user/info")
public Map<String, Object> getUserInfo(@AuthenticationPrincipal Jwt principal) {
Map<String, String> map = new Hashtable<String, String>();
map.put("user_name", principal.getClaimAsString("preferred_username"));
map.put("organization", principal.getClaimAsString("organization"));
return Collections.unmodifiableMap(map);
}
5.2. Конфигурация для добавления/удаления/переименования утверждений
А что, если мы хотим добавить больше утверждений на стороне сервера ресурсов? Или удалить или переименовать некоторые?
Допустим, мы хотим изменить утверждение организации
, поступающее с сервера аутентификации, чтобы получить значение в верхнем регистре. Однако, если утверждение отсутствует у пользователя, нам нужно установить его значение как unknown
.
Для этого нам нужно добавить класс, реализующий интерфейс Converter и использующий
MappedJwtClaimSetConverter
для преобразования утверждений :
public class OrganizationSubClaimAdapter implements
Converter<Map<String, Object>, Map<String, Object>> {
private final MappedJwtClaimSetConverter delegate =
MappedJwtClaimSetConverter.withDefaults(Collections.emptyMap());
public Map<String, Object> convert(Map<String, Object> claims) {
Map<String, Object> 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 для пользователя mike@other.com
, мы получим организацию
как UNKNOWN
.
Обратите внимание, что переопределение bean-компонента JwtDecoder
по умолчанию, настроенного Spring Boot, должно выполняться осторожно, чтобы убедиться, что вся необходимая конфигурация по-прежнему включена.
6. Загрузка ключей из хранилища ключей Java
В нашей предыдущей конфигурации мы использовали открытый ключ сервера авторизации по умолчанию для проверки целостности нашего токена.
Мы также можем использовать пару ключей и сертификат, хранящиеся в файле хранилища ключей Java, для выполнения процесса подписи.
6.1. Сгенерируйте файл JKS Java KeyStore
Давайте сначала сгенерируем ключи, а точнее файл .jks
, используя инструмент командной строки keytool
:
keytool -genkeypair -alias mytest
-keyalg RSA
-keypass mypass
-keystore mytest.jks
-storepass mypass
Команда сгенерирует файл с именем mytest.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
:
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<excludes>
<exclude>*.jks</exclude>
</excludes>
</resource>
</resources>
</build>
Если мы используем Spring Boot, нам нужно убедиться, что наш файл JKS добавлен в путь к классам приложения через подключаемый модуль Spring Boot Maven addResources
:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<addResources>true</addResources>
</configuration>
</plugin>
</plugins>
</build>
6.4. Сервер авторизации
Теперь мы настроим Keycloak для использования нашей пары ключей из mytest.jks
, добавив ее в раздел KeyProvider
файла JSON определения области следующим образом:
{
"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-uri
, которое мы указали ранее.
Опять же, эта конфигурация специфична для Keycloak и может отличаться для других реализаций сервера OAuth.
7. Заключение
В этой краткой статье мы сосредоточились на настройке нашего проекта Spring Security OAuth2 для использования веб-токенов JSON.
Полную реализацию этой статьи можно найти на GitHub .