Подтвердить что ты не робот

Как получить пользовательскую информацию пользователя от сервера авторизации сервера/пользователя OAuth2

У меня есть сервер ресурсов, настроенный с аннотацией @EnableResourceServer, и он ссылается на сервер авторизации через параметр user-info-uri следующим образом:

security:
  oauth2:
    resource:
      user-info-uri: http://localhost:9001/user


Терминал сервера авторизации/пользователя возвращает расширение org.springframework.security.core.userdetails.User, которое имеет, например, электронная почта:

{  
   "password":null,
   "username":"myuser",
    ...
   "email":"[email protected]"
}


Когда доступ к какой-либо конечной точке сервера ресурсов Spring проверяет токен доступа за кулисами, вызывая конечную точку сервера авторизации /user, и он фактически возвращает информацию о обогащенном пользователе (которая содержит, например, электронную почту, я проверил это с Wireshark).

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

4b9b3361

Ответ 1

Решением является реализация пользовательского UserInfoTokenServices

https://github.com/spring-projects/spring-boot/blob/master/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/UserInfoTokenServices.java

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

Внутри этого UserInfoTokenServices вы можете построить principal так, как хотите.

Этот UserInfoTokenServices используется для извлечения UserDetails из ответа конечной точки /users вашего сервера авторизации. Как вы можете видеть в

private Object getPrincipal(Map<String, Object> map) {
    for (String key : PRINCIPAL_KEYS) {
        if (map.containsKey(key)) {
            return map.get(key);
        }
    }
    return "unknown";
}

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

private Object getPrincipal(Map<String, Object> map) {
    MyUserDetails myUserDetails = new myUserDetails();
    for (String key : PRINCIPAL_KEYS) {
        if (map.containsKey(key)) {
            myUserDetails.setUserName(map.get(key));
        }
    }
    if( map.containsKey("email") {
        myUserDetails.setEmail(map.get("email"));
    }
    //and so on..
    return myUserDetails;
}

Электропроводка:

@Autowired
private ResourceServerProperties sso;

@Bean
public ResourceServerTokenServices myUserInfoTokenServices() {
    return new MyUserInfoTokenServices(sso.getUserInfoUri(), sso.getClientId());
}

! UPDATE с помощью Spring Загрузка 1.4 все становится проще!

С Spring Загружен 1.4.0 a PrincipalExtractor. Этот класс должен быть реализован для извлечения пользовательского принципала (см. Spring Примечания к выпуску Boot 1.4).

Ответ 2

Все данные уже находятся в объекте Principal, второй запрос не требуется. Верните только то, что вам нужно. Я использую метод ниже для входа в Facebook:

@RequestMapping("/sso/user")
@SuppressWarnings("unchecked")
public Map<String, String> user(Principal principal) {
    if (principal != null) {
        OAuth2Authentication oAuth2Authentication = (OAuth2Authentication) principal;
        Authentication authentication = oAuth2Authentication.getUserAuthentication();
        Map<String, String> details = new LinkedHashMap<>();
        details = (Map<String, String>) authentication.getDetails();
        logger.info("details = " + details);  // id, email, name, link etc.
        Map<String, String> map = new LinkedHashMap<>();
        map.put("email", details.get("email"));
        return map;
    }
    return null;
}

Ответ 3

На сервере ресурсов вы можете создать класс CustomPrincipal.

public class CustomPrincipal {

    public CustomPrincipal(){};

    private String email;

    //Getters and Setters
    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

}

Внедрить CustomUserInfoTokenServices, как это:

public class CustomUserInfoTokenServices implements ResourceServerTokenServices {

    protected final Log logger = LogFactory.getLog(getClass());

    private final String userInfoEndpointUrl;

    private final String clientId;

    private OAuth2RestOperations restTemplate;

    private String tokenType = DefaultOAuth2AccessToken.BEARER_TYPE;

    private AuthoritiesExtractor authoritiesExtractor = new FixedAuthoritiesExtractor();

    private PrincipalExtractor principalExtractor = new CustomPrincipalExtractor();

    public CustomUserInfoTokenServices(String userInfoEndpointUrl, String clientId) {
        this.userInfoEndpointUrl = userInfoEndpointUrl;
        this.clientId = clientId;
    }

    public void setTokenType(String tokenType) {
        this.tokenType = tokenType;
    }

    public void setRestTemplate(OAuth2RestOperations restTemplate) {
        this.restTemplate = restTemplate;
    }

    public void setAuthoritiesExtractor(AuthoritiesExtractor authoritiesExtractor) {
        Assert.notNull(authoritiesExtractor, "AuthoritiesExtractor must not be null");
        this.authoritiesExtractor = authoritiesExtractor;
    }

    public void setPrincipalExtractor(PrincipalExtractor principalExtractor) {
        Assert.notNull(principalExtractor, "PrincipalExtractor must not be null");
        this.principalExtractor = principalExtractor;
    }

    @Override
    public OAuth2Authentication loadAuthentication(String accessToken)
            throws AuthenticationException, InvalidTokenException {
        Map<String, Object> map = getMap(this.userInfoEndpointUrl, accessToken);
        if (map.containsKey("error")) {
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("userinfo returned error: " + map.get("error"));
            }
            throw new InvalidTokenException(accessToken);
        }
        return extractAuthentication(map);
    }

    private OAuth2Authentication extractAuthentication(Map<String, Object> map) {
        Object principal = getPrincipal(map);
        List<GrantedAuthority> authorities = this.authoritiesExtractor
                .extractAuthorities(map);
        OAuth2Request request = new OAuth2Request(null, this.clientId, null, true, null,
                null, null, null, null);
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
                principal, "N/A", authorities);
        token.setDetails(map);
        return new OAuth2Authentication(request, token);
    }

    /**
     * Return the principal that should be used for the token. The default implementation
     * delegates to the {@link PrincipalExtractor}.
     * @param map the source map
     * @return the principal or {@literal "unknown"}
     */
    protected Object getPrincipal(Map<String, Object> map) {

        CustomPrincipal customPrincipal = new CustomPrincipal();
        if( map.containsKey("principal") ) {
            Map<String, Object> principalMap = (Map<String, Object>) map.get("principal");
            customPrincipal.setEmail((String) principalMap.get("email"));

        }
        //and so on..
        return customPrincipal;

        /*
        Object principal = this.principalExtractor.extractPrincipal(map);
        return (principal == null ? "unknown" : principal);
        */

    }

    @Override
    public OAuth2AccessToken readAccessToken(String accessToken) {
        throw new UnsupportedOperationException("Not supported: read access token");
    }

    @SuppressWarnings({ "unchecked" })
    private Map<String, Object> getMap(String path, String accessToken) {
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Getting user info from: " + path);
        }
        try {
            OAuth2RestOperations restTemplate = this.restTemplate;
            if (restTemplate == null) {
                BaseOAuth2ProtectedResourceDetails resource = new BaseOAuth2ProtectedResourceDetails();
                resource.setClientId(this.clientId);
                restTemplate = new OAuth2RestTemplate(resource);
            }
            OAuth2AccessToken existingToken = restTemplate.getOAuth2ClientContext()
                    .getAccessToken();
            if (existingToken == null || !accessToken.equals(existingToken.getValue())) {
                DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(
                        accessToken);
                token.setTokenType(this.tokenType);
                restTemplate.getOAuth2ClientContext().setAccessToken(token);
            }
            return restTemplate.getForEntity(path, Map.class).getBody();
        }
        catch (Exception ex) {
            this.logger.warn("Could not fetch user details: " + ex.getClass() + ", "
                    + ex.getMessage());
            return Collections.<String, Object>singletonMap("error",
                    "Could not fetch user details");
        }
    }

}

Пользовательский основной инструмент:

public class CustomPrincipalExtractor implements PrincipalExtractor {

    private static final String[] PRINCIPAL_KEYS = new String[] {
            "user", "username", "principal",
            "userid", "user_id",
            "login", "id",
            "name", "uuid",
            "email"};

    @Override
    public Object extractPrincipal(Map<String, Object> map) {
        for (String key : PRINCIPAL_KEYS) {
            if (map.containsKey(key)) {
                return map.get(key);
            }
        }
        return null;
    }

    @Bean
    public DaoAuthenticationProvider daoAuthenticationProvider() {
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();

        daoAuthenticationProvider.setForcePrincipalAsString(false);
        return daoAuthenticationProvider;
    }

}

В вашем файле @Configuration определите bean как этот

@Bean
    public ResourceServerTokenServices myUserInfoTokenServices() {
        return new CustomUserInfoTokenServices(sso.getUserInfoUri(), sso.getClientId());
    }

И в конфигурации сервера ресурсов:

@Configuration
public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter {


    @Override
    public void configure(ResourceServerSecurityConfigurer config) {
        config.tokenServices(myUserInfoTokenServices());
    }

    //etc....

Если все настроено правильно, вы можете сделать что-то подобное в своем контроллере:

String userEmail = ((CustomPrincipal) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getEmail();

Надеюсь, что это поможет.

Ответ 4

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

Ответ 5

A Map представление объекта JSON, возвращаемого конечной точкой userdetails, доступно из объекта Authentication, который представляет Принципала:

Map<String, Object> details = (Map<String,Object>)oauth2.getUserAuthentication().getDetails();

Если вы хотите записать его для ведения журнала, хранения или кеширования, я бы рекомендовал его захватить, выполнив ApplicationListener. Например:

@Component
public class AuthenticationSuccessListener implements ApplicationListener<AuthenticationSuccessEvent> {

  private Logger log = LoggerFactory.getLogger(this.getClass()); 

  @Override
  public void onApplicationEvent(AuthenticationSuccessEvent event) {
    Authentication auth = event.getAuthentication();
    log.debug("Authentication class: "+auth.getClass().toString());

    if(auth instanceof OAuth2Authentication){

        OAuth2Authentication oauth2 = (OAuth2Authentication)auth;

        @SuppressWarnings("unchecked")
        Map<String, Object> details = (Map<String, Object>)oauth2.getUserAuthentication().getDetails();         

        log.info("User {} logged in: {}", oauth2.getName(), details);
        log.info("User {} has authorities {} ", oauth2.getName(), oauth2.getAuthorities());



    } else {
        log.warn("User authenticated by a non OAuth2 mechanism. Class is "+auth.getClass());
    }

  }
}

Если вы специально хотите настроить извлечение принципала из JSON или властей, вы можете реализовать org.springframework.boot.autoconfigure.security.oauth2.resource.PrincipalExtractor и /org.springframework.boot.autoconfigure.security.oauth2.resource.AuthoritiesExtractor соответственно.

Затем в классе @Configuration вы можете представить свои реализации как beans:

@Bean
public PrincipalExtractor merckPrincipalExtractor() {
        return new MyPrincipalExtractor();
}

@Bean 
public AuthoritiesExtractor merckAuthoritiesExtractor() {
        return new MyAuthoritiesExtractor(); 
}