如何在 UserDetailsService 中获取当前用户身份验证
How to get current user authentication inside UserDetailsService
在我的应用程序中,我试图将 ActiveDirectory
身份验证与 OAuth2
刷新令牌结合起来。
我能够通过 ActiveDirectoryLdapAuthenticationProvider
成功进行身份验证。我还提供了 LdapUserDetailsMapper
的自定义实现,它使用取自 ActiveDirectory
的一些自定义属性填充 UserDetails
。这里的关键是这些属性设置了机密标志,并且只对用户本身可用(即经过身份验证的用户可以为自己读取这些属性的值,但不能为其他人读取)。这些属性存储在 Authentication
对象中,并由应用程序在经过身份验证的用户的上下文中使用。
当我尝试向图片添加刷新标记时,事情变得棘手。刷新令牌要求我实现一个 UserDetailsService
,我必须在其中提供只有一个用户名的新 UserDetails
。由于机密标志,这是不可行的。即使我的应用程序中有一些能够浏览 ActiveDirectory
的主帐户,我也将无法检索机密属性。
所以我宁愿提供更多的原子实现,比如检查用户是否仍然活跃的函数或者提供一组更新的用户权限的函数。不幸的是,我没有在 Spring Security
中找到这种级别的原子性。因此,对于刷新令牌,我似乎必须提供 UserDetailsService
.
的实现
如果我必须提供新用户详细信息,我想访问以前的用户 Authentication
对象。在这种情况下,我将检查用户,如果它仍然活跃,我将复制之前 Authentication
的所有机密信息。问题是它似乎不可用。目前调用UserDetailsService::loadUserByUsername()
时SecurityContextHolder.getContext()
不包含用户认证。 UserDetailsService
API 也不提供身份验证 - 我只得到用户名。同时,用户的 Authentication 对象仅出现在 UserDetailsByNameServiceWrapper
class:
中的一个堆栈帧中
public UserDetails loadUserDetails(T authentication) throws UsernameNotFoundException {
return this.userDetailsService.loadUserByUsername(authentication.getName());
}
我在这里最不想做的是为所有用户机密信息实现一些内存存储,以便在我需要提供新的 UserDetails
时使用。我已经拥有由 Spring
管理的用户身份验证所需的所有信息,在我这边做这件事似乎只是多余的。
这里是问题列表:
- 如果你觉得我从应用安全架构的角度做的事情非常错误,请告诉我
- 有没有办法告诉
Spring
在刷新令牌过程中使用以前的 UserDetails
对象,以便应用程序可以回答用户是否仍处于活动状态并且应该被授予新访问权限的问题令牌(根本不提供 UserDetailsService
)?
- 有没有办法在调用
UserDetailsService::loadUserByUsername()
期间获取以前的用户 Authentication
对象,以便我可以将其用作机密信息的来源?
- 是否有一些我目前没有看到的其他方法可以将刷新令牌添加到我的应用程序?
更新:
Here 我看到一条评论说您可以实施自己的 AuthenticationUserDetailsService
来解决该问题。我不知道该怎么做。它在 AuthorizationServerEndpointsConfigurer
中被硬编码,它总是创建一个 UserDetailsByNameServiceWrapper
的实例,因此要提供您自己的实现,您将不得不干预 AuthorizationServerEndpointsConfigurer
初始化过程。
好的,看起来 Spring Security 4.0
的答案是 你不能。
所以我不得不应用以下有效的 hack,但我不太喜欢它。因为它有效,所以我将其发布在这里。由于它没有解决原始问题,而是解决了它,所以我不会将其标记为已被作者接受。
- 切换到 JWT 令牌
- 使用自定义
TokenEnhancer
将重新创建用户所需的所有信息(在我的例子中是用户机密)直接注入令牌。当然,这个值在添加到token中之前,必须要经过服务端的对称加密算法加密。
- 指示授权服务器使用自定义
AccessTokenConverter
。 AccessTokenConverter
的这种实现将从令牌中提取秘密值,对其进行解密并将其放入 ThreadLocal
字段。
- 指示自定义
UserDetailsService
从 step 3
中设置的 ThreadLocal
字段中检索用户密码。这是迄今为止我发现的将当前授权上下文传递给 UserDetailsService
的最佳方式。这是我解决方案中最不喜欢的部分。
- 使用自定义安全过滤器从
ThreadLocal
字段中删除 step 3
中设置的值。
P.S。我仍然看不到实现前面提到的自定义 AuthenticationUserDetailsService
的可能性。如果存在这种可能性,那可能是另一种解决问题的方法。
一些有用的链接:
我收到了 Joe Grandja on spring-security-oauth github page 的回复。
将它张贴在这里是因为它实际上提供了原始问题的答案。
嗨@masm22。为了帮助解决问题 1 和 2,下面是一个自定义配置,它允许您连接到 refresh_token 授权并提供您自己的行为或委托给 super 以继续当前行为。它还将允许您访问用户身份验证,以便您可以阅读您的自定义(机密)属性。
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
..... // other config
@Autowired
private ClientDetailsService clientDetailsService;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenServices(this.customTokenServices());
}
private DefaultTokenServices customTokenServices() {
DefaultTokenServices tokenServices = new CustomTokenServices();
tokenServices.setTokenStore(new InMemoryTokenStore());
tokenServices.setSupportRefreshToken(true);
tokenServices.setReuseRefreshToken(true);
tokenServices.setClientDetailsService(this.clientDetailsService);
return tokenServices;
}
private static class CustomTokenServices extends DefaultTokenServices {
private TokenStore tokenStore;
@Override
public OAuth2AccessToken refreshAccessToken(String refreshTokenValue, TokenRequest tokenRequest) throws AuthenticationException {
OAuth2RefreshToken refreshToken = this.tokenStore.readRefreshToken(refreshTokenValue);
OAuth2Authentication authentication = this.tokenStore.readAuthenticationForRefreshToken(refreshToken);
// Check attributes in the authentication and
// decide whether to grant the refresh token
boolean allowRefresh = true;
if (!allowRefresh) {
// throw UnauthorizedClientException or something similar
}
return super.refreshAccessToken(refreshTokenValue, tokenRequest);
}
@Override
public void setTokenStore(TokenStore tokenStore) {
super.setTokenStore(tokenStore);
this.tokenStore = tokenStore;
}
}
}
我想为您指出的另一件事是 DefaultTokenServices.refreshAccessToken(String refreshTokenValue, TokenRequest tokenRequest)
有以下代码:
OAuth2Authentication authentication = tokenStore.readAuthenticationForRefreshToken(refreshToken);
if (this.authenticationManager != null && !authentication.isClientOnly()) {
// The client has already been authenticated, but the user authentication might be old now, so give it a
// chance to re-authenticate.
Authentication user = new PreAuthenticatedAuthenticationToken(authentication.getUserAuthentication(), "", authentication.getAuthorities());
user = authenticationManager.authenticate(user);
Object details = authentication.getDetails();
authentication = new OAuth2Authentication(authentication.getOAuth2Request(), user);
authentication.setDetails(details);
}
用户正在 re-authenticated。如果需要,您可能希望在自定义实现中执行某些操作。
在我的应用程序中,我试图将 ActiveDirectory
身份验证与 OAuth2
刷新令牌结合起来。
我能够通过 ActiveDirectoryLdapAuthenticationProvider
成功进行身份验证。我还提供了 LdapUserDetailsMapper
的自定义实现,它使用取自 ActiveDirectory
的一些自定义属性填充 UserDetails
。这里的关键是这些属性设置了机密标志,并且只对用户本身可用(即经过身份验证的用户可以为自己读取这些属性的值,但不能为其他人读取)。这些属性存储在 Authentication
对象中,并由应用程序在经过身份验证的用户的上下文中使用。
当我尝试向图片添加刷新标记时,事情变得棘手。刷新令牌要求我实现一个 UserDetailsService
,我必须在其中提供只有一个用户名的新 UserDetails
。由于机密标志,这是不可行的。即使我的应用程序中有一些能够浏览 ActiveDirectory
的主帐户,我也将无法检索机密属性。
所以我宁愿提供更多的原子实现,比如检查用户是否仍然活跃的函数或者提供一组更新的用户权限的函数。不幸的是,我没有在 Spring Security
中找到这种级别的原子性。因此,对于刷新令牌,我似乎必须提供 UserDetailsService
.
如果我必须提供新用户详细信息,我想访问以前的用户 Authentication
对象。在这种情况下,我将检查用户,如果它仍然活跃,我将复制之前 Authentication
的所有机密信息。问题是它似乎不可用。目前调用UserDetailsService::loadUserByUsername()
时SecurityContextHolder.getContext()
不包含用户认证。 UserDetailsService
API 也不提供身份验证 - 我只得到用户名。同时,用户的 Authentication 对象仅出现在 UserDetailsByNameServiceWrapper
class:
public UserDetails loadUserDetails(T authentication) throws UsernameNotFoundException {
return this.userDetailsService.loadUserByUsername(authentication.getName());
}
我在这里最不想做的是为所有用户机密信息实现一些内存存储,以便在我需要提供新的 UserDetails
时使用。我已经拥有由 Spring
管理的用户身份验证所需的所有信息,在我这边做这件事似乎只是多余的。
这里是问题列表:
- 如果你觉得我从应用安全架构的角度做的事情非常错误,请告诉我
- 有没有办法告诉
Spring
在刷新令牌过程中使用以前的UserDetails
对象,以便应用程序可以回答用户是否仍处于活动状态并且应该被授予新访问权限的问题令牌(根本不提供UserDetailsService
)? - 有没有办法在调用
UserDetailsService::loadUserByUsername()
期间获取以前的用户Authentication
对象,以便我可以将其用作机密信息的来源? - 是否有一些我目前没有看到的其他方法可以将刷新令牌添加到我的应用程序?
更新:
Here 我看到一条评论说您可以实施自己的 AuthenticationUserDetailsService
来解决该问题。我不知道该怎么做。它在 AuthorizationServerEndpointsConfigurer
中被硬编码,它总是创建一个 UserDetailsByNameServiceWrapper
的实例,因此要提供您自己的实现,您将不得不干预 AuthorizationServerEndpointsConfigurer
初始化过程。
好的,看起来 Spring Security 4.0
的答案是 你不能。
所以我不得不应用以下有效的 hack,但我不太喜欢它。因为它有效,所以我将其发布在这里。由于它没有解决原始问题,而是解决了它,所以我不会将其标记为已被作者接受。
- 切换到 JWT 令牌
- 使用自定义
TokenEnhancer
将重新创建用户所需的所有信息(在我的例子中是用户机密)直接注入令牌。当然,这个值在添加到token中之前,必须要经过服务端的对称加密算法加密。 - 指示授权服务器使用自定义
AccessTokenConverter
。AccessTokenConverter
的这种实现将从令牌中提取秘密值,对其进行解密并将其放入ThreadLocal
字段。 - 指示自定义
UserDetailsService
从step 3
中设置的ThreadLocal
字段中检索用户密码。这是迄今为止我发现的将当前授权上下文传递给UserDetailsService
的最佳方式。这是我解决方案中最不喜欢的部分。 - 使用自定义安全过滤器从
ThreadLocal
字段中删除step 3
中设置的值。
P.S。我仍然看不到实现前面提到的自定义 AuthenticationUserDetailsService
的可能性。如果存在这种可能性,那可能是另一种解决问题的方法。
一些有用的链接:
我收到了 Joe Grandja on spring-security-oauth github page 的回复。
将它张贴在这里是因为它实际上提供了原始问题的答案。
嗨@masm22。为了帮助解决问题 1 和 2,下面是一个自定义配置,它允许您连接到 refresh_token 授权并提供您自己的行为或委托给 super 以继续当前行为。它还将允许您访问用户身份验证,以便您可以阅读您的自定义(机密)属性。
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
..... // other config
@Autowired
private ClientDetailsService clientDetailsService;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenServices(this.customTokenServices());
}
private DefaultTokenServices customTokenServices() {
DefaultTokenServices tokenServices = new CustomTokenServices();
tokenServices.setTokenStore(new InMemoryTokenStore());
tokenServices.setSupportRefreshToken(true);
tokenServices.setReuseRefreshToken(true);
tokenServices.setClientDetailsService(this.clientDetailsService);
return tokenServices;
}
private static class CustomTokenServices extends DefaultTokenServices {
private TokenStore tokenStore;
@Override
public OAuth2AccessToken refreshAccessToken(String refreshTokenValue, TokenRequest tokenRequest) throws AuthenticationException {
OAuth2RefreshToken refreshToken = this.tokenStore.readRefreshToken(refreshTokenValue);
OAuth2Authentication authentication = this.tokenStore.readAuthenticationForRefreshToken(refreshToken);
// Check attributes in the authentication and
// decide whether to grant the refresh token
boolean allowRefresh = true;
if (!allowRefresh) {
// throw UnauthorizedClientException or something similar
}
return super.refreshAccessToken(refreshTokenValue, tokenRequest);
}
@Override
public void setTokenStore(TokenStore tokenStore) {
super.setTokenStore(tokenStore);
this.tokenStore = tokenStore;
}
}
}
我想为您指出的另一件事是 DefaultTokenServices.refreshAccessToken(String refreshTokenValue, TokenRequest tokenRequest)
有以下代码:
OAuth2Authentication authentication = tokenStore.readAuthenticationForRefreshToken(refreshToken);
if (this.authenticationManager != null && !authentication.isClientOnly()) {
// The client has already been authenticated, but the user authentication might be old now, so give it a
// chance to re-authenticate.
Authentication user = new PreAuthenticatedAuthenticationToken(authentication.getUserAuthentication(), "", authentication.getAuthorities());
user = authenticationManager.authenticate(user);
Object details = authentication.getDetails();
authentication = new OAuth2Authentication(authentication.getOAuth2Request(), user);
authentication.setDetails(details);
}
用户正在 re-authenticated。如果需要,您可能希望在自定义实现中执行某些操作。