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?
6 réponses
La solution est la mise en œuvre d'un custom UserInfoTokenServices
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' /users
endpoint 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).
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;
}
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.
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
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.PrincipalExtractor
org.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();
}
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;
}