Come ottenere informazioni utente personalizzate dal server di authorization OAuth2 / endpoint utente

Ho un server delle risorse configurato con @EnableResourceServer annotazione @EnableResourceServer e fa riferimento al server di authorization tramite user-info-uri parametro user-info-uri come segue:

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

Il server di authorization / endpoint utente restituisce un’estensione di org.springframework.security.core.userdetails.User che ha ad esempio un’e-mail:

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

Ogni volta che si accede a un endpoint del server delle risorse, Spring verifica il token di accesso dietro le quinte chiamando l’endpoint del server di authorization /user e recupera effettivamente le informazioni sull’utente arricchito (che contiene ad esempio informazioni sull’email, che ho verificato con Wireshark).

Quindi la domanda è: come posso ottenere queste informazioni utente personalizzate senza una seconda chiamata esplicita all’endpoint del server di authorization /user . Spring lo memorizza da qualche parte localmente sul server delle risorse dopo l’authorization o qual è il modo migliore per implementare questo tipo di memorizzazione delle informazioni utente se non c’è nulla di pronto all’uso?

La soluzione è l’implementazione di un UserInfoTokenServices personalizzato

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

Fornisci la tua implementazione personalizzata come bean e verrà utilizzata al posto di quella predefinita.

All’interno di questo UserInfoTokenServices è ansible creare il principal come si desidera.

Questo UserInfoTokenServices viene utilizzato per estrarre gli UserDetails dalla risposta dell’endpoint /users del server delle autorizzazioni. Come puoi vedere in

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

Solo le proprietà specificate in PRINCIPAL_KEYS vengono estratte per impostazione predefinita. E questo è esattamente il tuo problema. Devi estrarre più del solo nome utente o qualunque sia il nome della tua proprietà. Quindi cerca altre chiavi.

 private Object getPrincipal(Map 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; } 

Cablaggio:

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

!! UPDATE con Spring Boot 1.4 le cose stanno diventando più facili !!

Con Spring Boot 1.4.0 è stato introdotto un PrincipalExtractor . Questa class deve essere implementata per estrarre un’entity framework personalizzata (vedere Note di rilascio di Spring Boot 1.4 ).

Tutti i dati sono già nell’object Principal, non è necessaria una seconda richiesta. Ritorna solo ciò di cui hai bisogno. Io uso il metodo seguente per l’accesso a Facebook:

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

Nel server delle risorse è ansible creare una class CustomPrincipal. In questo modo:

 public class CustomPrincipal { public CustomPrincipal(){}; private String email; //Getters and Setters public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } } 

Implementa un CustomUserInfoTokenServices come questo:

 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 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 map) { Object principal = getPrincipal(map); List 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 map) { CustomPrincipal customPrincipal = new CustomPrincipal(); if( map.containsKey("principal") ) { Map principalMap = (Map) 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 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.singletonMap("error", "Could not fetch user details"); } } } 

Uno strumento principale personalizzato:

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

Nel tuo file @Configuration definisci un bean come questo

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

E nella configurazione del server di risorse:

 @Configuration public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter { @Override public void configure(ResourceServerSecurityConfigurer config) { config.tokenServices(myUserInfoTokenServices()); } //etc.... 

Se tutto è impostato correttamente, puoi fare qualcosa di simile nel tuo controller:

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

Spero che questo ti aiuti.

Puoi utilizzare token JWT. Non è necessario il datastore in cui sono archiviate tutte le informazioni dell’utente, ma è ansible codificare informazioni aggiuntive nel token stesso. Quando il token viene decodificato, l’app sarà in grado di accedere a tutte queste informazioni utilizzando l’object principale

Una rappresentazione Map dell’object JSON restituito dall’endpoint userdetails è disponibile dall’object Authentication che rappresenta il preside:

 Map details = (Map)oauth2.getUserAuthentication().getDetails(); 

Se si desidera catturarlo per la registrazione, l’archiviazione o il caricamento in cache, consigliamo di acquisirlo implementando un ApplicationListener . Per esempio:

 @Component public class AuthenticationSuccessListener implements ApplicationListener { 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 details = (Map)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()); } } } 

Se si desidera personalizzare in modo specifico l’estrazione del principal dal JSON o dalle autorità, è ansible implementare org.springframework.boot.autoconfigure.security.oauth2.resource.PrincipalExtractor e / org.springframework.boot.autoconfigure.security.oauth2.resource.AuthoritiesExtractor rispettivamente.

Quindi, in una class @Configuration le tue implementazioni come bean:

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

Lo recuperiamo dal metodo getContext di SecurityContextHolder, che è statico e quindi può essere recuperato da qualsiasi luogo.

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