Comment obtenir des informations utilisateur personnalisées à partir du serveur d'autorisation OAuth2 / point d'arrivée de l'utilisateur

j'ai un serveur de ressources configuré avec @EnableResourceServer annotation et se réfère au serveur d'autorisation via user-info-uri paramètre comme suit:

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



Autorisation server / user endpoint renvoie une extension de org.springframework.security.core.userdetails.User qui a par exemple un e-mail:

{  
   "password":null,
   "username":"myuser",
    ...
   "email":"me@company.com"
}



Chaque fois qu'un endpoint du serveur de ressources est accédé, Spring vérifie le token d'accès dans les coulisses en appelant /user point de terminaison et il en fait obtient en arrière l'information d'utilisateur enrichie (qui contient par exemple l'information d'email, j'ai vérifié cela avec Wireshark).

alors la question Est Comment puis-je obtenir cette information utilisateur personnalisée sans un second appel explicite au serveur d'autorisation /user point de terminaison. Est-ce que Spring le stocke quelque part localement sur le serveur de ressources après autorisation ou Quelle est la meilleure façon d'implémenter ce genre d'information utilisateur stockant s'il n'y a rien disponible hors de la boîte?

18
demandé sur Sergey Pauk 2016-01-28 11:21:42

6 réponses

La solution est la mise en œuvre d'un custom 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

il suffit de fournir votre implémentation personnalisée comme un haricot et il sera utilisé à la place de celle par défaut.

dans ce UserInfoTokenServices vous pouvez construire le principal comme vous voulez de.

C'UserInfoTokenServices est utilisé pour extraire la UserDetails de la réponse de l' /usersendpoint de votre serveur d'autorisation. Comme vous pouvez le voir dans

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

Seulement les propriétés spécifiées à l' PRINCIPAL_KEYS sont extraites par défaut. Et c'est exactement votre problème. Vous devez extraire plus que le nom d'utilisateur ou quelque soit le nom de votre propriété. Alors regardez pour plus de touches.

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;
}

Câblage:

@Autowired
private ResourceServerProperties sso;

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

!!Mise à jour avec Botte de printemps 1.4 les choses deviennent plus faciles!!

Avec le Printemps de Démarrage 1.4.0 Principalextracteur a été introduit. Cette classe doit être implémentée pour extraire un principe personnalisé (voir Démarrage À Ressort 1.4 Notes De Mise À Jour).

18
répondu Yannic Klem 2016-08-10 13:17:58

Toutes les données sont déjà dans l'objet Principal, aucune demande n'est nécessaire. Ne rapportez que ce dont vous avez besoin. J'ai utiliser la méthode ci-dessous pour Facebook login:

@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;
}
5
répondu 2017-02-03 02:01:53

dans le serveur de ressources, vous pouvez créer une classe CustomPrincipal comme ceci:

public class CustomPrincipal {

    public CustomPrincipal(){};

    private String email;

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

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

}

implémenter un CustomUserInfoTokenServices comme ceci:

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");
        }
    }

}

Personnalisé PrincipalExtractor:

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;
    }

}

dans votre fichier @Configuration, définissez un bean comme celui-ci

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

et dans la Configuration du serveur de ressources:

@Configuration
public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter {


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

    //etc....

si tout est réglé correctement, vous pouvez faire quelque chose comme ça dans votre contrôleur:

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

J'espère que cela vous aidera.

2
répondu Paolo Mastinu 2017-06-12 14:55:39

vous pouvez utiliser des tokens JWT. Vous n'aurez pas besoin de datastore où toutes les informations utilisateur sont stockées mais vous pouvez encoder des informations supplémentaires dans le token lui-même. Lorsque le token est décodé, votre application pourra accéder à toutes ces informations en utilisant L'objet Principal

1
répondu vladsfl 2016-01-29 02:42:46

Map la représentation de l'objet JSON retourné par le paramètre userdetails est disponible à partir du Authentication objet qui représente le Principal:

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

si vous voulez le capturer pour la journalisation, le stockage ou la mise en cache, je vous recommande de le capturer en implémentant un ApplicationListener. Par exemple:

@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());
    }

  }
}

si vous voulez spécifiquement personnaliser l'extraction du principal du JSON ou des autorités, alors vous pouvez mettre en œuvre org.springframework.boot.autoconfigure.security.oauth2.resource.PrincipalExtractororg.springframework.boot.autoconfigure.security.oauth2.resource.AuthoritiesExtractor respectivement.

Puis, dans un @Configuration classe vous permet d'exposer vos implémentations de haricots:

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

@Bean 
public AuthoritiesExtractor merckAuthoritiesExtractor() {
        return new MyAuthoritiesExtractor(); 
}
0
répondu Mark 2017-09-20 17:53:58

nous le récupérons à partir de la méthode getContext de SecurityContextHolder, qui est statique, et peut donc être récupéré à partir de n'importe où.

// this is userAuthentication's principal
Map<?, ?> getUserAuthenticationFromSecurityContextHolder() {
    Map<?, ?> userAuthentication = new HashMap<>();
    try {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (!(authentication instanceof OAuth2Authentication)) {
            return userAuthentication;
        }
        OAuth2Authentication oauth2Authentication = (OAuth2Authentication) authentication;
        Authentication userauthentication = oauth2Authentication.getUserAuthentication();
        if (userauthentication == null) {
            return userAuthentication;
        }
        Map<?, ?> details = (HashMap<?, ?>) userauthentication.getDetails();    //this effect in the new RW OAUTH2 userAuthentication
        Object principal = details.containsKey("principal") ? details.get("principal") : userAuthentication; //this should be effect in the common OAUTH2 userAuthentication
        if (!(principal instanceof Map)) {
            return userAuthentication;
        }
        userAuthentication = (Map<?, ?>) principal;
    } catch (Exception e) {
        logger.error("Got exception while trying to obtain user info from security context.", e);
    }
    return userAuthentication;
}
0
répondu Jose Martinez 2018-05-03 14:16:10