Перейти к основному содержимому

Использование JWT с Spring Security OAuth (устаревший стек)

· 8 мин. чтения

1. Обзор

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

Мы также продолжаем развивать предыдущую статью из этой серии OAuth.

Прежде чем мы начнем — одно важное замечание. Имейте в виду, что основная команда Spring Security находится в процессе реализации нового стека OAuth2, причем некоторые аспекты уже реализованы, а некоторые еще находятся в разработке.

Версию этой статьи, использующую новый стек Spring Security 5, можно найти в нашей статье Использование JWT с Spring Security OAuth .

Хорошо, давайте сразу.

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

Во- первых, нам нужно добавить зависимость spring-security-jwt к нашему pom.xml :

<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>

Обратите внимание, что нам нужно добавить зависимость spring-security-jwt как к серверу авторизации, так и к серверу ресурсов.

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

Далее мы настроим наш сервер авторизации для использования JwtTokenStore следующим образом:

@Configuration
@EnableAuthorizationServer
public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints)
throws Exception {
endpoints.tokenStore(tokenStore())
.accessTokenConverter(accessTokenConverter())
.authenticationManager(authenticationManager);
}

@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}

@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("123");
return converter;
}

@Bean
@Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
return defaultTokenServices;
}
}

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

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

Теперь давайте посмотрим на нашу конфигурацию сервера ресурсов, которая очень похожа на конфигурацию сервера авторизации:

@Configuration
@EnableResourceServer
public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer config) {
config.tokenServices(tokenServices());
}

@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}

@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("123");
return converter;
}

@Bean
@Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
return defaultTokenServices;
}
}

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

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

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

Мы определим TokenEnhancer для настройки нашего токена доступа с помощью этих дополнительных утверждений.

В следующем примере мы добавим дополнительное поле « organization » в наш токен доступа — с этим CustomTokenEnhancer :

public class CustomTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(
OAuth2AccessToken accessToken,
OAuth2Authentication authentication) {
Map<String, Object> additionalInfo = new HashMap<>();
additionalInfo.put(
"organization", authentication.getName() + randomAlphabetic(4));
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(
additionalInfo);
return accessToken;
}
}

Затем мы подключим это к нашей конфигурации сервера авторизации — следующим образом:

@Override
public void configure(
AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(
Arrays.asList(tokenEnhancer(), accessTokenConverter()));

endpoints.tokenStore(tokenStore())
.tokenEnhancer(tokenEnhancerChain)
.authenticationManager(authenticationManager);
}

@Bean
public TokenEnhancer tokenEnhancer() {
return new CustomTokenEnhancer();
}

С этой новой конфигурацией и работающей — вот как будет выглядеть полезная нагрузка маркера токена:

{
"user_name": "john",
"scope": [
"foo",
"read",
"write"
],
"organization": "johnIiCh",
"exp": 1458126622,
"authorities": [
"ROLE_USER"
],
"jti": "e0ad1ef3-a8a5-4eef-998d-00b26bc2c53f",
"client_id": "fooClientIdPassword"
}

5.1. Используйте токен доступа в JS-клиенте

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

Итак, что мы собираемся сделать, так это использовать утверждение « organization » в нашем index.html :

<p class="navbar-text navbar-right">{{organization}}</p>

<script type="text/javascript"
src="https://cdn.rawgit.com/auth0/angular-jwt/master/dist/angular-jwt.js">
</script>

<script>
var app =
angular.module('myApp', ["ngResource","ngRoute", "ngCookies", "angular-jwt"]);

app.controller('mainCtrl', function($scope, $cookies, jwtHelper,...) {
$scope.organiztion = "";

function getOrganization(){
var token = $cookies.get("access_token");
var payload = jwtHelper.decodeToken(token);
$scope.organization = payload.organization;
}
...
});

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

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

Здесь мы сделаем следующее: извлечем дополнительные утверждения из токена доступа:

public Map<String, Object> getExtraInfo(OAuth2Authentication auth) {
OAuth2AuthenticationDetails details =
(OAuth2AuthenticationDetails) auth.getDetails();
OAuth2AccessToken accessToken = tokenStore
.readAccessToken(details.getTokenValue());
return accessToken.getAdditionalInformation();
}

В следующем разделе мы обсудим, как добавить эту дополнительную информацию к нашим данным аутентификации с помощью пользовательского AccessTokenConverter.

6.1. Пользовательский AccessTokenConverter

Давайте создадим CustomAccessTokenConverter и установим детали аутентификации с утверждениями токена доступа:

@Component
public class CustomAccessTokenConverter extends DefaultAccessTokenConverter {

@Override
public OAuth2Authentication extractAuthentication(Map<String, ?> claims) {
OAuth2Authentication authentication =
super.extractAuthentication(claims);
authentication.setDetails(claims);
return authentication;
}
}

Примечание. DefaultAccessTokenConverter используется для установки сведений об аутентификации в значение Null.

6.2. Настроить JwtTokenStore

Далее мы настроим наш JwtTokenStore для использования нашего CustomAccessTokenConverter :

@Configuration
@EnableResourceServer
public class OAuth2ResourceServerConfigJwt
extends ResourceServerConfigurerAdapter {

@Autowired
private CustomAccessTokenConverter customAccessTokenConverter;

@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}

@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setAccessTokenConverter(customAccessTokenConverter);
}
// ...
}

6.3. Дополнительные утверждения, доступные в объекте аутентификации

Теперь, когда сервер авторизации добавил несколько дополнительных утверждений в токен, теперь мы можем получить доступ на стороне сервера ресурсов, непосредственно в объекте аутентификации:

public Map<String, Object> getExtraInfo(Authentication auth) {
OAuth2AuthenticationDetails oauthDetails =
(OAuth2AuthenticationDetails) auth.getDetails();
return (Map<String, Object>) oauthDetails
.getDecodedDetails();
}

6.4. Проверка сведений об аутентификации

Давайте удостоверимся, что наш объект Authentication содержит эту дополнительную информацию:

@RunWith(SpringRunner.class)
@SpringBootTest(
classes = ResourceServerApplication.class,
webEnvironment = WebEnvironment.RANDOM_PORT)
public class AuthenticationClaimsIntegrationTest {

@Autowired
private JwtTokenStore tokenStore;

@Test
public void whenTokenDoesNotContainIssuer_thenSuccess() {
String tokenValue = obtainAccessToken("fooClientIdPassword", "john", "123");
OAuth2Authentication auth = tokenStore.readAuthentication(tokenValue);
Map<String, Object> details = (Map<String, Object>) auth.getDetails();

assertTrue(details.containsKey("organization"));
}

private String obtainAccessToken(
String clientId, String username, String password) {

Map<String, String> params = new HashMap<>();
params.put("grant_type", "password");
params.put("client_id", clientId);
params.put("username", username);
params.put("password", password);
Response response = RestAssured.given()
.auth().preemptive().basic(clientId, "secret")
.and().with().params(params).when()
.post("http://localhost:8081/spring-security-oauth-server/oauth/token");
return response.jsonPath().getString("access_token");
}
}

Примечание: мы получили токен доступа с дополнительными утверждениями с сервера авторизации, затем мы считываем с него объект аутентификации , который содержит дополнительную информацию «организация» в объекте деталей.

7. Асимметричная пара ключей

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

@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("123");
return converter;
}

Мы также можем использовать асимметричные ключи (открытые и закрытые ключи) для выполнения процесса подписи.

7.1. Сгенерируйте файл JKS Java KeyStore

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

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

Команда сгенерирует файл с именем mytest.jks , который содержит наши ключи — открытый и закрытый ключи.

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

7.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-----

Мы берем только наш Публичный ключ и копируем его на наш сервер ресурсов src/main/resources/public.txt :

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgIK2Wt4x2EtDl41C7vfp
OsMquZMyOyteO2RsVeMLF/hXIeYvicKr0SQzVkodHEBCMiGXQDz5prijTq3RHPy2
/5WJBCYq7yHgTLvspMy6sivXN7NdYE7I5pXo/KHk4nz+Fa6P3L8+L90E/3qwf6j3
DKWnAgJFRY8AbSYXt1d5ELiIG1/gEqzC0fZmNhhfrBtxwWXrlpUDT0Kfvf0QVmPR
xxCLXT+tEe1seWGEqeOLL5vXRLqmzZcBe1RZ9kQQm43+a9Qn5icSRnDfTAesQ3Cr
lAWJKl2kcWU1HwJqw+dZRSZ1X4kEXNMyzPdPBbGmU6MHdhpywI7SKZT7mX4BDnUK
eQIDAQAB
-----END PUBLIC KEY-----

В качестве альтернативы мы можем экспортировать только открытый ключ, добавив аргумент -noout :

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

7.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>

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

Теперь мы настроим JwtAccessTokenConverter для использования нашей пары ключей из mytest.jks следующим образом:

@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
KeyStoreKeyFactory keyStoreKeyFactory =
new KeyStoreKeyFactory(new ClassPathResource("mytest.jks"), "mypass".toCharArray());
converter.setKeyPair(keyStoreKeyFactory.getKeyPair("mytest"));
return converter;
}

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

Наконец, нам нужно настроить наш сервер ресурсов для использования открытого ключа — следующим образом:

@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
Resource resource = new ClassPathResource("public.txt");
String publicKey = null;
try {
publicKey = IOUtils.toString(resource.getInputStream());
} catch (final IOException e) {
throw new RuntimeException(e);
}
converter.setVerifierKey(publicKey);
return converter;
}

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

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

Полную реализацию этого руководства можно найти в проекте github — это проект на основе Eclipse, поэтому его легко импортировать и запускать как есть.