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