Как я могу проверить данные пользователя токена OAuth 2.0 в аннотации @PreAuthorize в службе Spring Boot REST

Мне нужно сделать проверку в аннотации @PreAuthorize. Что-то типа:

@PreAuthorize("hasRole('ROLE_VIEWER') or hasRole('ROLE_EDITOR')")

Это нормально, но мне также нужно проверить некоторые данные пользователя, хранящиеся в токене OAuth 2.0, с данными в пути запроса, поэтому мне нужно будет сделать что-то вроде (oauthToken.userDetails — это просто пример:

@PreAuthorize("#pathProfileId.equals(oauthToken.userDetails.profileId)")

(profileId не является идентификатором пользователя или именем пользователя, это данные пользователя, которые мы добавляем в токен OAuth при его создании)

Каков самый простой способ сделать свойства токена OAuth видимыми на предварительно авторизованном языке выражений безопасности аннотаций?


person icordoba    schedule 09.07.2019    source источник


Ответы (1)


У вас есть два варианта:

1-

Установка экземпляра UserDetailsService в DefaultUserAuthenticationConverter и установка конвертера в JwtAccessTokenConverter, поэтому, когда Spring вызывает метод extractAuthentication из DefaultUserAuthenticationConverter, он нашел (userDetailsService != null), поэтому он получает весь объект UserDetails, вызывая реализацию loadUserByUsername при вызове этой строки:

userDetailsService.loadUserByUsername((String) map.get(ИМЯ ПОЛЬЗОВАТЕЛЯ))

реализован в следующем методе внутри класса Spring org.springframework.security.oauth2.provider.token.DefaultUserAuthenticationConverter.java, но просто добавил его, чтобы уточнить, как Spring получает основной объект из карты (сначала получая его по имени пользователя и если userDetailsService не нулевой, поэтому он получает весь объект):

//Note: This method implemented by spring but just putting it to show where spring exctract principal object and how extracting it
public Authentication extractAuthentication(Map<String, ?> map) {
        if (map.containsKey(USERNAME)) {
            Object principal = map.get(USERNAME);
            Collection<? extends GrantedAuthority> authorities = getAuthorities(map);
            if (userDetailsService != null) {
                UserDetails user = userDetailsService.loadUserByUsername((String) map.get(USERNAME));
                authorities = user.getAuthorities();
                principal = user;
            }
            return new UsernamePasswordAuthenticationToken(principal, "N/A", authorities);
        }
        return null;
    }

Итак, что вам нужно реализовать в своем микросервисе:

@Bean//this method just used with token store bean example: new JwtTokenStore(tokenEnhancer());
public JwtAccessTokenConverter tokenEnhancer() {
    /**
    * CustomTokenConverter is a class extends JwtAccessTokenConverter 
    * which override "enhance" to add extra information to OAuth2AccessToken after
    * authenticate the user and get it by loadUserByUsername implementation 
    * like profileId in your case
    **/  
    JwtAccessTokenConverter converter = new CustomTokenConverter();

    DefaultAccessTokenConverter datc = new DefaultAccessTokenConverter();
    datc.setUserTokenConverter(userAuthenticationConverter());
    converter.setAccessTokenConverter(datc);

    //Other method code implementation....
}

@Autowired
private UserDetailsService userDetailsService;

@Bean
public UserAuthenticationConverter userAuthenticationConverter() {
    DefaultUserAuthenticationConverter duac = new DefaultUserAuthenticationConverter();
    duac.setUserDetailsService(userDetailsService);
    return duac;
 }

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


2-

Если по какой-либо причине вы видите, что лучше не обращаться к базе данных в каждом запросе, и нет проблем с выполнением необходимых данных, таких как profileId, из токена, переданного в запросе.

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

Таким образом, это означает, что если права доступа пользователя изменились после создания токена, @PreAuthorize не будет проверять новые права доступа, поскольку они не будут удалены или добавлены к токену, и вам придется ждать, пока старый токен станет недействительным или срок его действия истечет, поэтому пользователь будет вынужден снова выполнить службу, чтобы получить новый токен oauth.

В любом случае, во втором варианте вам нужно только переопределить метод extractAuthentication внутри класса CustomTokenConverter, расширяет JwtAccessTokenConverter и забыть о настройке преобразователя токена доступа converter .setAccessTokenConverter из метода tokenEnhancer() в первом варианте, и вот весь CustomTokenConverter, который вы можете использовать для чтения данных из токена и возврата основного объекта, а не просто строки имени пользователя:

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;

public class CustomTokenConverter extends JwtAccessTokenConverter {

    // This is the method you need to override to read data direct from token passed in request
    @Override
    public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
        OAuth2Authentication authentication = super.extractAuthentication(map);

        Object userIdObj = map.get(AuthenticationUtils.USER_ID);
        UUID userId = userIdObj != null ? UUID.fromString(userIdObj.toString()) : null;
        Object profileIdObj = map.get(AuthenticationUtils.PROFILE_ID);
        UUID profileId = profileIdObj != null ? UUID.fromString(profileIdObj.toString()) : null;
        Object firstNameObj = map.get(AuthenticationUtils.FIRST_NAME);
        String firstName = firstNameObj != null ? String.valueOf(firstNameObj) : null;
        Object lastNameObj = map.get(AuthenticationUtils.LAST_NAME);
        String lastName = lastNameObj != null ? String.valueOf(lastNameObj) : null;

        JwtUser principal = new JwtUser(userId, profileId, authentication.getUserAuthentication().getName(), "N/A", authentication.getUserAuthentication().getAuthorities(), firstName, lastName);

        authentication = new OAuth2Authentication(authentication.getOAuth2Request(),
                new UsernamePasswordAuthenticationToken(principal, "N/A", authentication.getUserAuthentication().getAuthorities()));
        return authentication;
    }

    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        JwtUser user = (JwtUser) authentication.getPrincipal();
        Map<String, Object> info = new LinkedHashMap<>(accessToken.getAdditionalInformation());
        if (user.getId() != null)
            info.put(AuthenticationUtils.USER_ID, user.getId());
        if (user.getProfileId() != null)
            info.put(AuthenticationUtils.PROFILE_ID, user.getProfileId());
        if (isNotNullNotEmpty(user.getFirstName()))
            info.put(AuthenticationUtils.FIRST_NAME, user.getFirstName());
        if (isNotNullNotEmpty(user.getLastName()))
            info.put(AuthenticationUtils.LAST_NAME, user.getLastName());

        DefaultOAuth2AccessToken customAccessToken = new DefaultOAuth2AccessToken(accessToken);
        customAccessToken.setAdditionalInformation(info);
        return super.enhance(customAccessToken, authentication);
    }

    private boolean isNotNullNotEmpty(String str) {
        return Optional.ofNullable(str).map(String::trim).map(string -> !str.isEmpty()).orElse(false);
    }

}

Наконец: угадайте, откуда я знаю, что вы спрашиваете о JWT, используемом с OAuth2?

Потому что я часть вашей компании :P и вы это знаете :P

person mibrahim.iti    schedule 23.07.2019