zuul starter mkyong example ejemplo spring spring-security spring-boot spring-cloud spring-oauth2

spring - starter - Cómo obtener información de usuario personalizada del servidor de autorización OAuth2/punto final de usuario



spring-cloud-starter-oauth2 (6)

En el servidor de recursos puede crear una clase CustomPrincipal como esta:

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

Implemente un CustomUserInfoTokenServices como este:

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

Un PrincipalExtractor personalizado:

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

En su archivo @Configuration, defina un bean como este

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

Y en la configuración del servidor de recursos:

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

Si todo está configurado correctamente, puede hacer algo como esto en su controlador:

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

Espero que esto ayude.

Tengo un servidor de recursos configurado con la anotación @EnableResourceServer y se refiere al servidor de autorización a través user-info-uri parámetro user-info-uri siguiente manera:

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


El servidor de autorización / punto final de usuario devuelve una extensión de org.springframework.security.core.userdetails.User que tiene, por ejemplo, un correo electrónico:

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


Cada vez que se accede a algún punto final del servidor de recursos, Spring verifica el token de acceso detrás de escena llamando al punto final del servidor de autorización /user y en realidad recupera la información de usuario enriquecida (que contiene, por ejemplo, información de correo electrónico, lo he verificado con Wireshark).

Entonces, la pregunta es cómo obtengo esta información de usuario personalizada sin una segunda llamada explícita al punto final del servidor de autorización /user . ¿Spring lo almacena en algún lugar local en el servidor de recursos después de la autorización o cuál es la mejor manera de implementar este tipo de almacenamiento de información del usuario si no hay nada disponible de inmediato?


La solución es la implementación de un UserInfoTokenServices personalizado

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

Simplemente proporcione su implementación personalizada como Bean y se utilizará en lugar de la predeterminada.

Dentro de este UserInfoTokenServices puedes construir el principal como quieras.

Este UserInfoTokenServices se utiliza para extraer los detalles del usuario de la respuesta del punto final /users de su servidor de autorización. Como puedes ver en

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

Solo las propiedades especificadas en PRINCIPAL_KEYS se extraen de forma predeterminada. Y ese es exactamente tu problema. Debe extraer más que solo el nombre de usuario o el nombre de su propiedad. Así que busca más llaves.

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

Alambrado:

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

¡¡¡ACTUALIZACIÓN con Spring Boot 1.4 las cosas se están volviendo más fáciles !!

Con Spring Boot 1.4.0 se introdujo un PrincipalExtractor . Esta clase debe implementarse para extraer un principal personalizado (consulte las Notas de la versión de Spring Boot 1.4 ).


Lo recuperamos del método getContext de SecurityContextHolder, que es estático y, por lo tanto, se puede recuperar desde cualquier lugar.

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


Puedes usar tokens JWT. No necesitará un almacén de datos donde se almacene toda la información del usuario; en cambio, puede codificar información adicional en el token. Cuando se decodifique el token, su aplicación podrá acceder a toda esta información utilizando el objeto Principal


Todos los datos ya están en el objeto Principal, no es necesaria una segunda solicitud. Devuelve solo lo que necesitas. Utilizo el siguiente método para iniciar sesión en 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; }


Una representación de Map del objeto JSON devuelta por el punto final de detalles del usuario está disponible en el objeto de Authentication que representa el Principal:

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

Si desea capturarlo para el registro, el almacenamiento o el almacenamiento en caché, recomiendo capturarlo implementando un ApplicationListener . Por ejemplo:

@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 desea personalizar específicamente la extracción del principal del JSON o de las autoridades, puede implementar org.springframework.boot.autoconfigure.security.oauth2.resource.PrincipalExtractor y / org.springframework.boot.autoconfigure.security.oauth2.resource.AuthoritiesExtractor respectivamente.

Luego, en una clase @Configuration expondría sus implementaciones como beans:

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