spring 如何从 OAuth2 授权服务器/用户端点获取自定义用户信息
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/35056169/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
How to get custom user info from OAuth2 authorization server /user endpoint
提问by Sergey Pauk
I have a resource server configured with @EnableResourceServer
annotation and it refers to authorization server via user-info-uri
parameter as follows:
我有一个配置了@EnableResourceServer
注解的资源服务器,它通过user-info-uri
参数引用授权服务器,如下所示:
security:
oauth2:
resource:
user-info-uri: http://localhost:9001/user
Authorization server /user endpoint returns an extension of org.springframework.security.core.userdetails.User
which has e.g. an email:
授权服务器/用户端点返回一个扩展名,org.springframework.security.core.userdetails.User
例如一个电子邮件:
{
"password":null,
"username":"myuser",
...
"email":"[email protected]"
}
Whenever some resource server endpoint is accessed Spring verifies the access token behind the scenes by calling the authorization server's /user
endpoint and it actually gets back the enriched user info (which contains e.g. email info, I've verified that with Wireshark).
每当访问某个资源服务器端点时,Spring 都会通过调用授权服务器的/user
端点来验证幕后的访问令牌,并且它实际上会取回丰富的用户信息(其中包含例如电子邮件信息,我已经使用 Wireshark 进行了验证)。
So the question is how do I get this custom user info without an explicit second call to the authorization server's /user
endpoint. Does Spring store it somewhere locally on the resource server after authorization or what is the best way to implement this kind of user info storing if there's nothing available out of the box?
所以问题是如何在没有显式第二次调用授权服务器/user
端点的情况下获取此自定义用户信息。Spring 是否在授权后将其存储在资源服务器的本地某个位置,或者如果没有任何现成可用的东西,那么实现这种用户信息存储的最佳方法是什么?
回答by Yannic Klem
The solution is the implementation of a custom UserInfoTokenServices
解决方案是实现自定义 UserInfoTokenServices
Just Provide your custom implementation as a Bean and it will be used instead of the default one.
只需将您的自定义实现作为 Bean 提供,它将被用来代替默认实现。
Inside this UserInfoTokenServices you can build the principal
like you want to.
在这个 UserInfoTokenServices 中,你可以构建principal
你想要的。
This UserInfoTokenServices is used to extract the UserDetails out of the response of the /users
endpoint of your authorization server. As you can see in
此 UserInfoTokenServices 用于从/users
授权服务器端点的响应中提取 UserDetails 。正如你所看到的
private Object getPrincipal(Map<String, Object> map) {
for (String key : PRINCIPAL_KEYS) {
if (map.containsKey(key)) {
return map.get(key);
}
}
return "unknown";
}
Only the properties specified in PRINCIPAL_KEYS
are extracted by default. And thats exactly your problem. You have to extract more than just the username or whatever your property is named. So look for more keys.
PRINCIPAL_KEYS
默认情况下仅提取 中指定的属性。这正是你的问题。您必须提取的不仅仅是用户名或您的财产名称。所以寻找更多的钥匙。
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;
}
Wiring:
接线:
@Autowired
private ResourceServerProperties sso;
@Bean
public ResourceServerTokenServices myUserInfoTokenServices() {
return new MyUserInfoTokenServices(sso.getUserInfoUri(), sso.getClientId());
}
!!UPDATE with Spring Boot 1.4 things are getting easier!!
!!更新 Spring Boot 1.4 事情变得更容易了!!
With Spring Boot 1.4.0 a PrincipalExtractorwas introduced. This class should be implemented to extract a custom principal (see Spring Boot 1.4 Release Notes).
在 Spring Boot 1.4.0中引入了PrincipalExtractor。应实现此类以提取自定义主体(请参阅Spring Boot 1.4 发行说明)。
回答by Sergey Pauk
All the data is already in the Principal object, no second request is necessary. Return only what you need. I use the method below for Facebook login:
所有数据都已经在 Principal 对象中,不需要第二次请求。只返回您需要的东西。我使用以下方法登录 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;
}
回答by Paolo Mastinu
In the Resource server you can create a CustomPrincipal Class Like this:
在资源服务器中,您可以创建一个 CustomPrincipal 类,如下所示:
public class CustomPrincipal {
public CustomPrincipal(){};
private String email;
//Getters and Setters
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
Implement a CustomUserInfoTokenServices like this:
像这样实现一个 CustomUserInfoTokenServices:
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");
}
}
}
A Custom PrincipalExtractor:
自定义 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;
}
}
In your @Configuration file define a bean like this one
在你的@Configuration 文件中定义一个像这样的 bean
@Bean
public ResourceServerTokenServices myUserInfoTokenServices() {
return new CustomUserInfoTokenServices(sso.getUserInfoUri(), sso.getClientId());
}
And in the Resource Server Configuration:
在资源服务器配置中:
@Configuration
public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer config) {
config.tokenServices(myUserInfoTokenServices());
}
//etc....
If everything is set correctly you can do something like this in your controller:
如果一切设置正确,您可以在控制器中执行以下操作:
String userEmail = ((CustomPrincipal) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getEmail();
Hope this helps.
希望这可以帮助。
回答by Mark
A Map
representation of the JSON object returned by the userdetails endpoint is available from the Authentication
object that represents the Principal:
阿Map
由端点的UserDetails返回的JSON对象的表示可以从Authentication
一个表示Principal对象:
Map<String, Object> details = (Map<String,Object>)oauth2.getUserAuthentication().getDetails();
If you want to capture it for logging, storage or cacheing I'd recommend capturing it by implementing an ApplicationListener
. For example:
如果您想捕获它以进行日志记录、存储或缓存,我建议您通过实现ApplicationListener
. 例如:
@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());
}
}
}
If you specifically want to customize the extraction of the principal from the JSON or the authorities then you could implement org.springframework.boot.autoconfigure.security.oauth2.resource.PrincipalExtractor
and/ org.springframework.boot.autoconfigure.security.oauth2.resource.AuthoritiesExtractor
respectively.
如果您特别想自定义从 JSON 或权限中提取主体,那么您可以分别实现org.springframework.boot.autoconfigure.security.oauth2.resource.PrincipalExtractor
和/ org.springframework.boot.autoconfigure.security.oauth2.resource.AuthoritiesExtractor
。
Then, in a @Configuration
class you would expose your implementations as beans:
然后,在一个@Configuration
类中,您将实现作为 bean 公开:
@Bean
public PrincipalExtractor merckPrincipalExtractor() {
return new MyPrincipalExtractor();
}
@Bean
public AuthoritiesExtractor merckAuthoritiesExtractor() {
return new MyAuthoritiesExtractor();
}
回答by Jose Martinez
We retrieve it from the SecurityContextHolder's getContext method, which is static, and hence can be retrieved from anywhere.
我们从 SecurityContextHolder 的 getContext 方法中检索它,该方法是静态的,因此可以从任何地方检索。
// 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;
}
回答by vladsfl
You can use JWT tokens. You won't need datastore where all user information is stored instead you can encode additional information into the token itself. When token is decoded you app will be able to access all this information using Principal object
您可以使用 JWT 令牌。您不需要存储所有用户信息的数据存储,而是可以将附加信息编码到令牌本身中。当令牌被解码时,您的应用程序将能够使用 Principal 对象访问所有这些信息