Как сгенерировать токен доступа JWT с некоторыми настраиваемыми утверждениями в нем?

Я пытаюсь заставить мой сервер авторизации сгенерировать токен доступа JWT с некоторыми настраиваемыми утверждениями в нем.

Вот как выглядит токен-носитель, возвращаемый конечной точкой /auth/token сервера авторизации: 51aea31c-6b57-4c80-9d19-a72e15cb2bb7

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

И при использовании в последующих запросах к серверу ресурсов выдает ошибку: Cannot convert access token to JSON

Я использую следующие зависимости:

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.1.RELEASE</version>
    <relativePath/>
  </parent>

  <dependency>
    <groupId>org.springframework.security.oauth.boot</groupId>
    <artifactId>spring-security-oauth2-autoconfigure</artifactId>
    <version>2.1.2.RELEASE</version>
  </dependency>

Сервер авторизации настроен так:

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
  endpoints
  .tokenServices(defaultTokenServices())
  .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)
  .accessTokenConverter(jwtAccessTokenConverter())
  .userDetailsService(userDetailsService);

  endpoints
  .pathMapping("/oauth/token", RESTConstants.SLASH + DomainConstants.AUTH + RESTConstants.SLASH + DomainConstants.TOKEN);

  TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
  tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer(), jwtAccessTokenConverter()));
  endpoints
  .tokenStore(tokenStore())
  .tokenEnhancer(tokenEnhancerChain)
  .authenticationManager(authenticationManager);
}

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

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

@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
  JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
  jwtAccessTokenConverter.setKeyPair(new KeyStoreKeyFactory(new ClassPathResource(jwtProperties.getSslKeystoreFilename()), jwtProperties.getSslKeystorePassword().toCharArray()).getKeyPair(jwtProperties.getSslKeyPair()));
return jwtAccessTokenConverter;
}

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

И он использует класс:

class CustomTokenEnhancer implements TokenEnhancer {

  @Autowired
  private TokenAuthenticationService tokenAuthenticationService;

  // Add user information to the token
  @Override
  public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
    User user = (User) authentication.getPrincipal();
    Map<String, Object> info = new LinkedHashMap<String, Object>(accessToken.getAdditionalInformation());
    info.put(CommonConstants.JWT_CLAIM_USER_EMAIL, user.getEmail().getEmailAddress());
    info.put(CommonConstants.JWT_CLAIM_USER_FULLNAME, user.getFirstname() + " " + user.getLastname());
    info.put("scopes", authentication.getAuthorities().stream().map(s -> s.toString()).collect(Collectors.toList()));
    info.put("organization", authentication.getName());
    DefaultOAuth2AccessToken customAccessToken = new DefaultOAuth2AccessToken(accessToken);
    customAccessToken.setAdditionalInformation(info);
    customAccessToken.setExpiration(tokenAuthenticationService.getExpirationDate());
    return customAccessToken;
  }

}

Еще у меня есть класс:

@Configuration
class CustomOauth2RequestFactory extends DefaultOAuth2RequestFactory {

  @Autowired
  private TokenStore tokenStore;

  @Autowired
  private UserDetailsService userDetailsService;

  public CustomOauth2RequestFactory(ClientDetailsService clientDetailsService) {
    super(clientDetailsService);
  }

  @Override
  public TokenRequest createTokenRequest(Map<String, String> requestParameters, ClientDetails authenticatedClient) {
    if (requestParameters.get("grant_type").equals("refresh_token")) {
      OAuth2Authentication authentication = tokenStore
          .readAuthenticationForRefreshToken(tokenStore.readRefreshToken(requestParameters.get("refresh_token")));
      SecurityContextHolder.getContext()
          .setAuthentication(new UsernamePasswordAuthenticationToken(authentication.getName(), null,
              userDetailsService.loadUserByUsername(authentication.getName()).getAuthorities()));
    }
    return super.createTokenRequest(requestParameters, authenticatedClient);
  }

}

ОБНОВЛЕНИЕ: я также пробовал альтернативный способ указания настраиваемого утверждения:

@Component
class CustomAccessTokenConverter extends JwtAccessTokenConverter {

    @Autowired
  private TokenAuthenticationService tokenAuthenticationService;

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

  @Override
  public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
    User user = (User) authentication.getPrincipal();
    Map<String, Object> info = new LinkedHashMap<String, Object>(accessToken.getAdditionalInformation());
    info.put(CommonConstants.JWT_CLAIM_USER_EMAIL, user.getEmail().getEmailAddress());
    info.put(CommonConstants.JWT_CLAIM_USER_FULLNAME, user.getFirstname() + " " + user.getLastname());
    info.put("scopes", authentication.getAuthorities().stream().map(s -> s.toString()).collect(Collectors.toList()));
    info.put("organization", authentication.getName());
    DefaultOAuth2AccessToken customAccessToken = new DefaultOAuth2AccessToken(accessToken);
    customAccessToken.setAdditionalInformation(info);
    customAccessToken.setExpiration(tokenAuthenticationService.getExpirationDate());
    return super.enhance(customAccessToken, authentication);
  }

}

с его названием как:

endpoints
.tokenStore(tokenStore())
.tokenEnhancer(jwtAccessTokenConverter())
.accessTokenConverter(jwtAccessTokenConverter())

но это ничего не изменило, и ошибка осталась идентичной.

При работе с отладчиком ни одно из этих двух переопределений усилителя не вызывается.

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Версия Java на основе версии загрузки
Версия Java на основе версии загрузки
Если вы зайдете на официальный сайт Spring Boot , там представлен start.spring.io , который упрощает создание проектов Spring Boot, как показано ниже.
Документирование API с помощью Swagger на Springboot
Документирование API с помощью Swagger на Springboot
В предыдущей статье мы уже узнали, как создать Rest API с помощью Springboot и MySql .
7
0
9 577
2

Ответы 2

Если вы поделились образцом проекта, вам будет легче найти точное решение. Вместо этого вы установили точку останова на .tokenEnhancer(tokenEnhancerChain), и она сработала?

Я создал супер простой образец проекта, который показывает, как вызывается tokenEnhancer.

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder passwordEncoder(){
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    @Bean //by exposing this bean, password grant becomes enabled
    public AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService());
    }

    @Bean
    public UserDetailsService userDetailsService() {
        return new InMemoryUserDetailsManager(
            builder()
                .username("user")
                .password("{bcrypt}$2a$10$C8c78G3SRJpy268vInPUFu.3lcNHG9SaNAPdSaIOy.1TJIio0cmTK") //123
                .roles("USER")
                .build(),
            builder()
                .username("admin")
                .password("{bcrypt}$2a$10$XvWhl0acx2D2hvpOPd/rPuPA48nQGxOFom1NqhxNN9ST1p9lla3bG") //password
                .roles("ADMIN")
                .build()
        );
    }

    @EnableAuthorizationServer
    public static class Oauth2SecurityConfig extends AuthorizationServerConfigurerAdapter {
        private final PasswordEncoder passwordEncoder;
        private final AuthenticationManager authenticationManager;

        public Oauth2SecurityConfig(PasswordEncoder passwordEncoder,
                                    AuthenticationManager authenticationManager) {
            this.passwordEncoder = passwordEncoder;
            this.authenticationManager = authenticationManager;
        }

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

        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            endpoints
                .tokenEnhancer(tokenEnhancer())
                .authenticationManager(authenticationManager)
            ;

        }

        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            InMemoryClientDetailsService clientDetails = new InMemoryClientDetailsService();
            BaseClientDetails client = new BaseClientDetails(
                "testclient",
                null,
                "testscope,USER,ADMIN",
                "password",
                null
            );
            client.setClientSecret(passwordEncoder.encode("secret"));
            clientDetails.setClientDetailsStore(
                Collections.singletonMap(
                    client.getClientId(),
                    client
                )
            );
            clients.withClientDetails(clientDetails);
        }

    }

}

В этом примере также есть модульный тест

@Test
@DisplayName("perform a password grant")
void passwordGrant() throws Exception {
    mvc.perform(
        post("/oauth/token")
            .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
            .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
            .param("username", "admin")
            .param("password", "password")
            .param("grant_type", "password")
            .param("response_type", "token")
            .param("client_id", "testclient")
            .header("Authorization", "Basic "+ Base64.encodeBase64String("testclient:secret".getBytes()))
    )
        .andExpect(status().isOk())
        .andExpect(content().string(containsString("\"full_name\":\"Joe Schmoe\"")))
        .andExpect(content().string(containsString("\"email\":\"[email protected]\"")))
    ;
}

Не стесняйтесь проверить образец проекта и посмотреть, работает ли он для вас.,

Да, отладчик останавливается на строке .tokenEnhancer(tokenEnhancerChain), но не останавливается на двух моих разных методах enhancer.

Stephane 14.01.2019 23:05

У меня есть полный проект на https://github.com/stephaneeybert/user-resthttps://github.com/stephaneeybert/user-datahttps://github.com/stephaneeybert/toolbox Завтра я попробую ваш пример проекта.

Stephane 14.01.2019 23:07

Я проверил первый проект, и mvn clean package не работает. Отсутствуют зависимости. Почти как будто он ожидает зависимости от самого себя The POM for com.thalasoft:user-data:jar:0.0.1-SNAPSHOT is missing, no dependency information available Моя рекомендация - упростить кому-то использование вашего кода. Возьмите образец, вы его клонируете, импортируете, и все проекты появятся в вашей среде IDE.

Filip Hanik VMware 15.01.2019 03:18

Вы сначала собрали toolbox, а затем проекты user-data?

Stephane 15.01.2019 05:00

привет Стефан, я не сделал. Есть ли причина, по которой у вас нет одного репо, родительского POM с тремя модулями, которые собираются вместе? Я все еще могу дойти до этого, но мне нужно снизить приоритет проверки вашего кода из-за сложности настройки.

Filip Hanik VMware 15.01.2019 19:01

Хорошо, не беспокойтесь, я полностью понимаю. Проект разделен на части, чтобы при необходимости можно было когда-нибудь повторно использовать слой сохраняемости. Чтобы запустить его, нужно просто клонировать и собрать 3 проекта.

Stephane 15.01.2019 20:00

Эта цель может быть достигнута с помощью одного репо. взгляните на репозиторий Spring Security. опубликованные субмодули как отдельные артефакты. достигает того, что вы пытаетесь сделать

Filip Hanik VMware 15.01.2019 20:01

Чтобы создать сервер Spring Boot с OAuth2, JWT и дополнительными утверждениями, мы должны:

1) Добавить в проект зависимость:

<dependency>
    <groupId>org.springframework.security.oauth.boot</groupId>
    <artifactId>spring-security-oauth2-autoconfigure</artifactId>
    <version>2.1.2.RELEASE</version>
</dependency>

2) Добавьте конфигурацию веб-безопасности (чтобы опубликовать bean-компонент AuthenticationManager - он будет использован на следующем шаге), например:

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    @Autowired
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(username -> AuthUser.with()
                .username(username)
                .password("{noop}" + username)
                .email(username + "@mail.com")
                .authority(AuthUser.Role.values()[ThreadLocalRandom.current().nextInt(2)])
                .build()
        );
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
}

Здесь для тестирования реализован простой UserDetailsService. Он работает со следующим простым объектом «Пользователь» и перечислением Role, реализующим интерфейс GrantedAuthority. AuthUser имеет только одно дополнительное свойство email, которое будет добавлено к токену JWT в качестве утверждения.

@Value
@EqualsAndHashCode(callSuper = false)
public class AuthUser extends User {

    private String email;

    @Builder(builderMethodName = "with")
    public AuthUser(final String username, final String password, @Singular final Collection<? extends GrantedAuthority> authorities, final String email) {
        super(username, password, authorities);
        this.email = email;
    }

    public enum Role implements GrantedAuthority {
        USER, ADMIN;

        @Override
        public String getAuthority() {
            return this.name();
        }
    }
}

3) Настройте сервер авторизации и включите сервер ресурсов:

@Configuration
@EnableAuthorizationServer
@EnableResourceServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {

    public static final String TOKEN_KEY = "abracadabra";

    private final AuthenticationManager authenticationManager;

    public AuthServerConfig(final AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clientDetailsService) throws Exception {
        clientDetailsService.inMemory()
                .withClient("client")
                .secret("{noop}")
                .scopes("*")
                .authorizedGrantTypes("password", "refresh_token")
                .accessTokenValiditySeconds(60 * 2) // 2 min
                .refreshTokenValiditySeconds(60 * 60); // 60 min
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        TokenEnhancerChain chain = new TokenEnhancerChain();
        chain.setTokenEnhancers(List.of(tokenEnhancer(), tokenConverter()));
        endpoints
                .tokenStore(tokenStore())
                .reuseRefreshTokens(false)
                .tokenEnhancer(chain)
                .authenticationManager(authenticationManager);
    }

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

    @Bean
    public JwtAccessTokenConverter tokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(TOKEN_KEY);
        converter.setAccessTokenConverter(authExtractor());
        return converter;
    }

    private TokenEnhancer tokenEnhancer() {
        return (accessToken, authentication) -> {
                if (authentication != null && authentication.getPrincipal() instanceof AuthUser) {
                    AuthUser authUser = (AuthUser) authentication.getPrincipal();
                    Map<String, Object> additionalInfo = new HashMap<>();
                    additionalInfo.put("user_email", authUser.getEmail());
                    ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
                }
                return accessToken;
            };
    }

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

Здесь реализован простой ClientDetailsService. Он содержит только одного клиента с именем «client», пустым паролем и предоставленными типами «пароль» и «refresh_token». Это дает нам возможность создать новый токен доступа и обновить его. (Для работы со многими типами клиентов или в других сценариях вам необходимо реализовать более сложный и, возможно, постоянные варианты ClientDetailsService.)

Конечные точки авторизации настроены с помощью TokenEnhancerChain, который содержит tokenEnhancer и tokenConverter. Важно добавлять их в этой последовательности. Первый дополняет токен доступа дополнительными утверждениями (в нашем случае - адрес электронной почты пользователя). Второй создает токен JWT. В комплект endpoints входят простые JwtTokenStore, наши TokenEnhancerChain и authenticationManager.

Примечание для JwtTokenStore - если вы решите реализовать постоянный вариант магазина, вы можете найти дополнительную информацию здесь.

Последнее, что здесь есть, это authExtractor, который дает нам возможность извлекать заявки из токенов JWT входящих запросов.

Затем все настроено, мы можем запросить у нашего сервера токен доступа:

curl -i \
--user client: \
-H "Content-Type: application/x-www-form-urlencoded" \
-X POST \
-d "grant_type=password&username=user&password=user&scope=*" \
http://localhost:8080/oauth/token

Ответ:

{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2VtYWlsIjoidXNlckBtYWlsLmNvbSIsInVzZXJfbmFtZSI6InVzZXIiLCJzY29wZSI6WyIqIl0sImV4cCI6MTU0Nzc2NDIzOCwiYXV0aG9yaXRpZXMiOlsiQURNSU4iXSwianRpIjoiYzk1YzkzYTAtMThmOC00OGZjLWEzZGUtNWVmY2Y1YWIxMGE5IiwiY2xpZW50X2lkIjoiY2xpZW50In0.RWSGMC0w8tNafT28i2GLTnPnIiXfAlCdydEsNNZK-Lw",
    "token_type": "bearer",
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2VtYWlsIjoidXNlckBtYWlsLmNvbSIsInVzZXJfbmFtZSI6InVzZXIiLCJzY29wZSI6WyIqIl0sImF0aSI6ImM5NWM5M2EwLTE4ZjgtNDhmYy1hM2RlLTVlZmNmNWFiMTBhOSIsImV4cCI6MTU0Nzc2NzcxOCwiYXV0aG9yaXRpZXMiOlsiQURNSU4iXSwianRpIjoiZDRhNGU2ZjUtNDY2Mi00NGZkLWI0ZDgtZWE5OWRkMDJkYWI2IiwiY2xpZW50X2lkIjoiY2xpZW50In0.m7XvxwuPiTnPaQXAptLfi3CxN3imfQCVKyjmMCIPAVM",
    "expires_in": 119,
    "scope": "*",
    "user_email": "[email protected]",
    "jti": "c95c93a0-18f8-48fc-a3de-5efcf5ab10a9"
}

Если мы декодируем этот токен доступа на https://jwt.io/, мы увидим, что он содержит утверждение user_email:

{
  "user_email": "[email protected]",
  "user_name": "user",
  "scope": [
    "*"
  ],
  "exp": 1547764238,
  "authorities": [
    "ADMIN"
  ],
  "jti": "c95c93a0-18f8-48fc-a3de-5efcf5ab10a9",
  "client_id": "client"
}

Чтобы извлечь такое утверждение (и другие данные) из токена JWT входящих запросов, мы можем использовать следующий подход:

@RestController
public class DemoController {

    @GetMapping("/demo")
    public Map demo(OAuth2Authentication auth) {

        var details = (OAuth2AuthenticationDetails) auth.getDetails();
        //noinspection unchecked
        var decodedDetails = (Map<String, Object>) details.getDecodedDetails();

        return Map.of(
                "name", decodedDetails.get("user_name"),
                "email", decodedDetails.get("user_email"),
                "roles", decodedDetails.get("authorities")
        );
    }
}

Моя рабочая демонстрация: sb-jwt-oauth-demo

Связанная информация:

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

Stephane 20.01.2019 11:57

@Stephane Oauth - довольно сложная тема. Из-за этого непросто найти причину, по которой чужой код не работает. Поэтому я привел минимальный рабочий пример, чтобы вы могли сравнить его со своим и найти причину проблемы (или просто взять этот пример за основу). Основные моменты, на которые следует обратить внимание, - это tokenConverter (он создает токен JWT), tokenEnhancer (он добавляет утверждения к токену) и TokenEnhancerChain, который их объединяет.

Cepr0 20.01.2019 13:05

Да, я пытался увидеть разницу между нашими кодами или чем-то в вашем, что я не реализовал, но пока я все еще не вижу никакого решения.

Stephane 20.01.2019 18:11

@Stephane Я думаю, вы должны взять мой код за основу, а затем шаг за шагом расширять его своим кодом. Таким образом можно определить проблемное место.

Cepr0 20.01.2019 18:36

@ Cepr0 - этот образец добавляет к полезной нагрузке JWT И json. Мне это не кажется правильным. Он должен быть только в полезной нагрузке?

SledgeHammer 28.10.2019 04:49

@SledgeHammer Это интересный вопрос, требующий отдельного исследования.

Cepr0 28.10.2019 19:34

@ Cepr0 - нашел ответ здесь: stackoverflow.com/questions/46406905/…

SledgeHammer 28.10.2019 23:55

Другие вопросы по теме

JPA @JoinColumn не работает должным образом с REST
PageNotFound: не найдено сопоставление для HTTP-запроса с URI [../j_spring_security_check] в DispatcherServlet
Безопасность Spring: применить фильтр только к конечной точке
Настройка перехватчиков для выполнения «предварительных задач» при достижении конечных точек в веб-приложении на основе Spring
Метод сообщения формы не возвращается в контроллер при аутентификации входа Spring Securiry
Конфигурация Java безопасности Spring и несколько точек входа http
Spring Security OAuth2 Вход с использованием внешней страницы входа
Использование файла свойств для учетных данных пользователя с конфигурацией java в Spring Security
Как правильно включить CORS на серверной части с использованием пружинной безопасности. Также с использованием Apache cxf в проекте для REST framework
Как настроить многоуровневую аутентификацию для веб-службы RESTful с весенней загрузкой?