wake-up-neo.com

So rufen Sie benutzerdefinierte Benutzerinformationen vom OAuth2-Berechtigungsserver / Benutzerendpunkt ab

Ich habe einen Ressourcenserver mit der Annotation @EnableResourceServer Konfiguriert und er bezieht sich wie folgt auf den Autorisierungsserver über den Parameter user-info-uri:

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


Der Autorisierungsserver/Benutzer-Endpunkt gibt eine Erweiterung von org.springframework.security.core.userdetails.User Zurück, die z. eine E-Mail:

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


Immer wenn auf einen Resource Server-Endpunkt zugegriffen wird, überprüft Spring das Zugriffstoken im Hintergrund, indem es den Endpunkt des Authorization Servers /user Aufruft und tatsächlich die angereicherten Benutzerinformationen zurückgibt (die z. B. E-Mail-Informationen enthalten, I ' habe das mit Wireshark verifiziert).

Die Frage ist also, wie ich diese benutzerdefinierten Benutzerinformationen erhalten kann, ohne den Endpunkt /user Des Autorisierungsservers explizit erneut aufzurufen. Speichert Spring es nach der Autorisierung lokal auf dem Ressourcenserver oder wie lässt sich diese Art der Speicherung von Benutzerinformationen am besten implementieren, wenn nichts sofort verfügbar ist?

18
Sergey Pauk

Die Lösung ist die Implementierung eines benutzerdefinierten 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

Stellen Sie einfach Ihre benutzerdefinierte Implementierung als Bean bereit und diese wird anstelle der Standardimplementierung verwendet.

In diesen UserInfoTokenServices können Sie das principal wie gewünscht erstellen.

Diese UserInfoTokenServices werden verwendet, um die UserDetails aus der Antwort des /users - Endpunkts Ihres Autorisierungsservers zu extrahieren. Wie Sie in sehen können

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

Standardmäßig werden nur die in PRINCIPAL_KEYS Angegebenen Eigenschaften extrahiert. Und genau das ist dein Problem. Sie müssen mehr als nur den Benutzernamen oder den Namen Ihrer Immobilie extrahieren. Suchen Sie also nach weiteren Schlüsseln.

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

Verdrahtung:

@Autowired
private ResourceServerProperties sso;

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

!! UPDATE mit Spring Boot 1.4 wird immer einfacher !!

Mit Spring Boot 1.4.0 wurde ein PrincipalExtractor eingeführt. Diese Klasse sollte implementiert werden, um ein benutzerdefiniertes Prinzipal zu extrahieren (siehe Versionshinweise zu Spring Boot 1.4 ).

20
Yannic Klem

Alle Daten befinden sich bereits im Hauptobjekt, es ist keine zweite Anforderung erforderlich. Geben Sie nur das zurück, was Sie benötigen. Ich benutze die folgende Methode für die Facebook-Anmeldung:

@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
user2802927

Auf dem Ressourcenserver können Sie eine CustomPrincipal-Klasse wie folgt erstellen:

public class CustomPrincipal {

    public CustomPrincipal(){};

    private String email;

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

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

}

Implementieren Sie einen CustomUserInfoTokenServices wie folgt:

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

}

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

}

Definieren Sie in Ihrer @Configuration-Datei eine Bean wie diese

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

Und in der Resource Server-Konfiguration:

@Configuration
public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter {


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

    //etc....

Wenn alles richtig eingestellt ist, können Sie in Ihrem Controller Folgendes tun:

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

Hoffe das hilft.

3
Paolo Mastinu

Sie können JWT-Token verwenden. Sie benötigen keinen Datenspeicher, in dem alle Benutzerinformationen gespeichert sind. Stattdessen können Sie zusätzliche Informationen in das Token selbst kodieren. Wenn das Token dekodiert ist, kann die App mithilfe des Principal-Objekts auf alle diese Informationen zugreifen

1
vladsfl

Wir rufen es von der getContext-Methode von SecurityContextHolder ab, die statisch ist und daher von überall abgerufen werden kann.

// 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;
}
1
Jose Martinez

Eine Map -Darstellung des vom userdetails-Endpunkt zurückgegebenen JSON-Objekts ist über das Authentication -Objekt verfügbar, das den Principal darstellt:

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

Wenn Sie es für die Protokollierung, Speicherung oder Zwischenspeicherung erfassen möchten, empfehle ich, es durch Implementierung eines ApplicationListener zu erfassen. Beispielsweise:

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

  }
}

Wenn Sie die Extraktion des Principals aus dem JSON oder den Berechtigungen speziell anpassen möchten, können Sie org.springframework.boot.autoconfigure.security.oauth2.resource.PrincipalExtractor und/ org.springframework.boot.autoconfigure.security.oauth2.resource.AuthoritiesExtractor beziehungsweise.

Dann, in einem @Configuration Klasse würden Sie Ihre Implementierungen als Beans verfügbar machen:

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

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